Compare commits

...

862 Commits

Author SHA1 Message Date
ziajka
05d9ee8499 Re-release v2.1.4 due to travis issue 2018-03-14 15:28:15 +01:00
ziajka
b91fd4a0c2 Development on v2.1.5dev1 2018-03-12 09:25:41 +01:00
ziajka
718217e332 Release v2.1.4 2018-03-12 09:17:16 +01:00
ziajka
c202c5e4be Move connect to update settings into one place 2018-03-09 13:31:55 +01:00
ziajka
71830dd69f Merge pull request #2449 from GNS3/update-nodes
Update node on server on any change, Fixes: #2429
2018-03-09 12:55:48 +01:00
ziajka
37a7fdfa68 Update node on server on any change, Fixes: #2429 2018-03-09 12:54:29 +01:00
grossmj
0efe006cad Mark IOU layer 1 keepalive messages feature as non-functional. Fixes #2431. 2018-03-05 16:44:42 +07:00
ziajka
4a663a5910 Fix typo 2018-02-27 16:08:33 +01:00
ziajka
a559bd4ae4 Images refresh when added via settings, Fixes:#2423 2018-02-27 16:07:06 +01:00
ziajka
5ebb3011d3 Merge pull request #2433 from GNS3/show-if-labels-on-new-project
Show labels on the new project, Fixes: #2308
2018-02-19 13:05:22 +01:00
ziajka
81300fd40e Adjust tests 2018-02-19 12:55:52 +01:00
ziajka
d4dda2a285 Emit project_loaded_signal after project creation 2018-02-19 12:54:36 +01:00
ziajka
5a4342d4b8 Add option Show interface labels on new project, Ref. #2308 2018-02-16 14:32:07 +01:00
ziajka
94fc5e6c4f Improve finding pyuic3.exe on Windows 2018-02-16 14:30:49 +01:00
grossmj
a3e81fbf2e Use debug for error downloading file messages. Fixes #2398. 2018-02-07 16:12:50 +08:00
grossmj
514eb97eac Merge remote-tracking branch 'origin/2.1' into 2.1 2018-02-06 15:38:38 +08:00
grossmj
7637039cb2 Refresh buttons in the cloud node to query the server for available interfaces. Fixes #2416. 2018-02-06 15:36:27 +08:00
Jeremy Grossmann
ac989b191b Merge pull request #2410 from GNS3/new-appliance-symbol-from-controller
Appliance import looks for symbols on server, Fixes. #2405
2018-02-02 10:24:32 +01:00
Dominik Ziajka
c971cef31b Handle Certifacte Error, Ref. gns3-server#1262 2018-02-02 10:02:18 +01:00
Dominik Ziajka
c1af2df780 Backward compatibility for tests, Ref. #2405? 2018-02-02 08:47:56 +01:00
grossmj
eaaa141be9 Use UTF-8 for IOURC file migration. 2018-02-02 15:41:42 +08:00
ziajka
226169cdc6 Look for symbols on controller, Ref. #2405 2018-02-01 17:42:02 +01:00
grossmj
42a4c89f20 Display an error message if Telnet console program cannot be executed. 2018-01-29 18:59:28 +07:00
grossmj
1482b0e804 Back to development on v2.1.4dev1 2018-01-21 15:57:41 +07:00
grossmj
8ebe3435c4 Re-release v2.1.3 to fix idna packaging issue. 2018-01-21 15:16:25 +07:00
ziajka
a1cd34d7c4 Back to development on v2.1.4dev1 2018-01-19 08:18:19 +01:00
ziajka
1e4a44135c Re-release v2.1.3 2018-01-19 08:11:46 +01:00
ziajka
a407f1ec90 Update to python3.6 in tests - running xvfb 2018-01-19 08:05:07 +01:00
ziajka
faab113384 Update to python3.6 in tests 2018-01-19 08:01:11 +01:00
ziajka
c158b7fc46 Use Ubuntu 17.10 for TCI tests 2018-01-19 07:55:13 +01:00
ziajka
16de9e830f Use Ubuntu 16.04 for TCI tests 2018-01-19 07:44:37 +01:00
ziajka
25c625c0bb Development on v2.1.4dev1 2018-01-19 07:17:22 +01:00
ziajka
bf42d1a355 Release v2.1.3 2018-01-19 07:15:24 +01:00
grossmj
1c0f3493ee Fix more client/server version tests. 2018-01-18 16:14:09 +08:00
grossmj
c3c1f87c5e Change messages when there are different client and server versions. Fixes #2391. 2018-01-18 15:58:21 +08:00
grossmj
6b80914385 Bump version number to 2.1.3dev1 2018-01-18 15:32:06 +08:00
grossmj
a114d9ace7 Fix "Transport selection via DSN is deprecated" message. Sync is configured with HTTPTransport. 2018-01-15 16:56:16 +07:00
grossmj
4dca4d057a Refresh CPU/RAM info every 1 second. Ref #2262. 2018-01-15 14:42:01 +07:00
grossmj
17af21e29a Only check for AVG on Windows 2018-01-14 13:40:31 +07:00
grossmj
7fbce0266d Improve the search for VBoxManage. 2018-01-11 16:33:15 +07:00
grossmj
d5cdbdbf90 Allow telnet console to node with name containing double quotes. Fixes #2371. 2018-01-10 22:16:35 +07:00
ziajka
e5a790f4b2 Development v2.1.3dev1 2018-01-08 14:21:28 +01:00
ziajka
f3769df0d6 Add to CHANGELOG changes 2018-01-08 14:09:21 +01:00
ziajka
a21db74941 Release v2.1.2 2018-01-08 14:08:12 +01:00
grossmj
d1e1f6dfb6 Update VMware promotion in setup wizard. 2018-01-08 18:41:40 +07:00
grossmj
cc45c9631a Confirm exit. Fixes #2359. 2018-01-08 18:00:59 +07:00
ziajka
d16a52e389 Development on v2.1.2dev1 2017-12-22 13:28:39 +01:00
ziajka
ee2bea7cdd Release v2.1.1 2017-12-22 13:26:55 +01:00
grossmj
7cbc25cbbf Bump version to 2.1.1dev2 2017-12-20 11:20:07 +01:00
ziajka
7237cf1b88 Merge pull request #2373 from GNS3/fix-2363
Fix dragging appliance into topology from nodes window, fixes: #2363
2017-12-20 09:54:05 +01:00
ziajka
965923900b Fix dragging appliance into topology from nodes window, fixes: #2363 2017-12-20 09:53:19 +01:00
ziajka
a5a3a4e8cc Merge pull request #2372 from GNS3/fix-2362-2
Fix Appliances in Docked mode, fixes: #2362
2017-12-20 09:29:21 +01:00
ziajka
d898b30d84 Fix Appliances in Docked mode, fixes: #2362 2017-12-20 09:28:33 +01:00
ziajka
e86ced750e Merge pull request #2370 from GNS3/fix-2366
Create local variable in order to debug issue in the next occurrence,…
2017-12-19 14:48:44 +01:00
ziajka
e15b717cb0 Create local variable in order to debug issue in the next occurrence, #2366 2017-12-19 14:46:24 +01:00
ziajka
d8bd33f0e7 Merge pull request #2369 from GNS3/fix-2364
Fix ParseError: not well-formed (invalid token), #2364
2017-12-19 12:52:25 +01:00
ziajka
bc2fbe33ef Fix ParseError: not well-formed (invalid token), #2364 2017-12-19 12:51:20 +01:00
ziajka
b99b26f463 Merge pull request #2368 from GNS3/fix-2365
Fix local variable 'vm' referenced before assignment #2365
2017-12-19 11:41:30 +01:00
ziajka
5b7606793f Fix local variable 'vm' referenced before assignment #2365 2017-12-19 11:40:24 +01:00
ziajka
b8b5e8739e Merge pull request #2367 from GNS3/fix-2362
Fix: 'NodesDockWidget' object has no attribute 'uiNodesView', #2362
2017-12-19 11:38:11 +01:00
ziajka
0126c30887 Fix: 'NodesDockWidget' object has no attribute 'uiNodesView', #2362 2017-12-18 14:30:26 +01:00
grossmj
a89086ff60 Fix test. 2017-12-07 14:31:41 -06:00
grossmj
9ca35c56de Tentative fix for packet capture not working correctly when remote main server is configured. Ref #2111. 2017-12-07 13:59:02 -06:00
grossmj
3ddccf40a8 Log Qt messages with log.debug() instead of log.info(). 2017-12-05 14:24:24 -06:00
Jeremy Grossmann
1398ef323a Merge pull request #2345 from GNS3/no-timeout-snapshots
Snapshoting project without timeout but with Cancel button. Ref. #2314
2017-11-24 18:19:26 +07:00
grossmj
d52c4d839d Fix auto idle-pc from preferences. Fixes #2344. 2017-11-23 23:01:01 +07:00
ziajka
6989ee2c8b Snapshot creation, busy all the time as we cannot calculate progress. 2017-11-23 15:06:44 +01:00
ziajka
5c182e95ca Snapshoting project without timeout but with button. Ref. #2314 2017-11-23 14:13:56 +01:00
grossmj
bcb7a8e57b Improve validation for idle-pc. 2017-11-23 10:41:30 +07:00
grossmj
dba75e844e Activate faulthandler. 2017-11-21 13:44:29 +07:00
Jeremy Grossmann
6733739fa5 Merge pull request #2340 from ehlers/fix-osx-telnet
Fix OS X Telnet
2017-11-20 11:44:02 +07:00
Bernhard Ehlers
f5de62aa05 Add PATH to OS X console commands 2017-11-19 15:40:54 +01:00
Bernhard Ehlers
4087d35f6a Use raw triple quotes in large console settings
This eliminates one level of quoting
2017-11-19 15:31:26 +01:00
grossmj
f6a1af46a0 Fix issue in node summary when console is not supported by a node. 2017-11-19 20:30:58 +07:00
Bernhard Ehlers
6cef2fed5a Revert "Add preferred path to use Telnet console in DMG package. Ref #2274." 2017-11-19 12:37:21 +01:00
Bernhard Ehlers
e16c8db311 Revert "Force to use the telnet client embedded in DMG. Ref #2274." 2017-11-19 12:36:33 +01:00
Bernhard Ehlers
61c95a93ca Revert "Add debug when using Telnet path on OSX. Ref #2274." 2017-11-19 12:35:55 +01:00
Bernhard Ehlers
0df36dab30 Revert "Fix bug when replacing Telnet path on OSX. Ref #2274." 2017-11-19 12:33:54 +01:00
Bernhard Ehlers
9a5da633e0 Revert "Fix problem when embedded telnet client path contains a space on macOS. Ref #2328." 2017-11-19 12:32:29 +01:00
Bernhard Ehlers
02fed964f2 Revert "Support Telnet path containing spaces. Ref #2328." 2017-11-19 12:30:15 +01:00
grossmj
84ba56ae74 Remove unused symbols. Fixes #2320. 2017-11-19 14:50:12 +07:00
grossmj
e8c4758cb7 Show console information in Topology Summary Dock. Fixes #2258. 2017-11-19 13:56:54 +07:00
grossmj
d8dc31965f New option: require KVM. If false, Qemu VMs will not be prevented to run without KVM. 2017-11-19 12:39:37 +07:00
grossmj
9af48ba9a3 Implement variable replacement for Qemu VM options. 2017-11-18 17:36:11 +07:00
grossmj
67d8e317e0 Show on what server a node is installed in the servers summary pane. Fixes #2279. 2017-11-18 16:02:31 +07:00
grossmj
64392780c5 Add more info when cannot remove capture file after stopping packet capture in a remote project. Ref #1223. 2017-11-17 18:32:18 +07:00
grossmj
81bb159d45 Do not overwrites the disk images when copied to default directory. Fixes #2326. 2017-11-17 15:42:33 +07:00
ziajka
85352af9bd Update pyup config to use 2.2 branch 2017-11-15 13:21:14 +01:00
grossmj
65cfdf6b33 Revert "Only replace quoted telnet for macOS Telnet commands. Ref #2328." 2017-11-15 10:39:24 +07:00
grossmj
5d45dbebf6 Only replace quoted telnet for macOS Telnet commands. Ref #2328. 2017-11-14 18:42:17 +07:00
grossmj
0a7b6d81d8 Support Telnet path containing spaces. Ref #2328. 2017-11-14 18:34:06 +07:00
grossmj
588dcadd3a Fix problem when embedded telnet client path contains a space on macOS. Ref #2328. 2017-11-14 17:22:44 +07:00
grossmj
f24c93a55f Do not launch console for builtin nodes when using the "Console to all nodes" button. Fixes #2309. 2017-11-12 17:02:00 +08:00
grossmj
26e5c80406 Merge remote-tracking branch 'remotes/origin/master' into 2.1 2017-11-12 14:22:13 +08:00
ziajka
eb502232a2 Development on 2.1.1dev1 2017-11-09 10:50:12 +01:00
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
3d21f9a997 Merge pull request #2210 from CapnCheapo/patch-1
Update frame_relay_switch_configuration_page_ui.py
2017-08-08 11:14:19 +08: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
13739281da Update frame_relay_switch_configuration_page_ui.py
Fixes #2205
2017-08-07 14:04:44 -05: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
20efde749c Turn off timeout for node creation
The timeout is an issue if you use a local GNS3 server and
a remote over a slow link.

Fix https://github.com/GNS3/gns3-server/issues/1126
2017-07-19 15:26:51 +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
4e172fc7e3 2.0.2 2017-05-30 09:02:02 +02:00
Julien Duponchelle
56ebfc7fd0 Drop SSL support
Fix #1022
2017-05-26 15:52:09 +02:00
Julien Duponchelle
8ed8a2c115 Show a default symbol in case of corrupted file
Fix https://github.com/GNS3/gns3-server/issues/1043
2017-05-24 12:26:18 +02:00
Julien Duponchelle
073665a75d When another gui is already running exit instead of proper close to avoid any issue
Fix #2059
2017-05-23 18:00:02 +02:00
Julien Duponchelle
4ccc67aa46 Fix duplicate on remote server use wrong location
Fix https://github.com/GNS3/gns3-server/issues/1040
2017-05-23 17:25:29 +02:00
Julien Duponchelle
6e2632e91f Display the location of settings when we disallow opening due to old release 2017-05-23 15:54:35 +02:00
Julien Duponchelle
38ddcde902 Improve search for dynamips in development on OSX 2017-05-23 14:24:54 +02:00
Julien Duponchelle
436563afcb Merge pull request #2079 from GNS3/pyup-update-pytest-3.0.7-to-3.1.0
Update pytest to 3.1.0
2017-05-23 08:29:55 +02:00
pyup-bot
7eaab3e38b Update pytest from 3.0.7 to 3.1.0 2017-05-23 02:25:48 +02:00
Julien Duponchelle
0927a2a8c9 Fix error display when loading a .png custom symbol 2017-05-22 13:38:04 +02:00
Julien Duponchelle
87e6159ff6 Fix a crash in the progress dialog
Fix #2064
2017-05-22 09:56:31 +02:00
Julien Duponchelle
effdcf5e24 Fix a race condition when exporting a closed project
Fix #2078
2017-05-22 09:54:17 +02:00
Julien Duponchelle
021cdd2e65 Fix RuntimeError: wrapped C/C++ object of type NodeItem has been deleted
Fix #2070
2017-05-19 17:39:34 +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
363c4a9966 2.0.2dev1 2017-05-16 09:15:18 +02:00
Julien Duponchelle
7082c75511 2.0.1 2017-05-16 08:48:56 +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
grossmj
16c4a837d7 Improve inline help. Fixes #1999.
Add a warning about wifi interfaces in the cloud. Fixes #1902.
2017-05-14 22:18:35 +02:00
grossmj
fd42ac410c Copy remote directory path into clipboard in "Show in FileManager". Fixes #1966. 2017-05-12 16:49:26 +08:00
Jeremy Grossmann
6efc177804 Merge pull request #2054 from GNS3/export_thread
Do not run import / export of project in seperate thread
2017-05-12 10:23:25 +08:00
Julien Duponchelle
6f9e6c9b92 Fix display of error in progress dialog when we don't have thread 2017-05-11 17:48:38 +02:00
Jeremy Grossmann
0411c68150 Merge pull request #2057 from GNS3/lost_slot_and_port
Fix lost slot and port in dynamips settings
2017-05-11 23:37:39 +08:00
Julien Duponchelle
4246e731e5 Fix lost slot and port in dynamips settings
When you reopen a project you no longer have the
wic and slot, until you move the node and retrieve an
update. The settings is just lost in the GUI but is fine
on server.

Fix #2053
2017-05-11 09:49:47 +02:00
Julien Duponchelle
50cca71279 Do not run import / export of project in seperate thread
This trigger warning because you need to do the HTTP request
to the API from the main thread.

Fix #2008
2017-05-10 18:13:45 +02:00
Julien Duponchelle
c78ef8f348 Assert when running an HTTP query outside the main thread
Ref #2008
2017-05-10 17:57:02 +02:00
Julien Duponchelle
c03a5a9e0a Proper error when you try to load the pid file as config file
Fix #2044
2017-05-09 17:27:38 +02:00
Julien Duponchelle
c93b7836d8 Log malformed svg text item
Fix #2045
2017-05-09 17:14:02 +02:00
Julien Duponchelle
690b22cc24 Fix a race condition when right click and delete a node at the same time
Fix #2043
2017-05-09 17:03:23 +02:00
Julien Duponchelle
3560251816 Fix a race condition when snapshoting a closed project
Fix #2046
2017-05-09 16:37:42 +02:00
Jeremy Grossmann
90e861289f Merge pull request #2016 from GNS3/missing_xattr
Catch missing function listxattr on some linux host
2017-05-08 21:04:47 +07:00
Jeremy Grossmann
6e144d6122 Update doctor_dialog.py 2017-05-08 21:04:01 +07:00
grossmj
2f168193d1 Catch remaining missing function listxattr on some Linux host. 2017-05-08 21:01:27 +07:00
Jeremy Grossmann
0fe5559564 Merge pull request #2007 from GNS3/fix_project_close
Fix project closing when we have multiple client connected
2017-05-08 20:42:35 +07:00
Julien Duponchelle
37f0744d7c Fix a race condition when creating node and closing project
Fix #2018
2017-05-05 19:04:06 +02:00
Julien Duponchelle
000f4a4790 Fix Bug with python before 3.4.3
Fix #2035
2017-05-05 18:58:40 +02:00
Julien Duponchelle
a09b7d6738 Fix error if you put a path in a .gns3a file for qemu
Fix #2038
2017-05-05 18:31:13 +02:00
Julien Duponchelle
a1fa8f9ec2 Fix AttributeError: 'NoneType' object has no attribute '_refreshVisibleWidgets'
Fix #2034
2017-05-05 18:29:36 +02:00
Jeremy Grossmann
cd6b0b793e Merge pull request #2009 from GNS3/dissalow_gns3_on_remote
Disallow opening a .gns3 on a remote server
2017-05-05 20:53:29 +07:00
Jeremy Grossmann
6453932421 Update main_window.py 2017-05-05 20:52:03 +07:00
Julien Duponchelle
37a23d9682 Do not crash if the logging code raise an exception
Fix #2017
2017-05-04 12:18:26 +02:00
Julien Duponchelle
e18e10c701 Fix some crash in dynamips device preference page
Fix #2029
2017-05-04 12:02:49 +02:00
Julien Duponchelle
a9240e2e46 Fix warning when loading IOU images on Windows
Fix #2012
2017-05-04 10:43:13 +02:00
Julien Duponchelle
4df0c33013 Do not crash if you don't have configure a packet capture program on Windows
Fix #2026
2017-05-04 10:38:35 +02:00
Julien Duponchelle
4096316ceb Ignore error when we can't kill the packet capture
The user probalby manually kill it.

Fix #2013
2017-05-04 10:30:41 +02:00
Julien Duponchelle
e09292c647 Fix AttributeError: 'NoneType' object has no attribute 'wasCanceled'
Fix #2023
2017-05-04 10:27:45 +02:00
Julien Duponchelle
31c37161fa Fix RuntimeError: wrapped C/C++ object of type QComboBox has been deleted
Fix #2027
2017-05-04 10:25:46 +02:00
Julien Duponchelle
a047cd7f4c Fix RuntimeError: wrapped C/C++ object of type QTreeWidgetItem has been deleted
Fix #2028
2017-05-04 10:13:33 +02:00
Julien Duponchelle
87cde665a8 Fix detection of https when use for the local server
Ref #995
2017-05-03 17:23:03 +02:00
Julien Duponchelle
6361c94bbe Silent the _COMPIZ_TOOLKIT_ACTION warning
This a Qt bug.

Fix #2020
2017-05-03 15:54:55 +02:00
Julien Duponchelle
8a0aeff0bb Cacth TypeError: native Qt signal is not callable
Fix #2011
2017-05-03 10:58:38 +02:00
Julien Duponchelle
508f8b3ad5 Fix AttributeError: 'C7200' object has no attribute 'warning_signal'
Fix #2014
2017-05-03 10:51:12 +02:00
Julien Duponchelle
bfe942c029 Catch missing function listxattr on some linux host
Fix #2010
2017-05-03 10:47:22 +02:00
Julien Duponchelle
b3a86594ff 2.0.1dev1 2017-05-03 10:19:25 +02:00
Julien Duponchelle
debe88bd37 2.0.0 2017-05-02 10:14:01 +02:00
Julien Duponchelle
a948fd07b1 Disallow opening a .gns3 on a remote server
This prevent opening a local .gns3 on a remote server.
Because this is not working you need to import the
project on the server using portable project.

Fix https://github.com/GNS3/gns3-server/issues/984
2017-05-02 09:27:40 +02:00
Julien Duponchelle
072f714e21 Fix project closing when we have multiple client connected
In all case when we close the main window we let the server
manage if he need to close or not the project.

Fix https://github.com/GNS3/gns3-server/issues/991
2017-05-02 09:07:14 +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
a2059d3e7c Clarify that we don't override vmware custom adapters 2017-04-27 10:06:52 +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
c4cc819d50 Strip space from path at project creation
Fix #1997
2017-04-21 14:49:52 +02:00
Julien Duponchelle
0796d9aae5 2.0.0dev13 2017-04-20 10:57:28 +02:00
Julien Duponchelle
c8148c1877 2.0.0rc4 2017-04-20 10:56:55 +02:00
Julien Duponchelle
1b6d534b8e Merge branch '2.0' into 2.1 2017-04-20 10:30:58 +02:00
Julien Duponchelle
d6021afa0f Catch all error during the generation of log messages.
This prevent error loop is something goes wrong.

Fix #1993
2017-04-20 09:26:57 +02:00
Julien Duponchelle
9018bdac07 Remove no longer working capturelog plugin for pytest 2017-04-19 09:41:56 +02:00
Julien Duponchelle
a48b898d92 Catch a rare node creation error
Fix #1991
2017-04-19 09:39:32 +02:00
Julien Duponchelle
949c1bbe37 Fix missing menu text at application startup
Fix #1990
2017-04-19 09:35:31 +02:00
Julien Duponchelle
0089edecc0 Run test on windows also 2017-04-18 16:48:45 +02:00
Julien Duponchelle
91da1d492b Fix a race condition in the drawing item
Fix #1987
2017-04-18 11:32:33 +02:00
Julien Duponchelle
0242f53c17 Catch system error when connecting to local server
Fix #1988
2017-04-18 11:29:42 +02:00
Julien Duponchelle
bab7a1016f Catch a rare error when killing the capture
Fix #1985
2017-04-14 10:39:43 +02:00
Julien Duponchelle
2ec91e1ef7 Improve pcap streaming speed
Fix #1982
2017-04-14 10:30:49 +02:00
Julien Duponchelle
38af30ec15 Merge branch 'master' into 2.0 2017-04-14 09:04:17 +02:00
Julien Duponchelle
3f8a0cb527 Merge master 2017-04-14 09:03:08 +02:00
Julien Duponchelle
fd1a86df03 1.5.5dev1 2017-04-14 09:00:57 +02:00
Julien Duponchelle
c954a16ead 1.5.4 2017-04-13 17:27:37 +02:00
Julien Duponchelle
9418b997eb Mac Qt wheel is not working in our build envirronement 2017-04-13 16:07:52 +02:00
Julien Duponchelle
9682cad4f4 Add security issues section 2017-04-13 12:37:34 +02:00
Julien Duponchelle
60ee9e1374 Limit ubridge permission to the admin group on OSX 2017-04-13 10:04:08 +02:00
Julien Duponchelle
dbd5b9366a Upgrade to 5.7.1 2017-04-13 10:01:40 +02:00
Julien Duponchelle
f63314d4d0 Fix version number 2017-04-13 09:56:17 +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
96eaec0100 Recent projects list bug
Fix #1976
2017-04-10 17:59:17 +02:00
Julien Duponchelle
5d554976e2 Fix a race condition in the preferences dialog
Fix #1981
2017-04-10 11:42:13 +02:00
Julien Duponchelle
d1d8390b73 Try to fix some windows Z issues
Fix #1955
2017-04-07 11:06:51 +02:00
Julien Duponchelle
5f3f6462c2 Catch a garbage collection issue in the right click on a link 2017-04-03 16:33:14 +02:00
Julien Duponchelle
821809bf6f Fix a compatibility issue with Python 3.4
Fix #1973
2017-03-31 19:30:32 +02:00
Julien Duponchelle
a712fab7a4 2.0.0dev12 2017-03-31 09:44:27 +02:00
Julien Duponchelle
4ab21ea9f9 2.0.0rc3 2017-03-31 09:41:48 +02:00
Julien Duponchelle
ce12eb86e8 Merge branch '2.0' into 2.1 2017-03-30 10:07:55 +02:00
Julien Duponchelle
1a4902279b Check hibernation only if it's a remote main server
Fix #1971
2017-03-29 17:19:00 +02:00
Julien Duponchelle
35412171b9 Improve timeout handling
Ref #1959
2017-03-29 12:34:10 +02:00
Julien Duponchelle
73939847b9 Improve logging when we display a qt message box
Ref #1959
2017-03-29 12:12:22 +02:00
Julien Duponchelle
5acfc5bc56 Fix a problem with settings not pushed to the server 2017-03-28 16:22:17 +02:00
Julien Duponchelle
d67d0d146f Try to detect computer hibernation
I'm not really happy with the patch but it the most simple way
I found. If between two request we have more than 120 seconds
of differences we disconnect.

Fix #1968
2017-03-28 14:41:43 +02:00
Julien Duponchelle
23da6a4c31 Fix crash when we send some errors to the user console 2017-03-27 18:16:11 +02:00
Julien Duponchelle
fd5f999756 Use QtFile for managing file capture
This prevent application freeze when we run a capture.

Fix #1959
2017-03-27 14:19:07 +02:00
Julien Duponchelle
c05304b86f Allow to delete a profile from the profile select dialog
Fix #1961
2017-03-27 10:26:29 +02:00
Julien Duponchelle
6361980591 Filter hidden folder in the profil directory
Fix #1960
2017-03-23 12:02:32 +01:00
Julien Duponchelle
d5ea98ba2a Prevent user putting port in the remote host name
Fix #1948
2017-03-22 11:56:27 +01:00
Julien Duponchelle
3ca01384c9 Fix RuntimeError: wrapped C/C++ object of type EllipseItem has been deleted
Fix #1957
2017-03-21 18:19:47 +01:00
Julien Duponchelle
f35bb6a281 Fix a rare error in LinkItem
Fix #1950
2017-03-21 16:56:23 +01:00
Julien Duponchelle
0c4a7693e6 Fix Image field in nodes list is stale after changing an image
Fix #1956
2017-03-21 16:54:10 +01:00
Julien Duponchelle
20506525c5 Fix RuntimeError: Set changed size during iteration
Fix #1951
2017-03-21 15:13:49 +01:00
Julien Duponchelle
bb00a9f64c Better detection of remote server changes
Fix #1949
2017-03-21 14:38:20 +01:00
Julien Duponchelle
ef36792379 Add a notice about the fact you need to apply server settings
Fix #1952
2017-03-21 11:35:03 +01:00
Julien Duponchelle
873fd409bd Check python version only for setup.py install 2017-03-21 10:10:14 +01:00
Julien Duponchelle
03221e8ab7 Fix refresh issue with link in topology summary
Fix #1938
2017-03-20 18:24:16 +01:00
Julien Duponchelle
55f9836dc9 Remove duplicate code 2017-03-20 17:34:17 +01:00
Julien Duponchelle
3ab5144fe2 Catch appliance error when creating an appliance new version
Fix #1941
2017-03-20 17:10:49 +01:00
Julien Duponchelle
c91a22c9f8 If a node can't be deleted do not remove it
Fix #1933
2017-03-16 19:42:38 +01:00
Julien Duponchelle
a8ba909568 If something is wrong during packet capture do not disconnect us from the server
Fix #1520
2017-03-16 18:14:55 +01:00
Julien Duponchelle
d33e9ed833 If something is wrong during packet capture do not disconnect us from the server
Fix #1520
2017-03-16 14:37:32 +01:00
Julien Duponchelle
aa31af1dca Fix saving dynamips
Fix #1939
2017-03-16 14:33:16 +01:00
Julien Duponchelle
4d07a7391f Try to fix the hang dialog on some computers
Ref #1915
2017-03-16 12:19:09 +01:00
Julien Duponchelle
417395718d Fix a rare crash in progress dialog 2017-03-15 16:36:13 +01:00
Julien Duponchelle
212048c4d1 If we pass --profile skip the profile select dialog
Fix #1934
2017-03-15 16:09:35 +01:00
Julien Duponchelle
11c27063b4 Raise an error if the progress dialog is not created from the main thread
Ref #1915
2017-03-15 15:14:12 +01:00
Julien Duponchelle
b8696fa54e Log qt log to python log 2017-03-15 14:50:19 +01:00
Julien Duponchelle
ba9785f83f Revert "Rollback to Qt 5.7.1"
This reverts commit 5038a72610.
2017-03-15 14:46:57 +01:00
Julien Duponchelle
4ae742529c Fix image are not uploaded to remote main server
Fix #1926, #1927, #1925
2017-03-15 14:33:25 +01:00
Julien Duponchelle
92cc335708 Fix race condition when editing a project
Fix #1932
2017-03-15 14:30:00 +01:00
Julien Duponchelle
2099a4ae9a Poll settings each 5 seconds
Fix #1923, #1924
2017-03-15 12:55:14 +01:00
Julien Duponchelle
5038a72610 Rollback to Qt 5.7.1
Ref #1915
2017-03-15 11:51:22 +01:00
Julien Duponchelle
b9e320996b Merge pull request #1935 from GNS3/pyup-update-pytest-3.0.6-to-3.0.7
Update pytest to 3.0.7
2017-03-15 10:20:50 +01:00
pyup-bot
9c86cd71c8 Update pytest from 3.0.6 to 3.0.7 2017-03-14 23:53:42 +01:00
Julien Duponchelle
7fdf42b442 Avoid progress dialog not disapear
Fix #1915
2017-03-14 15:40:04 +01:00
Julien Duponchelle
61d86e919e Remove wrong mention about the fact super putty is include 2017-03-14 12:02:02 +01:00
Julien Duponchelle
bc71985ee3 Avoid a crash when an ios router don't have a chassis
Fix #1920
2017-03-13 17:25:58 +01:00
Julien Duponchelle
6bebf2d14d Fix a potentatial crash in the progress dialog
Fix #1919
2017-03-13 17:16:34 +01:00
Julien Duponchelle
490021aa47 Fix appliance file 2017-03-13 15:48:59 +01:00
Julien Duponchelle
c8db5e7e49 2.0.0 dev 11 2017-03-13 15:42:06 +01:00
Julien Duponchelle
8efaacbc3d Support official docker images in appliances 2017-03-13 15:41:14 +01:00
Julien Duponchelle
c5da24b954 2.0.0rc2 2017-03-10 20:40:10 +01:00
Julien Duponchelle
4456cfd68e Merge branch 'master' into 2.0 2017-03-10 20:39:03 +01:00
Julien Duponchelle
3468909db1 Deploy on pypi when we tag 2017-03-10 20:37:57 +01:00
Julien Duponchelle
212728eb94 Fix rare crash in GNS3 VM preference page
Fix #1912
2017-03-09 10:51:38 +01:00
Julien Duponchelle
cf4f73a7e1 Fix an error on Windows when loading SVG files
Fix #1913
2017-03-09 10:25:18 +01: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
5948e5c4cb Prevent a potential crash 2017-03-08 18:12:46 +01:00
Julien Duponchelle
791aa27158 Workaround a rare crash when sending analytics
It seem PyQT is confuse between the signal and error function.

Fix #1861
2017-03-08 14:52:47 +01:00
Julien Duponchelle
0b8ab56ffc Catch error when you try to create a node a not existing server
Fix #1905
2017-03-08 14:44:30 +01:00
Julien Duponchelle
12dcfda756 Fix an error when your local server crash and computer return non unicode
Fix #1904
2017-03-08 14:40:29 +01:00
Julien Duponchelle
ef44beac41 Fix KeyError: 'slot1'
Fix #1907
2017-03-08 09:54:17 +01:00
Julien Duponchelle
97a71904f7 Fix a rare crash in import appliance
Fix #1908
2017-03-08 09:53:27 +01:00
Julien Duponchelle
012bc1e406 Display the appliances in the application
Ref #1045
2017-03-07 18:10:15 +01:00
Julien Duponchelle
94f4059d67 Rollback to PyQT 5.8 because 5.8.1 seem to have trouble at install 2017-03-07 15:49:26 +01:00
Julien Duponchelle
14ed2546bc Merge pull request #1903 from GNS3/pyup-update-pyqt5-5.8-to-5.8.1
Update pyqt5 to 5.8.1
2017-03-07 13:25:40 +01:00
pyup-bot
908258c163 Update pyqt5 from 5.8 to 5.8.1 2017-03-07 12:09:10 +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
704191ba25 2.0.0 dev 10 2017-03-06 19:42:25 +01:00
Julien Duponchelle
be7fc9abe2 2.0.0rc1 2017-03-06 19:39:46 +01:00
Julien Duponchelle
e0daf1dbb1 Fix syntax error 2017-03-01 09:04:05 +01:00
Julien Duponchelle
a8877d4f8a UltraVNC support 2017-02-28 18:08:48 +01: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
2db850a3f3 Fix tests 2017-02-28 15:58:25 +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
Julien Duponchelle
ec7cdedb86 Display less noisy dialog when we can't connect to the remote server
Fix #1887
2017-02-28 14:06:52 +01:00
Julien Duponchelle
606fa15a55 Prevent the usage of gns3vm as a remote server name 2017-02-28 13:50:31 +01:00
Jeremy Grossmann
3eca9b0e54 Merge pull request #1881 from GNS3/catch_local_process_errors
Monitor and display local server stderr
2017-02-28 02:46:27 +08:00
Julien Duponchelle
3abaf74580 Fix the VMware wizard for not using a remote server by default
Fix #1893
2017-02-27 19:00:02 +01:00
Julien Duponchelle
9713748633 Prevent the GNS3 VM to appear in remote compute in the VM wizard
Fix #1894
2017-02-27 18:08:34 +01:00
Julien Duponchelle
405e86aff4 Remove iouyap settings
Fix #1892
2017-02-27 15:26:05 +01:00
Julien Duponchelle
4da824dc2b Fix missing permission error management
Fix #1888
2017-02-24 09:49:10 +01:00
Julien Duponchelle
0ec177c644 Avoid a crash when create a new dynamips version in the appliance wizard
Fix #1884
2017-02-23 15:13:00 +01:00
Julien Duponchelle
9ef4f86050 Disallow user to add the same server as a remote server and as local server
Fix #1883
2017-02-23 11:13:14 +01:00
Julien Duponchelle
96b830817e Fix 'module' object has no attribute 'run'
Fix #1878
2017-02-23 09:11:31 +01:00
Julien Duponchelle
0db8b00fb9 Monitor and display local server stderr
It's the best solution I found. Streaming in real time
the log will require to start an aditionnal thread

Fix #1880
2017-02-22 17:51:23 +01:00
Julien Duponchelle
005dde6c2b Fix some import errors 2017-02-22 17:23:58 +01:00
Julien Duponchelle
260f9b352e Remove placeholder string from appliance wizard 2017-02-22 11:02:25 +01:00
Julien Duponchelle
852b0bc498 Avoiding calling multiple time /computes at the same time. And reduce timeout
Fix #1848
2017-02-22 10:29:41 +01:00
Julien Duponchelle
3a503a5fc0 Support for appliance v4 2017-02-22 09:04:22 +01:00
Jeremy Grossmann
88b408695a Merge pull request #1875 from GNS3/hdpi_enable
Disable HDPI by default on Linux and allow to configure it
2017-02-20 20:39:19 -08:00
grossmj
776b45363b Some tweaks for enabling/disabling HDPI mode. 2017-02-21 12:33:09 +08:00
Julien Duponchelle
adb270b64c Do not display error at first step of the setup wizard
Fix #1827
2017-02-20 20:04:43 +01:00
Julien Duponchelle
5329b8fd72 Disable HDPI by default on Linux and allow to configure it
Fix #1870
2017-02-20 19:21:02 +01:00
Julien Duponchelle
d6379e4bb9 Fix an issue when you edit a VPCS node from the node view
Fix https://github.com/GNS3/gns3-gui/issues/1874
2017-02-20 18:09:48 +01:00
Julien Duponchelle
6ca18d5b29 Catch a race condition in managing error static assets download
Fix #1872
2017-02-20 15:11:44 +01:00
Julien Duponchelle
50c008ddec Handle error if you try to import an appliance without having the images
Fix #1871
2017-02-20 12:29:37 +01:00
Julien Duponchelle
4f56f100fa Improve crash proof code of the progress dialog
Fix #1873
2017-02-20 12:16:32 +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
0c16a5b0d1 2.0.0dev9 2017-02-16 11:27:49 +01:00
Julien Duponchelle
8f33ad3c70 2.0.0b4 2017-02-16 11:26:32 +01:00
Julien Duponchelle
7dca1b404c Fix a rare crash 2017-02-15 19:28:38 +01:00
Julien Duponchelle
99ff98ff47 Merge pull request #1858 from GNS3/pyup-update-pyqt5-5.7.1-to-5.8
Update pyqt5 to 5.8
2017-02-15 18:55:09 +01:00
pyup-bot
083d6e1298 Update pyqt5 from 5.7.1 to 5.8 2017-02-15 18:44:11 +01:00
Julien Duponchelle
71716c451e Drop from console view the show command not supported by 2.0
Fix #1855
2017-02-15 18:15:09 +01:00
Julien Duponchelle
3c4a244b75 Try to avoid segfault in some PyQT version
Fix #1856
2017-02-15 16:19:13 +01:00
Julien Duponchelle
85892a3bd6 Display git commit version in version number 2017-02-15 12:52:17 +01:00
Julien Duponchelle
1d3d721cf3 Support for strike and underline
Fix #1851
2017-02-14 17:45:48 +01:00
Julien Duponchelle
37a72af75f Do not use native font selector on mac it could crash 2017-02-14 17:01:21 +01:00
Julien Duponchelle
c93713b9e7 Use a dedicated QNetwork manager for notification
I hope this will solve issue for some users where the connection
return random error when they change the project.

Ref #1848
2017-02-14 15:15:29 +01:00
Julien Duponchelle
77b3118cbc Try to workaround unknow error on some user computers
Ref #1848
2017-02-14 12:27:34 +01:00
Julien Duponchelle
bc0cbfd040 Fix a display error in console error message 2017-02-14 11:37:20 +01:00
Julien Duponchelle
0cfc66b4d4 Use signal for writting on console to avoid some potential segfault 2017-02-14 11:23:41 +01:00
Julien Duponchelle
1b9b1cbe3c Fix tests about HTTP errors 2017-02-14 10:19:34 +01:00
Julien Duponchelle
407187c826 Fix a rare warning
Fix https://github.com/GNS3/gns3-server/issues/901
2017-02-14 09:53:12 +01:00
Julien Duponchelle
a6eb1e65ac Add more debug when we have an http error
Ref #1848
2017-02-13 19:15:19 +01:00
Julien Duponchelle
efbb19a862 Disable timeout on project open
Ref #1848
2017-02-13 17:39:13 +01:00
Julien Duponchelle
2a94816b58 Support for gvncviewer
Fix #1845
2017-02-13 15:26:12 +01:00
Julien Duponchelle
aab307a519 Fix a rare crash in the file editor dialog
Fix #1849
2017-02-13 15:22:03 +01:00
Julien Duponchelle
1279e16484 Fix a race condition when we display the error
Ref #1848
2017-02-13 13:08:38 +01:00
Julien Duponchelle
c2e20f9bd6 Fix an issue with invalid hostname detected as an IPV6 2017-02-10 18:13:48 +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
4acdaf6b5a When you update a a node from the node view send settings to controller 2017-02-10 15:51:54 +01:00
Julien Duponchelle
25313cbcde Fix error when permission on the loaded image is broken
Fix #1843
2017-02-08 11:19:41 +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
0de0eb12eb Fix tests 2017-02-07 17:36:21 +01:00
Julien Duponchelle
b58b92c9f0 Merge branch '2.0' into 2.1 2017-02-07 15:03:44 +01:00
Julien Duponchelle
7d0fe52600 Fix crash with invalid image file in appliance wizard
Fix #1837
2017-02-06 17:27:30 +01:00
Julien Duponchelle
ba1d3b2423 Fix error when loading an handmade appliance file
Fix #1839
2017-02-06 17:10:30 +01:00
Julien Duponchelle
cae6afe85d Fix no error if your VNC client is not configured
Fix #1838
2017-02-06 16:59:06 +01:00
Julien Duponchelle
5865ac267b Avoid high cpu usage when connection is lost
Fix #1840
2017-02-06 11:27:36 +01:00
Julien Duponchelle
162993839c Support {name} in cloud template
Fix #1833
2017-02-03 15:15:47 +01:00
Julien Duponchelle
4359b490cc Fix text of the export dialog
Fix #1834
2017-02-03 14:47:07 +01:00
Julien Duponchelle
e4a8e67229 Fix error message when a project is already open
Fix #1832
2017-02-03 14:45: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
6605270e64 Fix missing info in tooltip of ethernet switch
Fix  https://github.com/GNS3/gns3-gui/issues/1828
2017-01-31 19:28:03 +01:00
Julien Duponchelle
09e2bfeed0 The server manage the vmname when we update the linked virtual box VM
Ref https://github.com/GNS3/gns3-gui/issues/1821
2017-01-31 18:50:52 +01:00
Julien Duponchelle
47f34fd5af Merge branch '2.0' into 2.1 2017-01-31 17:00:40 +01:00
Julien Duponchelle
b24733466d Fix z value for text
Fix #1822
2017-01-30 16:00:43 +01:00
Julien Duponchelle
c31be48f20 Avoid a segfault when display an error 2017-01-30 10:51:31 +01:00
Julien Duponchelle
a9ed27f42c Add sata options in the appliance schema
Fix #1817
2017-01-27 11:01:11 +01:00
Julien Duponchelle
89321a6cad Allow drawing lines
Ref #997
2017-01-27 10:15:05 +01:00
Julien Duponchelle
1440caa532 Fix a rare crash when exporting IOU configurations
Fix #1800
2017-01-25 13:55:19 +01:00
Julien Duponchelle
513eb21940 Allow additionnal properties in registry files 2017-01-25 12:14:36 +01:00
Julien Duponchelle
de348d39da Fix a potential crash when a symbol is not found 2017-01-25 12:04:40 +01:00
Julien Duponchelle
0a3697962b Strip unused code for OVA support in the registry 2017-01-24 12:14:25 +01:00
Julien Duponchelle
e0edcf3d23 Increase the timeout for killing local server
Fix #1813
2017-01-24 11:20:50 +01:00
Julien Duponchelle
6690ba7108 2.1.0dev1 2017-01-24 10:38:13 +01:00
Julien Duponchelle
57dbff6a8e Merge pull request #1804 from GNS3/pyup-update-pytest-3.0.5-to-3.0.6
Update pytest to 3.0.6
2017-01-24 09:57:13 +01:00
Julien Duponchelle
0149bc90f2 Fix error when changing the layer of a drawing item
Fix #1810
2017-01-23 17:30:55 +01:00
Julien Duponchelle
f7292deb0f Fix double click for open file on OSX
Fix #1808
2017-01-23 17:22:17 +01:00
Julien Duponchelle
be01448c36 Add debug to see the arguments use to start the application
Ref #1808
2017-01-23 13:58:19 +01:00
Julien Duponchelle
484617ce25 Put the selected engine in the first position of the listbox
This avoid trigerring unexpected signals

Fix #1803
2017-01-23 11:31:55 +01:00
Julien Duponchelle
8ec53a6004 Fix rare crash with dynamips
Fix #1806
2017-01-23 10:15:29 +01:00
Julien Duponchelle
cfd1bbd9d1 Fix rare crash in the progress dialog
Fix #1802
2017-01-23 10:13:43 +01:00
Julien Duponchelle
25ae214b6b Fix a rare crash in console view
Fix #1807
2017-01-23 10:11:36 +01:00
Julien Duponchelle
456160beb1 Fix rare crash
Fix #1805
2017-01-23 10:09:49 +01:00
Julien Duponchelle
9affe2d9f4 Fix crash when you drag a file inside GNS3
Fix #1798
2017-01-23 10:08:03 +01:00
pyup-bot
3ed2f89b3b Update pytest from 3.0.5 to 3.0.6 2017-01-22 23:58:47 +01:00
Julien Duponchelle
9bb353fdbd 2.0.0dev8 2017-01-19 11:23:19 +01:00
Julien Duponchelle
c414ea28e4 2.0.0b3 2017-01-19 11:20:31 +01:00
Julien Duponchelle
cb0253f7cb Fix error if you already have an image with a different name on remote server
Fix #1794
2017-01-18 17:13:00 +01:00
Julien Duponchelle
f28663c626 Ask pyup to not monitor pywin32 2017-01-18 09:32:39 +01:00
Julien Duponchelle
3ef018f90e Allow any pywin32 version 2017-01-17 10:59:25 +01:00
Julien Duponchelle
e5ae7f77fa Drop gns3 converter from requirements 2017-01-17 10:58:36 +01:00
Julien Duponchelle
b6bac0cd3b Show correct server name in tooltip
Fix #1783
2017-01-16 20:46:49 +01:00
Julien Duponchelle
6a8e435210 Menu item to open controller webpage
Fix #1784
2017-01-16 17:43:06 +01:00
grossmj
442318af49 Fixes potential exception when adding network module to an IOS router. Fixes #1774. 2017-01-16 13:53:25 +08:00
Julien Duponchelle
ad93b46c94 Merge pull request #1776 from GNS3/pyup-update-pypiwin32-219-to-220
Update pypiwin32 to 220
2017-01-13 09:13:40 +01:00
pyup-bot
f7a9cc09ea Update pypiwin32 from 219 to 220 2017-01-12 18:47:41 +01:00
Julien Duponchelle
0d5507cf3d Merge branch '2.0' of github.com:GNS3/gns3-gui into 2.0 2017-01-12 08:55:17 +01:00
Julien Duponchelle
f885e33cbd Merge branch 'master' into 2.0 2017-01-12 08:53:57 +01:00
Julien Duponchelle
4813a8681f 1.5.4dev1 2017-01-12 08:51:46 +01:00
Julien Duponchelle
829f750c76 1.5.3 2017-01-12 08:14:42 +01:00
Julien Duponchelle
08b9f4a6d2 Merge pull request #1772 from GNS3/sata-qemu
Sata disk interface support for Qemu VMs.
2017-01-11 16:53:58 +01:00
Julien Duponchelle
7fdf022b36 Do not export a file config file if empty 2017-01-10 15:56:17 +01:00
Julien Duponchelle
f12935076e Allow to set console type in qemu wizard
/bin/bash: q: command not found
2017-01-09 13:02:12 +01:00
Julien Duponchelle
7a472d1574 Fix overwrite of projects
Fix #1743
2017-01-09 12:35:36 +01:00
Julien Duponchelle
4a82dc8705 Fix creation of new appliance version when filename is different
Fix #1755
2017-01-09 12:08:09 +01:00
Julien Duponchelle
52acaadfce Fix you can't configure port 0 on ethernet switch
Fix #1765
2017-01-09 10:23:06 +01:00
Julien Duponchelle
ef1967ff00 Fix a race condition when saving as a project and closing it
Fix #1770
2017-01-09 10:17:03 +01:00
Julien Duponchelle
6584f3b2d4 Reorder multi link when you delete one
Fix #1750
2017-01-06 11:42:26 +01:00
Julien Duponchelle
8ad55290b1 Ensure we can't connect to occupy port
Fix https://github.com/GNS3/gns3-gui/issues/1759
2017-01-06 10:30:24 +01:00
Julien Duponchelle
8ff34d63c0 Fix AttributeError: 'QImageSvgRenderer' object has no attribute '_svg'
Fix #1760
2017-01-06 09:12:13 +01:00
Julien Duponchelle
da88947028 Fix Unsaved preferences in GNS3 VM warning 2017-01-05 17:40:05 +01:00
Julien Duponchelle
854dc5db71 Fix Unsaved preferences in GNS3 VM warning
Fix #1740
2017-01-05 15:50:41 +01:00
Julien Duponchelle
e8ac144011 Merge pull request #1753 from GNS3/pyup-update-pyqt5-5.7-to-5.7.1
Update pyqt5 to 5.7.1
2017-01-05 08:22:10 +01:00
grossmj
8b20f4d568 Force margins in configuration tabs. 2017-01-05 14:52:39 +08:00
grossmj
cb88a4f6d9 Sata disk interface support for Qemu VMs. 2017-01-05 11:24:49 +08:00
grossmj
270f12dc1e Remove "sata" disk interface. Does not exist in Qemu. Ref #1749 2017-01-02 15:02:19 +08:00
grossmj
46ff586055 Add SATA and none disk interfaces on Qemu VM configuration page. Fixes #1749. 2017-01-01 23:25:31 +08:00
pyup-bot
78b999be57 Update pyqt5 from 5.7 to 5.7.1 2016-12-29 12:06:02 +01:00
Julien Duponchelle
4f702d9339 Fix TypeError: argument of type 'NoneType' is not iterable
Fix #1733
2016-12-21 14:34:58 +01:00
Julien Duponchelle
6f210c0e91 2.0.0 dev7 2016-12-21 09:38:50 +01:00
Julien Duponchelle
a677cff0a2 Fix an error when you edit readme and no projet is opened
Fix #1732
2016-12-21 09:37:23 +01:00
Julien Duponchelle
5929e3b56d 2.0.0 beta 2 2016-12-20 12:01:10 +01:00
Julien Duponchelle
5b1050e427 1.5.3 dev2 2016-12-20 11:48:07 +01:00
Julien Duponchelle
271e987972 Upgrade Qt 5.7 2016-12-20 11:40:32 +01:00
grossmj
80402185a5 AUX console button text change in MainWindow. 2016-12-20 18:31:46 +08:00
Julien Duponchelle
1df34148b2 1.5.3 rc1 2016-12-20 09:32:38 +01:00
Julien Duponchelle
dc2070b24e Remove debug 2016-12-20 08:56:08 +01:00
grossmj
7dcbb14b75 Remove forgotten code comments. Ref #1713. 2016-12-20 12:27:41 +11:00
Julien Duponchelle
36c90e0d96 Fix GNS3 Client not connecting to remote controller
Fix #1726
2016-12-19 18:58:05 +01:00
Julien Duponchelle
53b0bef527 Delete from project list deleted projects
Fix #1724
2016-12-19 18:52:31 +01:00
Julien Duponchelle
8227cf1aba Keep a shared list of projects internally
Ref #1724
2016-12-19 18:29:48 +01:00
Julien Duponchelle
7e09d9042b Fix recent files in new project dialog 2016-12-19 17:45:16 +01:00
Julien Duponchelle
49688d6c0e Move recent projects to the file menu
Fix #1713
2016-12-19 17:40:20 +01:00
Julien Duponchelle
962ce3e3d8 Fix Tail process for wireshark trace not killed when we change project
Fix #1725
2016-12-19 14:57:55 +01:00
grossmj
8cbae911e9 Move project menu items. Ref #1713. 2016-12-19 23:23:26 +11:00
Julien Duponchelle
10c94baab7 Fix AttributeError: 'NoneType' object has no attribute 'project_updated_signal'
Fix #1728
2016-12-19 09:11:39 +01:00
Julien Duponchelle
2f2c0ba3ba Display recent files for local controller, recent project for remote controller
Ref #1713
2016-12-16 11:43:01 +01:00
Julien Duponchelle
879647de5e Do not display the remote server if the server is use as a GNS3 VM
Fix #1718
2016-12-15 18:07:00 +01:00
Julien Duponchelle
cd2c9d6b0c If the notification stream is stopped by something we auto reconnect 2016-12-14 19:58:09 +01:00
Julien Duponchelle
95c443e127 Ignore system proxy to avoid trouble with "Security Suites"
Fix #1721
2016-12-13 18:04:19 +01:00
Julien Duponchelle
d403b26b44 Avoid close and delete a project at the same time
Ref #1714
2016-12-12 22:08:49 +01:00
Julien Duponchelle
0f4d4e2071 Alpha sort of servers summaries 2016-12-12 21:29:16 +01:00
Julien Duponchelle
b51e772664 Fix new remote server doesn't show up in compute summary
Fix #1703
2016-12-12 21:25:33 +01:00
Julien Duponchelle
83c7be0f60 Fix interface number for Switch & Hub templates
Fix #1711, #1712
2016-12-12 16:30:33 +01:00
Julien Duponchelle
0063f7d97f Fix sync of node alignements with the server
Fix #1710
2016-12-12 12:50:02 +01:00
Julien Duponchelle
0a65eeeee2 Fix rare condition when you close a project and add a node
Fix #1716
2016-12-12 09:57:49 +01:00
Julien Duponchelle
a7e69e7260 Fix 'LocalServer' object has no attribute '_server_started_by_me'
Fix #1715
2016-12-12 09:13:05 +01:00
Julien Duponchelle
cea15dab4c Options -q for quiet startup
Fix #1708
2016-12-12 09:11:13 +01:00
Julien Duponchelle
7646b59078 Fix an error when apply permission on OSX 2016-12-08 18:52:48 +01:00
Julien Duponchelle
a3c996a3d8 Fix test suites 2016-12-08 17:49:06 +01:00
Julien Duponchelle
e6017ea102 Support Qemu cpus in GNS3A
Fix https://github.com/GNS3/gns3-server/issues/811
2016-12-08 17:34:48 +01:00
Julien Duponchelle
e7db81c277 Support for BIOS images
Fix https://github.com/GNS3/gns3-gui/issues/1700
2016-12-08 16:21:30 +01:00
Julien Duponchelle
a182f5e4b6 Fix IdlePC can't be found during setup wizard
Fix #1693
2016-12-08 12:41:16 +01:00
Julien Duponchelle
da73357763 2.0.0 dev 6 2016-12-08 10:31:34 +01:00
Julien Duponchelle
09e37725f7 2.0.0b1 2016-12-07 19:46:33 +01:00
Julien Duponchelle
5ab9f95379 Use osascript on OSX for asking admin permission
Fix #1699
2016-12-07 16:45:01 +01:00
Julien Duponchelle
6731909971 Change the method for creating the tmpdir for symbols cache
Fix #1701
2016-12-07 16:09:05 +01:00
Julien Duponchelle
284948e377 Fix a connection error at the end of the setup wizard
Fix #1698
2016-12-07 15:59:54 +01:00
Julien Duponchelle
1e55c974eb Merge pull request #1702 from GNS3/CapnCheapo-remove_use_local
Remove use local
2016-12-07 15:33:03 +01:00
grossmj
b186dee326 Change some more preferences tab names. 2016-12-07 22:04:28 +11:00
grossmj
3ae0abc19e Change how some tabs are organized or named. 2016-12-07 21:42:09 +11:00
Julien Duponchelle
dde660ae15 General settings => local settings 2016-12-06 19:40:33 +01:00
Julien Duponchelle
d9fbd04552 Drop more reference to use local server 2016-12-06 19:36:24 +01:00
pyup.io bot
a20c15cbac Update pytest from 3.0.4 to 3.0.5 (#1697) 2016-12-05 17:29:42 +01:00
Stephen Moore
5a54b9f5ca Remove local server checkbox from preferences 2016-12-04 22:14:19 -06:00
Julien Duponchelle
3528d85501 Make sure to not start local server during setup wizard remote server
Fix #1674
2016-12-01 18:18:59 +01:00
Julien Duponchelle
b773599c56 Merge branch 'master' into 2.0 2016-12-01 13:43:22 +01:00
Julien Duponchelle
73346c8de7 Fix Error when editing IOS image created using .gns3a file
Fix #1684
2016-12-01 13:42:42 +01:00
Julien Duponchelle
d33b21fdc1 Fix Error when editing IOS image created using .gns3a file
Fix #1684
2016-12-01 13:41:33 +01:00
Julien Duponchelle
8139010796 Fix test suites around sip deleted 2016-12-01 13:30:00 +01:00
Julien Duponchelle
f0bc1a4abb Do not auto start the local server in setup wizard
Until you quit setup wizard or choose local server the server
don't start.

Fix #1687
2016-12-01 12:33:00 +01:00
Julien Duponchelle
9d0d5da3fc On OSX execute all sudo in a single operation
Fix #1687
2016-12-01 12:10:46 +01:00
Julien Duponchelle
26d188f228 Catch key Compute is missing during conversion error
Fix #1690
2016-12-01 11:12:21 +01:00
Julien Duponchelle
c7af6c2ae2 Fix rare crash in gns3.dialogs.appliance_wizard in validateCurrentPage
Fix #1691
2016-12-01 10:52:28 +01:00
Julien Duponchelle
8707113db8 Fix AttributeError: 'Nat' object has no attribute 'configPage'
Fix #1685
2016-12-01 10:10:19 +01:00
Julien Duponchelle
e88b4814e7 Catch one more RuntimeError: wrapped C/C++
Fix #1692
2016-12-01 09:56:19 +01:00
Julien Duponchelle
97926fe6d5 Fix a rare crash in port
Fix #1688
2016-12-01 09:46:14 +01:00
Julien Duponchelle
4fb9d46953 Fix a rare crash when set symbol
Fix #1689
2016-12-01 09:44:57 +01:00
Julien Duponchelle
c066f7f3df Fix a potential crash 2016-11-29 08:51:33 +01:00
Julien Duponchelle
5117ef8a58 Fix a potential crash at exit 2016-11-28 20:00:03 +01:00
Julien Duponchelle
35ebb39e22 Fix crashes 2016-11-28 14:41:54 +01:00
Julien Duponchelle
66f8c19478 Remove unused settings from general preferences
Fix #1681
2016-11-28 14:17:01 +01:00
Julien Duponchelle
e07ea40b24 Catch error when you try to import a IOU bin as a licence
Fix #1679
2016-11-28 11:08:34 +01:00
Julien Duponchelle
149bc38b2b Fix rare crash when exiting
Fix #1676
2016-11-28 11:06:45 +01:00
Julien Duponchelle
23ca81ccc4 Fix crash when freeing some ressources
Fix #1678, #1677
2016-11-28 11:05:01 +01:00
Julien Duponchelle
b4ab559b00 Fix timeout when exporting large project
Fix https://github.com/GNS3/gns3-server/issues/797
2016-11-25 17:32:09 +01:00
Julien Duponchelle
ad88735e63 Avoid a rare crash when we free a port
Fix #1672
2016-11-25 17:20:16 +01:00
Julien Duponchelle
66ec4f9de5 Fix you can't download symbols after you got an error
Fix #1673
2016-11-25 17:15:41 +01:00
Julien Duponchelle
35d6b424e6 2.0.0dev5 2016-11-24 12:52:54 +01:00
Julien Duponchelle
eb6b099fa1 2.0.0a4 2016-11-24 12:09:33 +01:00
grossmj
b8c1a7a4c9 Some cosmetic for the setup wizard. 2016-11-23 23:06:36 +11:00
Julien Duponchelle
bc07bdaf74 Fix project open (merge error) 2016-11-22 18:41:20 +01:00
Jeremy Grossmann
3109befe73 Merge pull request #1491 from GNS3/dissallow_unknow
Dissallow unknown extensions
2016-11-22 19:02:27 +10:30
Julien Duponchelle
044683de21 Mark preferences changes when you change a QPlainTextEdit 2016-11-21 18:16:02 +01:00
Julien Duponchelle
e607663575 Force the VPCS config initial file
Ref #1655
2016-11-21 17:11:22 +01:00
Julien Duponchelle
abcff7192b Replace the IOU licence path by an input text
Ref #1662
2016-11-21 15:29:53 +01:00
Julien Duponchelle
bc43c43c53 Fix 403 when loading a remote project
Fix #782
2016-11-18 17:59:30 +01:00
Julien Duponchelle
dbc7c4675f Fix some possible PID issues
Fix #1658
2016-11-18 16:45:55 +01:00
Julien Duponchelle
3f0e23f34d Hide the connection refused dialog when we success to reconnect
Fix #1665
2016-11-18 12:36:59 +01:00
Julien Duponchelle
ef7bd7c77c Avoid a rare crash when changing topology 2016-11-18 09:43:43 +01:00
Julien Duponchelle
77950c8a2c When loading another project disconnect from current project
Ref #1646
2016-11-17 12:09:21 +01:00
Julien Duponchelle
a801b5b21f Do not crash if we can't list remote list of GNS3 VM engines 2016-11-16 12:02:18 +01:00
Julien Duponchelle
d637a6dcac Init the VPCS base config
Fix #1655
2016-11-16 10:33:36 +01:00
Julien Duponchelle
8370a22966 Fix invalid ressource path on OSX 2016-11-15 11:41:06 +01:00
Julien Duponchelle
7ea8c8b8f1 Disable the usage of the wheel for Qt 5.7 2016-11-15 11:37:57 +01:00
Julien Duponchelle
11ab87245b Crash fix 2016-11-15 11:33:06 +01:00
Julien Duponchelle
cf63b49b82 Fix segfault when deleting a node
Ref #1654
2016-11-14 19:05:01 +01:00
Julien Duponchelle
030384c990 Do not download multiple time the same symbol
Fix #1661
2016-11-14 16:04:19 +01:00
Julien Duponchelle
49c734b52c Kill tail process when capture stop
Fix #772
2016-11-14 10:41:09 +01:00
pyup.io bot
2a7b6144d6 Update pytest from 3.0.3 to 3.0.4 (#1657) 2016-11-14 09:45:47 +01:00
Julien Duponchelle
f1ecc0cc15 Fix Topology summary contain non existing links
Fix #1640
2016-11-11 11:18:29 +01:00
Julien Duponchelle
adb7663d03 Fix a rare crash when deleting a link
Fix #1653
2016-11-11 09:59:54 +01:00
pyup.io bot
118b0a85d2 Update pytest-timeout from 1.0.0 to 1.2.0 (#1652) 2016-11-10 22:54:29 +01:00
Julien Duponchelle
5fcf1e156d Fix export of debug informations when not connected to the controller
Fix #1650
2016-11-10 13:29:30 +01:00
Julien Duponchelle
5272befb60 Fix AttributeError: 'DockerVM' object has no attribute 'server'
Fix #1647
2016-11-06 21:49:17 +01:00
Julien Duponchelle
b10a524496 Fix error message if you double click on builtin switch
Fix #1642
2016-11-04 13:34:43 +01:00
Julien Duponchelle
de7417787b Fix a rare crash in packet capture
Fix #1643
2016-11-04 13:28:16 +01:00
Julien Duponchelle
a00b80529b Restrict ubridge to admin users on OSX 2016-11-04 09:50:14 +01:00
Julien Duponchelle
b1fa44d176 Natural sort of Nodes in topology summary
Fix #1634
2016-11-03 19:53:36 +01:00
Julien Duponchelle
8251b460ad Drop serial console type
Fix https://github.com/GNS3/gns3-server/issues/748
2016-11-03 18:56:39 +01:00
Julien Duponchelle
4403044584 Fix syntax error 2016-11-03 18:45:42 +01:00
Julien Duponchelle
39c7a56041 Display an error if you try to open a 0.8.x file
Fix #1639
2016-11-03 15:15:03 +01:00
Julien Duponchelle
383dbb5fca Fix tab order when editing a compute 2016-11-02 13:35:04 +01:00
Julien Duponchelle
fc94e784cc Fix a crash in ethernet switch settings
Fix #1635
2016-11-02 11:58:24 +01:00
Julien Duponchelle
37937b0096 2.0.0dev4 2016-10-28 19:38:32 +02:00
Julien Duponchelle
3e9d303ec4 Update changelog 2016-10-28 19:25:26 +02:00
Julien Duponchelle
68908e17d3 2.0.0 alpha 3 2016-10-28 19:23:51 +02:00
Julien Duponchelle
026f482b1b Merge branch 'master' into 2.0 2016-10-28 10:29:28 +02:00
Julien Duponchelle
879784cd67 Fix error when opening a project from the cli with a gns3 installed via setup.py
Fix #1581
2016-10-28 10:27:07 +02:00
Julien Duponchelle
8b8cbb4d74 Fix a rare crash in snapshot dialog
Fix #1627
2016-10-28 09:54:20 +02:00
Julien Duponchelle
40783eba15 Fix crash when importing project on a remote server
Fix #1628
2016-10-28 09:52:52 +02:00
Julien Duponchelle
e63d20ce48 Fix crash in appliance wizard
Fix #1624
2016-10-27 21:01:11 +02:00
Julien Duponchelle
f6ecc4d0bb Fix crash when local server is not available 2016-10-27 20:14:36 +02:00
Julien Duponchelle
64339e6846 Disallow to overwrite a running project
Fix https://github.com/GNS3/gns3-server/issues/745
2016-10-27 16:02:08 +02:00
Julien Duponchelle
2fe73056f0 Fix a rare crash when deleting a link
Fix #1623
2016-10-27 15:29:03 +02:00
Julien Duponchelle
95051035a9 Merge branch 'master' into 2.0 2016-10-27 12:30:27 +02:00
Julien Duponchelle
af6294fe7e Fix appliance with wrong file name after import 2016-10-27 12:29:32 +02:00
Julien Duponchelle
2fd53be083 Fix a crash at startup on Mac when coming from old GNS3 version
Fix #1622
2016-10-26 17:51:07 +02:00
Julien Duponchelle
93199e3695 Fix a crash 2016-10-26 17:04:54 +02:00
Julien Duponchelle
d0a3051656 Fix key error in settings if a compute no longer exists
Fix #1612
2016-10-26 16:16:55 +02:00
Julien Duponchelle
516e84d328 All check for vmware linked base are already made server side
Fix #1609
2016-10-26 15:50:13 +02:00
Julien Duponchelle
a06e4cb7ba Fix Save as is not switching to the saved project
Fix #1620
2016-10-26 15:37:13 +02:00
Julien Duponchelle
a1dd5fee9f Auto reopen a project if connection is lost
Fix #1619
2016-10-26 15:28:25 +02:00
Julien Duponchelle
7e00ac4e50 Empty the list of computes nodes when connection is lost
Ref #1619
2016-10-26 15:01:17 +02:00
Julien Duponchelle
59201e8af6 Try to fix duplicate nodes after snapshot restore on some user computer
Fix #1571
2016-10-26 11:28:35 +02:00
Julien Duponchelle
fbde9e0746 Allow only IPV4 in setup wizard
Fix #1608
2016-10-26 10:55:51 +02:00
Julien Duponchelle
f714add057 Catch error if user tmp directory is read only
Fix #1614
2016-10-26 10:25:59 +02:00
Julien Duponchelle
ce2724a892 Raise a proper error if packet capture program is invalid
Fix #1597
2016-10-26 10:23:20 +02:00
Julien Duponchelle
aff475cd5b Fix AttributeError: 'NoneType' object has no attribute 'upper'
Fix #1615
2016-10-26 10:20:55 +02:00
Julien Duponchelle
b18d7c7cc5 Fix rare crash when killing wireshark
Fix #1616
2016-10-26 10:19:00 +02:00
Julien Duponchelle
687088cbd7 Export debug informations also from the controller
Ref #1562, https://github.com/GNS3/gns3-server/issues/740
2016-10-25 11:41:49 +02:00
Julien Duponchelle
31fc18b150 Fix a crash in vm wizard
Fix #1610
2016-10-25 10:41:09 +02:00
Julien Duponchelle
dd960d5556 Fix an invalid test 2016-10-25 10:39:25 +02:00
Julien Duponchelle
004d9f90e3 Fix error when uploading an images from preferences
Fix #1594
2016-10-25 10:27:56 +02:00
Julien Duponchelle
f8f7ee686e Fix snap to grid when initialy drop a node in the topology 2016-10-24 21:58:47 +02:00
Bernhard Ehlers
c695e565ea Optimize snap-to-grid code
Signed-off-by: Julien Duponchelle <julien@gns3.net>

Fix #1599
2016-10-24 21:45:01 +02:00
Julien Duponchelle
d32c3ebebe Fix a crash with linked clone 2016-10-24 19:20:41 +02:00
Julien Duponchelle
b863bae4e7 Move prevent using twice the same VM when linked clone is not enable
Ref #1593
2016-10-24 18:14:01 +02:00
Julien Duponchelle
8f033e8bd3 Fix If you show interface label and delete the link ghost interface label will appear
Fix #1607
2016-10-24 17:40:51 +02:00
Julien Duponchelle
c8776486c5 Display short interface label instead of long version
Fix #1606
2016-10-24 17:30:34 +02:00
Julien Duponchelle
679e9ad4bf Fix error AttributeError: 'NoneType' object has no attribute 'capabilities'
Fix #1595
2016-10-24 17:15:17 +02:00
Julien Duponchelle
877b255f23 Fix PermissionError when killing local server
Fix #1601
2016-10-24 17:03:18 +02:00
Julien Duponchelle
e404716f88 Handle empty color
Fix #1602
2016-10-24 17:01:35 +02:00
Julien Duponchelle
4e58df60ea Fix rare crash in save as
Fix #1592
2016-10-24 16:58:56 +02:00
Julien Duponchelle
e7f761c8d6 Fix crash in restore default server settings
Fix #1603
2016-10-24 16:51:52 +02:00
Julien Duponchelle
50ca85b7ce Fix an error during import of some 0.8x projects
Fix #1578
2016-10-21 10:55:26 +02:00
Julien Duponchelle
9659c29dd0 2.0.0dev3 2016-10-20 22:12:39 +02:00
Julien Duponchelle
535f3193db 2.0.0a2 2016-10-20 21:33:01 +02:00
Julien Duponchelle
7fae1eac48 Support pure remote server for importing appliance
Fix #1590
2016-10-20 21:24:38 +02:00
Julien Duponchelle
4cc2975e97 Dissallow bindingi GNS3 server to an IPV6 (not supported by some emulators)
Ref https://github.com/GNS3/gns3-server/issues/725
2016-10-20 09:51:17 +02:00
Julien Duponchelle
149d1ad590 Drop vmware host type choice in client
Fix https://github.com/GNS3/gns3-gui/issues/1579
2016-10-19 12:05:21 +02:00
Julien Duponchelle
abc900a764 Merge branch 'master' into 2.0 2016-10-19 10:51:12 +02:00
Julien Duponchelle
14084192da Ask for restart after installing vmrun
Fix https://github.com/GNS3/gns3-server/issues/724
2016-10-19 10:50:07 +02:00
Julien Duponchelle
2f6603070b Ask user to restart GNS3 after VMware installation
Ref https://github.com/GNS3/gns3-server/issues/724
2016-10-19 10:29:11 +02:00
Julien Duponchelle
c3aac9f0a6 Improve duplicate prevention in topology summary
Fix #1571
2016-10-19 09:01:23 +02:00
Julien Duponchelle
afde80dab5 Add a duplicate button in the project library dialog
Fix #1585
2016-10-18 16:55:49 +02:00
pyup.io bot
b7f68afbf1 Update dependencies from pyup
* Pin pep8 to latest version 1.7.0
* Pin pypiwin32 to latest version 219
* Pin pytest to latest version 3.0.3
* Pin pytest-capturelog to latest version 0.7
* Pin pytest-pythonpath to latest version 0.7.1

* Pin pytest-timeout to latest version 1.0.0
2016-10-18 15:21:44 +02:00
Julien Duponchelle
08634f330c Fix error introduce in previous commits 2016-10-18 12:04:55 +02:00
Julien Duponchelle
83e35e6aa0 Fix duplicates in recent project list
Fix #1584
2016-10-18 11:28:40 +02:00
Julien Duponchelle
dc3bfef038 Fix a project override error 2016-10-18 11:26:18 +02:00
Julien Duponchelle
c13bb77b08 Fix Duplicated node in node summary when restoring a snapshot
Fix #1571
2016-10-18 10:12:36 +02:00
Julien Duponchelle
fab621bb40 Try pyup.io 2016-10-18 09:41:27 +02:00
Julien Duponchelle
bb760cd861 Fix a crash in the VMware / VirtualBox wizard 2016-10-17 18:44:00 +02:00
Julien Duponchelle
59e7b3fd93 If console host is 0.0.0.0 use controller address 2016-10-17 18:30:44 +02:00
Julien Duponchelle
315870aa09 Fix save issue when importing an appliance
Fix #1564
2016-10-17 14:26:01 +02:00
Julien Duponchelle
5ed17acd6b Strip HTML in console view logs and log files
Fix #1576
2016-10-17 10:01:55 +02:00
Julien Duponchelle
d0dde822dd Fix TypeError: _expandAllSlot() takes 1 positional argument but 2 were given
Fix #1577
2016-10-14 23:02:22 +02:00
Julien Duponchelle
e3023532ed Fix Cannot open created project by using Recents projects
Fix #1544
2016-10-14 22:57:19 +02:00
grossmj
2a4c229c9d Update edit project Ui. 2016-10-08 14:51:00 -06:00
Julien Duponchelle
7b77627db7 Update crash report key 2016-10-07 10:31:36 +02:00
Julien Duponchelle
fd5f32bcc4 Fix a crash when exporting debug without project open 2016-10-05 16:29:30 +02:00
Julien Duponchelle
7e7eecfa3e Fix a crash 2016-10-05 16:28:33 +02:00
Julien Duponchelle
8325339b58 Fix a crash in rare condition when logging informations to the console 2016-10-05 11:35:47 +02:00
Julien Duponchelle
b39185ceb3 Fix a crash in compute summary view 2016-10-05 11:01:55 +02:00
Julien Duponchelle
d93100f3c3 Add a text about how to change the topology size in 2.0 in general preferences
Fix #1561
2016-10-05 10:36:11 +02:00
Julien Duponchelle
1691558989 Merge branch 'master' into 2.0 2016-10-05 09:36:11 +02:00
Julien Duponchelle
17cabba48f Improve warning when connection issue to GNS3 VM 2016-10-05 09:32:31 +02:00
Julien Duponchelle
c9a2cc05ea Fix crash in setup wizard 2016-10-04 21:11:18 +02:00
Julien Duponchelle
f9ac5302ca Fix the wizard for creating appliance template doesn't support remote main server
Fix #1550
2016-10-04 20:44:32 +02:00
Julien Duponchelle
7c8c8dfc20 Appliance wizard support remote controller
Ref #1550
2016-10-04 19:22:47 +02:00
Julien Duponchelle
6622bc7560 Fix Browse button is not working in the local server page in the setup wizard
Fix #1559
2016-10-04 17:14:21 +02:00
Julien Duponchelle
3df7bd99eb Fix test 2016-10-04 17:11:40 +02:00
Julien Duponchelle
da1636af60 Check if local server is running in the setup wizard
Fix #1560
2016-10-04 16:45:36 +02:00
Julien Duponchelle
2378d7ff78 Hide setup wizard after first successful run 2016-10-04 16:00:59 +02:00
Julien Duponchelle
c2f357cc0e Import appliance and New project are display at the same time
Fix #1558
2016-10-04 15:58:33 +02:00
Julien Duponchelle
f2bd85d803 Support remote controller in the setup wizard
Fix #1549
2016-10-04 15:44:56 +02:00
Julien Duponchelle
ec3a856c59 Fix When importing a gns3a the correct qemu binary is not selected
Fix #1556
2016-10-03 22:56:13 +02:00
Julien Duponchelle
1a55487e7b Increase creation timeout for docker container 2016-10-03 22:30:54 +02:00
Julien Duponchelle
0f0ac33345 Make WaitForLambdaWorker more crash proof 2016-10-03 18:41:36 +02:00
Julien Duponchelle
d0b4e5045b Fix a crash when importing appliance
Fix #705
2016-10-03 18:30:36 +02:00
Julien Duponchelle
393cff0343 Fix error in import appliances 2016-10-03 17:24:15 +02:00
Julien Duponchelle
8e56b59bb8 Try to fix the a segfault when importing appliance
Fix https://github.com/GNS3/gns3-server/issues/705
2016-10-03 15:38:56 +02:00
Julien Duponchelle
a939dfe37f Fix crash in upload images 2016-10-03 12:34:51 +02:00
Julien Duponchelle
9b7e06bd54 Trust the server for link creation error (avoid sync issue)
Fix #1553
2016-10-03 12:31:53 +02:00
Julien Duponchelle
cc2f7920b8 Try to fix crash when import appliances
Fix #705
2016-10-03 10:44:37 +02:00
Julien Duponchelle
66a69dd22d Fix an Error in server preference page
Fix #1551
2016-10-03 10:36:58 +02:00
Julien Duponchelle
d3f7eaee1c Fix compatibility with remote server of 1.X
Fix https://github.com/GNS3/gns3-server/issues/696
2016-09-30 16:04:00 +02:00
Julien Duponchelle
3200f4cd28 New appliance dialog should not be display if you cancel the setup wizard
Fix #1545
2016-09-30 10:56:01 +02:00
Julien Duponchelle
fc04b30b7e 2.0.0dev2 2016-09-29 20:41:50 +02:00
Julien Duponchelle
b81a02fbb4 Update crash report key 2016-09-22 10:47:02 +02:00
Julien Duponchelle
c36695c59a Dissallow unknown extensions
Fix #1490
2016-09-15 17:12:09 +02:00
488 changed files with 107556 additions and 156134 deletions

3
.gitignore vendored
View File

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

2
.pyup.yml Normal file
View File

@@ -0,0 +1,2 @@
branch:
2.2

View File

@@ -1,19 +1,21 @@
sudo: required
services:
- docker
- docker
notifications:
email: false
#email:
# - julien@gns3.net
#irc:
# channels:
# - "chat.freenode.net#gns3"
# on_success: change
# on_failure: always
script:
- docker build -t gns3-gui-test .
- docker run gns3-gui-test
- docker run gns3-gui-test
before_deploy:
- sudo pip install twine
- sudo pip install urllib3[secure]
deploy:
provider: pypi
edge:
branch: v1.8.45
user: noplay
password:
secure: FofcqlJjgqf2jaDaXpLHeigVoexbrOz3WwnDuiJpwJxeFUlPY8s2cQs/Bm+dzxzZaOaGiVE0A83v/Xa10yD5tflThHt4sqYJK3iQCinA7wgeAlDimB4xrWUNplfNJZ/Eod5Ssa++E02W+3i29PxpXY//mjCY7qDxaoxul1gnFJY=
on:
tags: true
repo: GNS3/gns3-gui

571
CHANGELOG
View File

@@ -1,5 +1,557 @@
# Change Log
## 2.1.4 12/03/2018
* Update node on server on any change, Fixes: #2429
* Mark IOU layer 1 keepalive messages feature as non-functional. Fixes #2431.
* Images refresh when added via settings, Fixes:#2423
* Emit project_loaded_signal after project creation
* Add option Show interface labels on new project, Ref. #2308
* Improve finding pyuic3.exe on Windows
* Use debug for error downloading file messages. Fixes #2398.
* Refresh buttons in the cloud node to query the server for available interfaces. Fixes #2416.
* Handle Certifacte Error, Ref. gns3-server#1262
* Backward compatibility for tests, Ref. #2405?
* Use UTF-8 for IOURC file migration.
* Look for symbols on controller, Ref. #2405
* Display an error message if Telnet console program cannot be executed.
## 2.1.3 19/01/2018
* Change messages when there are different client and server versions. Fixes #2391.
* Fix "Transport selection via DSN is deprecated" message. Sync is configured with HTTPTransport.
* Refresh CPU/RAM info every 1 second. Ref #2262.
* Only check for AVG on Windows
* Improve the search for VBoxManage.
* Allow telnet console to node with name containing double quotes. Fixes #2371.
## 2.1.2 08/01/2018
* Update VMware promotion in setup wizard.
* Confirm exit. Fixes #2359.
* Fix with .exe build
## 2.1.1 22/12/2017
* Fix dragging appliance into topology from nodes window, fixes: #2363
* Fix Appliances in Docked mode, fixes: #2362
* Create local variable in order to debug issue in the next occurrence, #2366
* Fix ParseError: not well-formed (invalid token), #2364
* Fix local variable 'vm' referenced before assignment #2365
* Fix: 'NodesDockWidget' object has no attribute 'uiNodesView', #2362
* Tentative fix for packet capture not working correctly when remote main server is configured. Ref #2111.
* Log Qt messages with log.debug() instead of log.info().
* Fix auto idle-pc from preferences. Fixes #2344.
* Snapshoting project without timeout but with button. Ref. #2314
* Improve validation for idle-pc.
* Activate faulthandler.
* Add PATH to OS X console commands
* Use raw triple quotes in large console settings This eliminates one level of quoting
* Fix issue in node summary when console is not supported by a node.
* Remove unused symbols. Fixes #2320.
* Show console information in Topology Summary Dock. Fixes #2258.
* New option: require KVM. If false, Qemu VMs will not be prevented to run without KVM.
* Implement variable replacement for Qemu VM options.
* Show on what server a node is installed in the servers summary pane. Fixes #2279.
* Add more info when cannot remove capture file after stopping packet capture in a remote project. Ref #1223.
* Do not overwrites the disk images when copied to default directory. Fixes #2326.
* Only replace quoted telnet for macOS Telnet commands. Ref #2328.
* Support Telnet path containing spaces. Ref #2328.
* Fix problem when embedded telnet client path contains a space on macOS. Ref #2328.
* Do not launch console for builtin nodes when using the "Console to all nodes" button. Fixes #2309.
* Update frame_relay_switch_configuration_page_ui.py
* Turn off timeout for node creation
## 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
* When another gui is already running exit instead of proper close to avoid any issue
* Fix duplicate on remote server use wrong location
* Display the location of settings when we disallow opening due to old release
* Improve search for dynamips in development on OSX
* Fix error display when loading a .png custom symbol
* Fix a crash in the progress dialog
* Fix a race condition when exporting a closed project
* Fix RuntimeError: wrapped C/C++ object of type NodeItem has been deleted
## 2.0.1 16/05/2017
* Improve inline help. Fixes #1999. Add a warning about wifi interfaces in the cloud. Fixes #1902.
* Copy remote directory path into clipboard in "Show in FileManager". Fixes #1966.
* Fix display of error in progress dialog when we don't have thread
* Fix lost slot and port in dynamips settings
* Do not run import / export of project in seperate thread
* Assert when running an HTTP query outside the main thread
* Proper error when you try to load the pid file as config file
* Log malformed svg text item
* Fix a race condition when right click and delete a node at the same time
* Fix a race condition when snapshoting a closed project
* Update doctor_dialog.py
* Catch remaining missing function listxattr on some Linux host.
* Fix a race condition when creating node and closing project
* Fix error if you put a path in a .gns3a file for qemu
* Fix AttributeError: 'NoneType' object has no attribute '_refreshVisibleWidgets'
* Do not crash if the logging code raise an exception
* Fix some crash in dynamips device preference page
* Fix warning when loading IOU images on Windows
* Do not crash if you don't have configure a packet capture program on Windows
* Ignore error when we can't kill the packet capture
* Fix AttributeError: 'NoneType' object has no attribute 'wasCanceled'
* Fix RuntimeError: wrapped C/C++ object of type QComboBox has been deleted
* Fix RuntimeError: wrapped C/C++ object of type QTreeWidgetItem has been deleted
* Fix detection of https when use for the local server
* Silent the _COMPIZ_TOOLKIT_ACTION warning
* Cacth TypeError: native Qt signal is not callable
* Fix AttributeError: 'C7200' object has no attribute 'warning_signal'
* Catch missing function listxattr on some linux host
* Disallow opening a .gns3 on a remote server
* Fix project closing when we have multiple client connected
## 2.0.0 02/05/2017
* Clarify that we don't override vmware custom adapters
* Strip space from path at project creation
## 2.0.0rc4 20/04/2017
* Catch all error during the generation of log messages.
* Catch a rare node creation error
* Fix missing menu text at application startup
* Fix a race condition in the drawing item
* Catch system error when connecting to local server
* Catch a rare error when killing the capture
* Improve pcap streaming speed
* Upgrade to 5.7.1
* Recent projects list bug
* Fix a race condition in the preferences dialog
* Try to fix some windows Z issues
* Catch a garbage collection issue in the right click on a link
* Fix a compatibility issue with Python 3.4
## 1.5.4 13/04/2017
* Limit ubridge permission to the admin group on OSX
* Upgrade to Qt 5.7.1 on Windows
## 2.0.0rc3 31/03/2017
* Improve timeout handling
* Improve logging when we display a qt message box
* Try to detect computer hibernation
* Fix crash when we send some errors to the user console
* Use QtFile for managing file capture
* Allow to delete a profile from the profile select dialog
* Filter hidden folder in the profil directory
* Prevent user putting port in the remote host name
* Fix RuntimeError: wrapped C/C++ object of type EllipseItem has been deleted
* Fix a rare error in LinkItem
* Fix Image field in nodes list is stale after changing an image
* Fix RuntimeError: Set changed size during iteration
* Better detection of remote server changes
* Add a notice about the fact you need to apply server settings
* Check python version only for setup.py install
* Catch appliance error when creating an appliance new version
* If a node can't be deleted do not remove it
* If something is wrong during packet capture do not disconnect us from the server
* Fix saving dynamips
* Try to fix the hang dialog on some computers
* Fix a rare crash in progress dialog
* If we pass --profile skip the profile select dialog
* Raise an error if the progress dialog is not created from the main thread
* Log qt log to python log
* Fix image are not uploaded to remote main server
* Fix race condition when editing a project
* Poll settings each 5 seconds
* Avoid progress dialog not disapear
* Remove wrong mention about the fact super putty is include
* Avoid a crash when an ios router don't have a chassis
* Fix a potentatial crash in the progress dialog
* Support official docker images in appliances
## 2.0.0rc2 10/03/2017
* Deploy on pypi when we tag
* Fix rare crash in GNS3 VM preference page
* Fix an error on Windows when loading SVG files
* Prevent a potential crash
* Workaround a rare crash when sending analytics
* Catch error when you try to create a node a not existing server
* Fix an error when your local server crash and computer return non unicode
* Fix KeyError: 'slot1'
* Fix a rare crash in import appliance
* Rollback to PyQT 5.8 because 5.8.1 seem to have trouble at install
* Update pyqt5 from 5.8 to 5.8.1
## 2.0.0 RC 1 06/03/2017
* UltraVNC support
* Display less noisy dialog when we can't connect to the remote server
* Prevent the usage of gns3vm as a remote server name
* Fix the VMware wizard for not using a remote server by default
* Prevent the GNS3 VM to appear in remote compute in the VM wizard
* Remove iouyap settings
* Fix missing permission error management
* Avoid a crash when create a new dynamips version in the appliance wizard
* Disallow user to add the same server as a remote server and as local server
* Fix 'module' object has no attribute 'run'
* Monitor and display local server stderr
* Fix some import errors
* Remove placeholder string from appliance wizard
* Avoiding calling multiple time /computes at the same time. And reduce timeout
* Support for appliance v4
* Some tweaks for enabling/disabling HDPI mode.
* Do not display error at first step of the setup wizard
* Disable HDPI by default on Linux and allow to configure it
* Fix an issue when you edit a VPCS node from the node view
* Catch a race condition in managing error static assets download
* Handle error if you try to import an appliance without having the images
* Improve crash proof code of the progress dialog
## 2.0.0 beta 4 19/01/2017
* Update pyqt5 from 5.7.1 to 5.8
* Drop from console view the show command not supported by 2.0
* Try to avoid segfault in some PyQT version
* Support for strike and underline
* Do not use native font selector on mac it could crash
* Use a dedicated QNetwork manager for notification
* Fix a display error in console error message
* Use signal for writting on console to avoid some potential segfault
* Fix a rare warning
* Add more debug when we have an http error
* Disable timeout on project open
* Support for gvncviewer
* Fix a rare crash in the file editor dialog
* Fix a race condition when we display the error
* Fix an issue with invalid hostname detected as an IPV6
* When you update a a node from the node view send settings to controller
* Fix error when permission on the loaded image is broken
* Fix crash with invalid image file in appliance wizard
* Fix error when loading an handmade appliance file
* Fix no error if your VNC client is not configured
* Avoid high cpu usage when connection is lost
* Support {name} in cloud template
* Fix text of the export dialog
* Fix error message when a project is already open
* Fix missing info in tooltip of ethernet switch
* The server manage the vmname when we update the linked virtual box VM
* Fix z value for text
* Avoid a segfault when display an error
* Add sata options in the appliance schema
* Fix a rare crash when exporting IOU configurations
* Allow additionnal properties in registry files
* Fix a potential crash when a symbol is not found
* Strip unused code for OVA support in the registry
* Increase the timeout for killing local server
* Fix error when changing the layer of a drawing item
* Fix double click for open file on OSX
* Add debug to see the arguments use to start the application
* Put the selected engine in the first position of the listbox
* Fix rare crash with dynamips
* Fix rare crash in the progress dialog
* Fix a rare crash in console view
* Fix crash when you drag a file inside GNS3
## 2.0.0 beta 3 19/01/2017
* Fix error if you already have an image with a different name on remote server
* Drop gns3 converter from requirements
* Show correct server name in tooltip
* Menu item to open controller webpage
* Fixes potential exception when adding network module to an IOS router. Fixes #1774.
* Do not export a file config file if empty
* Allow to set console type in qemu wizard
* Fix overwrite of projects
* Fix creation of new appliance version when filename is different
* Fix you can't configure port 0 on ethernet switch
* Fix a race condition when saving as a project and closing it
* Reorder multi link when you delete one
* Ensure we can't connect to occupy port
* Fix AttributeError: 'QImageSvgRenderer' object has no attribute '_svg'
* Fix Unsaved preferences in GNS3 VM warning
* Force margins in configuration tabs.
* Sata disk interface support for Qemu VMs.
* Remove "sata" disk interface. Does not exist in Qemu. Ref #1749
* Add SATA and none disk interfaces on Qemu VM configuration page. Fixes #1749.
* Update pyqt5 from 5.7 to 5.7.1
* Fix TypeError: argument of type 'NoneType' is not iterable
* Fix an error when you edit readme and no projet is opened
* Upgrade Qt 5.7
## 1.5.3 12/01/2017
* Upgrade Qt 5.7
## 2.0.0 beta 2 20/12/2016
* AUX console button text change in MainWindow.
* Fix GNS3 Client not connecting to remote controller
* Delete from project list deleted projects
* Keep a shared list of projects internally
* Fix recent files in new project dialog
* Move recent projects to the file menu
* Fix Tail process for wireshark trace not killed when we change project
* Move project menu items. Ref #1713.
* Display recent files for local controller, recent project for remote controller
* Do not display the remote server if the server is use as a GNS3 VM
* If the notification stream is stopped by something we auto reconnect
* Ignore system proxy to avoid trouble with "Security Suites"
* Avoid close and delete a project at the same time
* Alpha sort of servers summaries
* Fix new remote server doesn't show up in compute summary
* Fix interface number for Switch & Hub templates
* Fix sync of node alignements with the server
* Fix rare condition when you close a project and add a node
* Options -q for quiet startup
* Fix an error when apply permission on OSX
* Support Qemu cpus in GNS3A
* Support for BIOS images
* Fix IdlePC can't be found during setup wizard
## 2.0.0 beta 1 07/12/2016
* Use osascript on OSX for asking admin permission
* Change the method for creating the tmpdir for symbols cache
* Fix a connection error at the end of the setup wizard
* Change how some tabs are organized or named.
* General settings => local settings
* Drop more reference to use local server
* Remove local server checkbox from preferences
* Make sure to not start local server during setup wizard remote server
* Fix Error when editing IOS image created using .gns3a file
* Fix test suites around sip deleted
* Do not auto start the local server in setup wizard
* On OSX execute all sudo in a single operation
* Catch key Compute is missing during conversion error
* Fix rare crash in gns3.dialogs.appliance_wizard in validateCurrentPage
* Fix AttributeError: 'Nat' object has no attribute 'configPage'
* Catch one more RuntimeError: wrapped C/C++
* Fix a rare crash in port
* Fix a rare crash when set symbol
* Fix a potential crash
* Fix a potential crash at exit
* Fix crashes
* Remove unused settings from general preferences
* Catch error when you try to import a IOU bin as a licence
* Fix rare crash when exiting
* Fix crash when freeing some ressources
* Fix timeout when exporting large project
* Avoid a rare crash when we free a port
* Fix you can't download symbols after you got an error
## 2.0.0 alpha 4 24/11/2016
* Mark preferences changes when you change a QPlainTextEdit
* Force the VPCS config initial file
* Replace the IOU licence path by an input text
* Fix 403 when loading a remote project
* Fix some possible server not starting on Windows
* Hide the connection refused dialog when we success to reconnect
* Avoid a rare crash when changing topology
* When loading another project disconnect from current project
* Do not crash if we can't list remote list of GNS3 VM engines
* Init the VPCS base config
* Fix invalid ressource path on OSX
* Fix segfault when deleting a node
* Do not download multiple time the same symbol
* Kill tail process when capture stop
* Fix Topology summary contain non existing links
* Fix a rare crash when deleting a link
* Fix export of debug informations when not connected to the controller
* Fix AttributeError: 'DockerVM' object has no attribute 'server'
* Fix error message if you double click on builtin switch
* Fix a rare crash in packet capture
* Restrict ubridge to admin users on OSX
* Natural sort of Nodes in topology summary
* Drop serial console type
* Display an error if you try to open a 0.8.x file
* Fix tab order when editing a compute
* Fix a crash in ethernet switch settings
* Dissallow unknown extensions
## 2.0.0 alpha 3 28/10/2016
* Fix error when opening a project from the cli with a gns3 installed via setup.py
* Fix a rare crash in snapshot dialog
* Fix crash when importing project on a remote server
* Fix crash in appliance wizard
* Fix crash when local server is not available
* Disallow to overwrite a running project
* Fix a rare crash when deleting a link
* Fix appliance with wrong file name after import
* Fix a crash at startup on Mac when coming from old GNS3 version
* Fix key error in settings if a compute no longer exists
* All check for vmware linked base are already made server side
* Fix Save as is not switching to the saved project
* Auto reopen a project if connection is lost
* Empty the list of computes nodes when connection is lost
* Try to fix duplicate nodes after snapshot restore on some user computer
* Allow only IPV4 in setup wizard
* Catch error if user tmp directory is read only
* Raise a proper error if packet capture program is invalid
* Fix AttributeError: 'NoneType' object has no attribute 'upper'
* Fix rare crash when killing wireshark
* Export debug informations also from the controller
* Fix a crash in vm wizard
* Fix error when uploading an images from preferences
* Fix snap to grid when initialy drop a node in the topology
* Optimize snap-to-grid code
* Fix a crash with linked clone
* Move prevent using twice the same VM when linked clone is not enable
* Fix If you show interface label and delete the link ghost interface label will appear
* Display short interface label instead of long version
* Fix error AttributeError: 'NoneType' object has no attribute 'capabilities'
* Fix PermissionError when killing local server
* Handle empty color
* Fix rare crash in save as
* Fix crash in restore default server settings
* Fix an error during import of some 0.8x projects
## 2.0.0 alpha 2 20/10/2016
* Support pure remote server for importing appliance
* Dissallow binding GNS3 server to an IPV6 (not supported by some emulators)
* Drop vmware host type choice in client
* Ask user to restart GNS3 after VMware installation
* Improve duplicate prevention in topology summary
* Add a duplicate button in the project library dialog
* Fix error introduce in previous commits
* Fix duplicates in recent project list
* Fix a project override error
* Fix Duplicated node in node summary when restoring a snapshot
* Fix a crash in the VMware / VirtualBox wizard
* If console host is 0.0.0.0 use controller address
* Fix save issue when importing an appliance
* Strip HTML in console view logs and log files
* Fix TypeError: _expandAllSlot() takes 1 positional argument but 2 were given
* Fix Cannot open created project by using Recents projects
* Update edit project Ui.
* Update crash report key
* Fix a crash when exporting debug without project open
* Fix a crash in rare condition when logging informations to the console
* Fix a crash in compute summary view
* Add a text about how to change the topology size in 2.0 in general preferences
* Improve warning when connection issue to GNS3 VM
* Fix crash in setup wizard
* Fix the wizard for creating appliance template doesn't support remote main server
* Appliance wizard support remote controller
* Fix Browse button is not working in the local server page in the setup wizard
* Check if local server is running in the setup wizard
* Hide setup wizard after first successful run
* Import appliance and New project are display at the same time
* Support remote controller in the setup wizard
* Fix When importing a gns3a the correct qemu binary is not selected
* Increase creation timeout for docker container
* Make WaitForLambdaWorker more crash proof
* Fix a crash when importing appliance
* Fix error in import appliances
* Try to fix the a segfault when importing appliance
* Fix crash in upload images
* Trust the server for link creation error (avoid sync issue)
* Fix an Error in server preference page
* Fix compatibility with remote server of 1.X
* New appliance dialog should not be display if you cancel the setup wizard
## 2.0.0 alpha 1 29/09/2016
* Save as you go
* Smart packet capture
@@ -33,6 +585,25 @@
* Edit the scene size
* New API
## 1.5.3 rc1 20/12/2016
* Fix Error when editing IOS image created using .gns3a file
* Fix error when opening a project from the cli with a gns3 installed via setup.py
* Fix a crash at startup on Mac when coming from old GNS3 version
* Fix an error during import of some 0.8x projects
* Ask for restart after installing vmrun
* Improve warning when connection issue to GNS3 VM
* Changes wording in VM wizards.
* Changed sentence.
* Display an error if settings come from a more recent version of GNS3
* Fix Error when no GNS3 VM is configured and you click on new Docker or IOU
* Disallow / in docker container name
* Update iTerm3 console settings
* Fix rename ethernet switch doesn't release the name
* Support for VNC display number in command line replacement
* Fix a crash when a directory with image is not accessible at gns3a import
## 1.5.2 18/08/2016
* Make more clear that VMware VM are not ESXi

View File

@@ -1,12 +1,12 @@
# Run tests inside a container
FROM ubuntu:vivid
FROM ubuntu:17.10
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.6 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.6-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.6 -m pytest -vv

View File

@@ -40,3 +40,8 @@ Or start the app with --debug flag.
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
https://github.com/Kozea/wdb
Security issues
----------------
Please contact us using contact informations available here:
http://docs.gns3.com/1ON9JBXSeR7Nt2-Qum2o3ZX0GU86BZwlmNSUgvmqNWGY/index.html

19
appveyor.yml Normal file
View File

@@ -0,0 +1,19 @@
version: '{build}-{branch}'
image: Visual Studio 2015
platform: x64
environment:
PYTHON: "C:\\Python36-x64"
DISTUTILS_USE_SDK: "1"
install:
- cinst nmap
- "%PYTHON%\\python.exe -m pip install -r dev-requirements.txt"
- "%PYTHON%\\python.exe -m pip install -r win-requirements.txt"
build: off
test_script:
- "%PYTHON%\\python.exe -m pytest -v"

View File

@@ -1,7 +1,6 @@
-rrequirements.txt
pep8
pytest
pytest-pythonpath # useful for running tests outside tox
pytest-timeout
pytest-capturelog
pep8==1.7.0
pytest==3.1.0
pytest-pythonpath==0.7.1 # useful for running tests outside tox
pytest-timeout==1.2.0

119
gns3/appliance_manager.py Normal file
View File

@@ -0,0 +1,119 @@
#!/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
project_id = project.id()
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

@@ -29,14 +29,20 @@ log = logging.getLogger(__name__)
class Application(QtWidgets.QApplication):
file_open_signal = QtCore.pyqtSignal(str)
def __init__(self, argv):
def __init__(self, argv, hdpi=True):
self.setStyle(QtWidgets.QStyleFactory.create("Fusion"))
# both Qt and PyQt must be version >= 5.6 in order to enable high DPI scaling
if parse_version(QtCore.QT_VERSION_STR) >= parse_version("5.6") and parse_version(QtCore.PYQT_VERSION_STR) >= parse_version("5.6"):
# only available starting Qt version 5.6
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
if hdpi:
if sys.platform.startswith("linux"):
log.warning("HDPI mode is enabled. HDPI support on Linux is not fully stable and GNS3 may crash depending of your version of Linux. To disabled HDPI mode please edit ~/.config/GNS3/gns3_gui.conf and set 'hdpi' to 'false'")
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
else:
log.info("HDPI mode is disabled")
self.setAttribute(QtCore.Qt.AA_DisableHighDpiScaling)
super().__init__(argv)

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
@@ -46,7 +50,6 @@ class BaseNode(QtCore.QObject):
loaded_signal = QtCore.Signal()
deleted_signal = QtCore.Signal()
error_signal = QtCore.Signal(int, str)
warning_signal = QtCore.Signal(int, str)
server_error_signal = QtCore.Signal(int, str)
_instance_count = 1
@@ -90,7 +93,10 @@ class BaseNode(QtCore.QObject):
self._links.add(link)
def deleteLink(self, link):
self._links.remove(link)
try:
self._links.remove(link)
except KeyError:
pass
@classmethod
def reset(cls):
@@ -174,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):
"""
@@ -314,7 +317,6 @@ class BaseNode(QtCore.QObject):
self._project.get(path, callback, context=context, **kwargs)
def controllerHttpDelete(self, path, callback, context={}, **kwargs):
"""
Delete on current server / project
@@ -325,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

@@ -22,15 +22,16 @@ class Compute:
"""
A compute node on the remote server
"""
def __init__(self, compute_id=None):
if compute_id is None:
compute_id = str(uuid.uuid4())
self._compute_id = compute_id
self._name = compute_id
self._connected = False
self._protocol = None
self._protocol = "http"
self._host = None
self._port = None
self._port = 3080
self._user = None
self._password = None
self._cpu_usage_percent = None

View File

@@ -20,9 +20,13 @@ from .qt import QtCore
from .compute import Compute
from .controller import Controller
import sys
import copy
import logging
import urllib
import datetime
import logging
log = logging.getLogger(__name__)
@@ -36,6 +40,7 @@ class ComputeManager(QtCore.QObject):
self._computes = {}
self._controller = Controller.instance()
self._controller.connected_signal.connect(self._controllerConnectedSlot)
self._controller.disconnected_signal.connect(self._controllerDisconnectedSlot)
self._controllerConnectedSlot()
# If we receive fresh data from the notification feed no need to refresh via an API call
@@ -43,19 +48,30 @@ class ComputeManager(QtCore.QObject):
self._timer = QtCore.QTimer()
self._timer.setInterval(1000)
self._refreshingComputes = False
self._timer.timeout.connect(self._refreshComputesSlot)
self._timer.start()
def _refreshComputesSlot(self):
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 5:
if self._refreshingComputes:
return
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 1:
self._last_computes_refresh = datetime.datetime.now().timestamp()
self._controller.get("/computes", self._listComputesCallback, showProgress=True)
self._refreshingComputes = True
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
def _controllerConnectedSlot(self):
if self._controller.connected():
self._controller.get("/computes", self._listComputesCallback)
self._refreshingComputes = True
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
def _controllerDisconnectedSlot(self):
for compute_id in list(self._computes):
del self._computes[compute_id]
self.deleted_signal.emit(compute_id)
def _listComputesCallback(self, result, error=False, **kwargs):
self._refreshingComputes = False
if error is True:
log.error("Error while getting compute list: {}".format(result["message"]))
return
@@ -68,6 +84,7 @@ class ComputeManager(QtCore.QObject):
Called when we received data from a compute
node.
"""
self._last_computes_refresh = datetime.datetime.now().timestamp()
new_node = False
@@ -91,11 +108,25 @@ class ComputeManager(QtCore.QObject):
else:
self.updated_signal.emit(compute_id)
def computeIsTheRemoteGNS3VM(self, compute):
"""
:returns: Boolean True if the remote server is the remote GNS3 VM
"""
if compute.id() != "local" and compute.id() != "vm":
if self.vmCompute() and "GNS3 VM ({})".format(compute.name()) == self.vmCompute().name():
return True
return False
def computes(self):
"""
:returns: List of computes nodes
"""
return list(self._computes.values())
computes = []
for compute in self._computes.values():
# We filter the remote GNS3 VM compute from the computes list
if not self.computeIsTheRemoteGNS3VM(compute):
computes.append(compute)
return computes
def vmCompute(self):
"""
@@ -115,6 +146,17 @@ class ComputeManager(QtCore.QObject):
except KeyError:
return None
def localPlatform(self):
"""
Return the platform of the local compute.
With a remote controller it could be different of our local platform
"""
c = self.localCompute()
if c is None:
return sys.platform
return c.capabilities().get("platform", sys.platform)
def remoteComputes(self):
"""
:returns: List of non local and non VM computes
@@ -122,6 +164,12 @@ class ComputeManager(QtCore.QObject):
return [c for c in self._computes.values() if c.id() != "local" and c.id() != "vm"]
def getCompute(self, compute_id):
if compute_id.startswith("http:") or compute_id.startswith("https:"):
u = urllib.parse.urlsplit(compute_id)
for compute in self._computes.values():
if "{}:{}".format(compute.host(), compute.port()) == u.netloc:
return compute
raise KeyError("Compute {} is missing.".format(compute_id))
if compute_id not in self._computes:
self._computes[compute_id] = Compute(compute_id)
self.created_signal.emit(compute_id)
@@ -153,12 +201,14 @@ class ComputeManager(QtCore.QObject):
log.debug("Update compute %s", compute_id)
self._controller.put("/computes/" + compute_id, None, body=c.__json__())
self._computes[compute_id] = c
self.updated_signal.emit(compute_id)
# Create the new nodes
for compute in computes:
if compute.id() not in self._computes:
log.debug("Create compute %s", compute.id())
self._controller.post("/computes", None, body=compute.__json__())
self._computes[compute.id()] = compute
self.created_signal.emit(compute.id())
@staticmethod
def reset():

View File

@@ -19,10 +19,10 @@
Compute summary view that list all the compute, their status.
"""
import sip
from .qt import QtGui, QtCore, QtWidgets
from .compute_manager import ComputeManager
from .topology import Topology
from .node import Node
import logging
log = logging.getLogger(__name__)
@@ -62,7 +62,7 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
text = "{} CPU {}%, RAM {}%".format(text, self._compute.cpuUsagePercent(), self._compute.memoryUsagePercent())
self.setText(0, text)
self.setToolTip(0, text)
self.setToolTip(0, text + " on " + self._compute.capabilities().get("platform", ""))
if self._compute.connected():
self._status = "connected"
@@ -76,7 +76,23 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
else:
self._status = "stopped"
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
self._parent.sortItems(0, QtCore.Qt.AscendingOrder)
# add nodes belonging to this compute
self.takeChildren()
nodes = Topology.instance().nodes()
for node in nodes:
if node.compute().id() == self._compute.id():
item = QtWidgets.QTreeWidgetItem()
item.setText(0, node.name())
if node.status() == Node.started:
item.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
elif node.status() == Node.suspended:
item.setIcon(0, QtGui.QIcon(':/icons/led_yellow.svg'))
else:
item.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
self.addChild(item)
self.sortChildren(0, QtCore.Qt.AscendingOrder)
class ComputeSummaryView(QtWidgets.QTreeWidget):
@@ -106,17 +122,26 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
"""
compute = ComputeManager.instance().getCompute(compute_id)
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
return
self._computes[compute_id] = ComputeItem(self, compute)
def _computeUpdatedSlot(self, compute_id):
"""
Called when a compute is removed to the list of computes
Called when a compute is updated
:params url: URL of the compute
"""
if compute_id in self._computes:
self._computes[compute_id]._refreshStatusSlot()
compute = ComputeManager.instance().getCompute(compute_id)
# We hide the remote GNS3 VM
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
self._computeRemovedSlot(compute_id)
else:
self._computes[compute_id]._refreshStatusSlot()
else:
self._computeAddedSlot(compute_id)
def _computeRemovedSlot(self, compute_id):
"""

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.
@@ -249,44 +269,6 @@ class ConsoleCmd(cmd.Cmd):
print("{}: no such device".format(node_name))
continue
def _show_run(self, params):
"""
Handles the 'show run' command.
:param params: list of parameters
"""
if self._topology.project is None:
print("Sorry, the project hasn't been saved yet")
return
topology = self._topology.dump()
if len(params) == 1:
# print out whole topology
print(json.dumps(topology, sort_keys=True, indent=4))
elif len(params) >= 2:
# this is a 'show run <device_name>'
params.pop(0)
for param in params:
node_name = param
base_node_id = None
# get the node ID
for node in self._topology.nodes():
if node.name() == node_name:
base_node_id = node.id()
break
if base_node_id is None:
print("{}: no such device".format(node_name))
continue
if "nodes" in topology["topology"]:
for node in topology["topology"]["nodes"]:
if node["id"] == base_node_id:
print(json.dumps(node, sort_keys=True, indent=4))
break
def do_show(self, args):
"""
Show detail information about every device in current lab:
@@ -294,15 +276,6 @@ class ConsoleCmd(cmd.Cmd):
Show detail information about a device:
show device <device_name>
Show the whole topology:
show run
Show topology info of a device:
show run <device_name>
Show the GNS3 VM status
show gns3vm
"""
if '?' in args or args.strip() == "":
@@ -312,10 +285,6 @@ class ConsoleCmd(cmd.Cmd):
params = args.split()
if params[0] == "device":
self._show_device(params)
elif params[0] == "run":
self._show_run(params)
elif params[0] == "gns3vm":
self._show_gnsvm(params)
else:
print(self.do_show.__doc__)

View File

@@ -15,11 +15,12 @@
# 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 platform
import sys
import sip
import struct
import inspect
import datetime
import platform
from .qt import QtCore, Qt
from .topology import Topology
@@ -37,24 +38,31 @@ class ConsoleLogHandler(logging.StreamHandler):
"""
Display log event to the console
"""
def emit(self, record):
if sip.isdeleted(self._console_view):
return
message = self.format(record)
level_no = record.levelno
if level_no >= logging.ERROR:
self._console_view.write("{}\n".format(message), error=True)
self._console_view.write_message_signal.emit("{}\n".format(message), "error")
elif level_no >= logging.WARNING:
self._console_view.write("{}\n".format(message), warning=True)
self._console_view.write_message_signal.emit("{}\n".format(message), "warning")
elif level_no >= logging.INFO:
# To avoid noise on console we display all event only if log level is debug
# or if we force the display in the log record
if "show" in record.__dict__ or logging.getLogger().getEffectiveLevel() == logging.DEBUG:
self._console_view.write("{}\n".format(message))
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
elif level_no >= logging.DEBUG:
self._console_view.write("{}\n".format(message))
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
class ConsoleView(PyCutExt, ConsoleCmd):
# Emit this signal to write a message on console
write_message_signal = QtCore.Signal(str, str)
def __init__(self, parent):
# Set the prompt PyCutExt
@@ -89,16 +97,25 @@ class ConsoleView(PyCutExt, ConsoleCmd):
self._handleLogs()
if LocalConfig.instance().experimental():
log.warning("WARNING: Experimental features enable. You can use some unfinished features and lost data.")
log.warning("WARNING: Experimental features enable. You can use some unfinished features and lost data.")
for module in MODULES:
instance = module.instance()
instance.notification_signal.connect(self.writeNotification)
self.write_message_signal.connect(self._writeMessageSlot)
# required for Cmd module (do_help etc.)
self.stdout = sys.stdout
self._topology = Topology.instance()
def _writeMessageSlot(self, message, level):
if level == "error":
self.write(message, error=True)
elif level == "warning":
self.write(message, warning=True)
else:
self.write(message)
def _handleLogs(self):
"""
@@ -174,11 +191,9 @@ class ConsoleView(PyCutExt, ConsoleCmd):
"""
text = "Server notification: {}".format(message)
self.write(text, error=True)
self.write("\n")
if details:
self.write(details)
self.write("\n")
text += "\n" + details
self.write_message_signal.emit(text, "info")
def writeError(self, base_node_id, message):
"""
@@ -195,8 +210,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
text = "Error:{name} {message}".format(name=name,
message=message)
self.write(text, error=True)
self.write("\n")
self.write_message_signal.emit(text, "error")
def writeWarning(self, base_node_id, message):
"""
@@ -213,8 +227,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
text = "Warning:{name} {message}".format(name=name,
message=message)
self.write(text, warning=True)
self.write("\n")
self.write_message_signal.emit(text, "warning")
def writeServerError(self, base_node_id, message):
"""
@@ -235,8 +248,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
text = "Server error {server}:{name} {message}".format(server=server,
name=name,
message=message)
self.write(text.strip(), error=True)
self.write("\n")
self.write_message_signal.emit(text.strip(), "error")
def _run(self):
"""

View File

@@ -19,7 +19,7 @@ import os
import hashlib
import tempfile
from .qt import QtCore, QtGui, QtWidgets, qpartial
from .qt import QtCore, QtGui, QtWidgets, qpartial, qslot
from .symbol import Symbol
from .local_server_config import LocalServerConfig
from .settings import LOCAL_SERVER_SETTINGS
@@ -33,14 +33,27 @@ class Controller(QtCore.QObject):
An instance of the GNS3 server controller
"""
connected_signal = QtCore.Signal()
disconnected_signal = QtCore.Signal()
connection_failed_signal = QtCore.Signal()
project_list_updated_signal = QtCore.Signal()
def __init__(self, parent=None):
super().__init__()
self._connected = False
self._cache_directory = tempfile.TemporaryDirectory()
self._connecting = False
self._cache_directory = tempfile.mkdtemp()
self._http_client = None
# If it's the first error we display an alert box to the user
self._first_error = True
self._error_dialog = None
self._display_error = True
self._projects = []
# If we do multiple call in order to download the same symbol we queue them
self._static_asset_download_queue = {}
def host(self):
return self._http_client.host()
def isRemote(self):
"""
@@ -49,6 +62,12 @@ class Controller(QtCore.QObject):
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
return not settings["auto_start"]
def connecting(self):
"""
:returns: True if connection is in progress
"""
return self._connecting
def connected(self):
"""
Is the controller connected
@@ -67,34 +86,115 @@ class Controller(QtCore.QObject):
"""
self._http_client = http_client
if self._http_client:
if self.isRemote():
self._http_client.setMaxTimeDifferenceBetweenQueries(120)
self._http_client.connection_connected_signal.connect(self._httpClientConnectedSlot)
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
"""
self._display_error = val
self._first_error = True
def _connectingToServer(self):
"""
Connection process as started
"""
self._connected = False
self._connecting = True
self.get('/version', self._versionGetSlot)
def _httpClientDisconnectedSlot(self):
if self._connected:
self._connected = False
self.get('/version', self._versionGetSlot)
self.disconnected_signal.emit()
self._connectingToServer()
def _versionGetSlot(self, result, error=False, **kwargs):
"""
Called after the inital version get
"""
if error:
if "message" in result and self._first_error:
QtWidgets.QMessageBox.critical(self.parent(), "Connection", result["message"])
# Try to connect again in 1 seconds
QtCore.QTimer.singleShot(1000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))
if self._first_error:
self._connecting = False
self.connection_failed_signal.emit()
if "message" in result and self._display_error:
self._error_dialog = QtWidgets.QMessageBox(self.parent())
self._error_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
self._error_dialog.setWindowTitle("Connection to server")
self._error_dialog.setText("Error when connecting to the GNS3 server:\n{}".format(result["message"]))
self._error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
self._error_dialog.show()
# Try to connect again in x seconds
QtCore.QTimer.singleShot(5000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))
self._first_error = False
else:
self._first_error = True
if self._error_dialog:
self._error_dialog.reject()
self._error_dialog = None
def _httpClientConnectedSlot(self):
if not self._connected:
self._connected = True
self._connecting = False
self.connected_signal.emit()
self.refreshProjectList()
def get(self, *args, **kwargs):
return self.createHTTPQuery("GET", *args, **kwargs)
def getCompute(self, path, compute_id, *args, **kwargs):
"""
API get on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/{}{}".format(compute_id, path)
return self.get(path, *args, **kwargs)
def post(self, *args, **kwargs):
return self.createHTTPQuery("POST", *args, **kwargs)
def postCompute(self, path, compute_id, *args, **kwargs):
"""
API post on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/{}{}".format(compute_id, path)
return self.post(path, *args, **kwargs)
def __fix_compute_id(self, compute_id):
"""
Support for remote server <= 1.5
This fix should be not require after the 2.1
when all the appliance template will be managed
on server
"""
if compute_id.startswith("http:") or compute_id.startswith("https:"):
from .compute_manager import ComputeManager
try:
return ComputeManager.instance().getCompute(compute_id).id()
except KeyError:
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)
@@ -111,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():
"""
@@ -122,48 +225,114 @@ 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:
return
path = self.getStaticCachedPath(url)
if os.path.exists(path):
callback(path)
elif path in self._static_asset_download_queue:
self._static_asset_download_queue[path].append((callback, fallback, ))
else:
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.debug("Error while downloading file: {}".format(url))
del self._static_asset_download_queue[path]
return
try:
with open(path, "wb+") as f:
f.write(raw_body)
except OSError as e:
log.error("Can't write to {}: {}".format(path, str(e)))
return
log.debug("File stored {} for {}".format(path, url))
for callback, fallback in self._static_asset_download_queue[path]:
callback(path)
del self._static_asset_download_queue[path]
def getStaticCachedPath(self, url):
"""
Returns static cached (hashed) path
:param url:
:return:
"""
m = hashlib.md5()
m.update(url.encode())
if ".svg" in url:
extension = ".svg"
else:
extension = ".png"
path = os.path.join(self._cache_directory.name, m.hexdigest() + extension)
if os.path.exists(path):
callback(path)
else:
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, callback, url, path))
path = os.path.join(self._cache_directory, m.hexdigest() + extension)
return path
def _getStaticCallback(self, callback, url, path, result, error=False, raw_body=None, **kwargs):
if error:
log.error("Error while downloading file: {}".format(url))
return
with open(path, "wb+") as f:
f.write(raw_body)
log.debug("File stored {} for {}".format(path, url))
callback(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()
icon.addFile(path)
callback(icon)
def getSymbols(self, callback):
self.get('/symbols', callback=callback)
def deleteProject(self, project_id, callback=None):
Controller.instance().delete("/projects/{}".format(project_id), qpartial(self._deleteProjectCallback, callback=callback, project_id=project_id))
def _deleteProjectCallback(self, result, error=False, project_id=None, callback=None, **kwargs):
if error:
log.error("Error while deleting project: {}".format(result["message"]))
else:
self.refreshProjectList()
self._projects = [p for p in self._projects if p["project_id"] != project_id]
if callback:
callback(result, error=error, **kwargs)
@qslot
def refreshProjectList(self, *args):
self.get("/projects", self._projectListCallback)
def _projectListCallback(self, result, error=False, **kwargs):
if not error:
self._projects = result
self.project_list_updated_signal.emit()
def projects(self):
return self._projects

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 = "https://468f899afc3c46d99bad2eb474516d2c:415662f2e4c240829d5eaf8f09ca99b4@app.getsentry.com/38506"
DSN = "sync+https://b892f3e4a56e4443ad16cfe3d4c6d602:ca3ad57e613441ab95ea15a45e9e5435@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

@@ -16,13 +16,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import sip
import shutil
from ..qt import QtWidgets, QtCore, QtGui, qpartial
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
from ..image_manager import ImageManager
from ..modules import Qemu
from ..registry.appliance import Appliance
from ..registry.appliance import Appliance, ApplianceError
from ..registry.registry import Registry
from ..registry.config import Config, ConfigException
from ..registry.image import Image
@@ -30,27 +30,43 @@ from ..utils import human_filesize
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
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
import logging
log = logging.getLogger(__name__)
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
images_changed_signal = QtCore.Signal()
versions_changed_signal = QtCore.Signal()
def __init__(self, parent, path):
super().__init__(parent)
self._path = path
self.setupUi(self)
self.images_changed_signal.connect(self._refreshVersions)
self.versions_changed_signal.connect(self._versionRefreshedSlot)
self._refreshing = False
self._path = path
# Count how many images are curently uploading
self._image_uploading_count = 0
images_directories = list()
images_directories.append(os.path.dirname(self._path))
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
if download_directory != "" and download_directory != os.path.dirname(self._path):
images_directories.append(download_directory)
self._registry = Registry(images_directories)
self._registry.image_list_changed_signal.connect(self.images_changed_signal.emit)
self._appliance = Appliance(self._registry, self._path)
self._registry.appendImageDirectory(os.path.join(ImageManager.instance().getDirectory(), self._appliance.image_dir_name()))
self.uiApplianceVersionTreeWidget.currentItemChanged.connect(self._applianceVersionCurrentItemChangedSlot)
self.uiRefreshPushButton.clicked.connect(self._refreshVersions)
self.uiRefreshPushButton.clicked.connect(self.images_changed_signal.emit)
self.uiDownloadPushButton.clicked.connect(self._downloadPushButtonClickedSlot)
self.uiImportPushButton.clicked.connect(self._importPushButtonClickedSlot)
self.uiCreateVersionPushButton.clicked.connect(self._createVersionPushButtonClickedSlot)
@@ -60,8 +76,24 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
if Controller.instance().isRemote():
self.uiLocalRadioButton.setText("Install the appliance on the main server")
else:
if not path.endswith('.builtin.gns3a'):
destination = Config().appliances_dir
try:
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
# symbols loaded from controller
self._symbols = []
def initializePage(self, page_id):
"""
Initialize Wizard pages.
@@ -86,6 +118,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
type = "dynamips"
if self.page(page_id) == self.uiInfoWizardPage:
Controller.instance().getSymbols(self._getSymbolsCallback)
self.uiInfoWizardPage.setTitle(self._appliance["product_name"])
self.uiDescriptionLabel.setText(self._appliance["description"])
@@ -115,14 +149,23 @@ 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()
for compute in ComputeManager.instance().remoteComputes():
self.uiRemoteServersComboBox.addItem(compute.name(), compute)
if len(ComputeManager.instance().remoteComputes()) == 0:
self.uiRemoteRadioButton.setEnabled(False)
else:
self.uiRemoteRadioButton.setEnabled(True)
for compute in ComputeManager.instance().remoteComputes():
self.uiRemoteServersComboBox.addItem(compute.name(), compute)
if not ComputeManager.instance().vmCompute():
self.uiVMRadioButton.setEnabled(False)
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")):
if ComputeManager.instance().localPlatform() is None:
self.uiLocalRadioButton.setEnabled(False)
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():
@@ -132,18 +175,26 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if ComputeManager.instance().vmCompute():
self.uiVMRadioButton.setChecked(True)
elif ComputeManager.instance().localCompute():
elif ComputeManager.instance().localCompute() and self.uiLocalRadioButton.isEnabled():
self.uiLocalRadioButton.setChecked(True)
elif len(ComputeManager.instance().remoteComputes()) > 0:
elif self.uiRemoteRadioButton.isEnabled():
self.uiRemoteRadioButton.setChecked(True)
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._refreshVersions()
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
elif self.page(page_id) == self.uiQemuWizardPage:
Qemu.instance().getQemuBinariesFromServer(self._server, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
elif self.page(page_id) == self.uiSummaryWizardPage:
self.uiSummaryTreeWidget.clear()
@@ -166,12 +217,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiCheckServerLabel.setText("Please wait while checking server capacities...")
if 'qemu' in self._appliance:
if self._appliance['qemu'].get('kvm', 'require') == 'require':
self._server_check = False # If the server as the capacities for running the appliance
Qemu.instance().getQemuCapabilitiesFromServer(self._server, qpartial(self._qemuServerCapabilitiesCallback))
self._server_check = False # If the server as the capacities for running the appliance
self.uiCheckServerLabel.setText("")
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
return
self.uiCheckServerLabel.setText("")
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
self._server_check = True
self.next()
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
"""
@@ -192,91 +243,117 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
def _uiServerWizardPage_isComplete(self):
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
def _refreshVersions(self):
def _imageUploadedCallback(self, result, error=False, **kwargs):
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
@qslot
def _refreshVersions(self, *args):
"""
Refresh the list of files for different version of the appliance
"""
self.uiFilesWizardPage.setSubTitle("The following versions are available for " + self._appliance["product_name"] + ". Check the status of files required to install.")
self.uiApplianceVersionTreeWidget.clear()
if self._refreshing:
return
self._refreshing = True
worker = WaitForLambdaWorker(lambda: self._resfreshDialogWorker())
self.uiFilesWizardPage.setSubTitle("The following versions are available for " + self._appliance["product_name"] + ". Check the status of files required to install.")
worker = WaitForLambdaWorker(lambda: self._refreshDialogWorker())
progress_dialog = ProgressDialog(worker, "Add appliance", "Scanning directories for files...", None, busy=True, parent=self)
progress_dialog.show()
if progress_dialog.exec_():
for version in self._appliance["versions"]:
top = QtWidgets.QTreeWidgetItem(["{} {}".format(self._appliance["product_name"], version["name"])])
size = 0
status = "Ready to install"
for image in version["images"].values():
if image["status"] == "Missing":
status = "Missing files"
@qslot
def _versionRefreshedSlot(self, *args):
"""
Called when we finish to scan the disk for new versions
"""
if self._refreshing or self.currentPage() != self.uiFilesWizardPage:
return
self._refreshing = True
self.uiApplianceVersionTreeWidget.clear()
size += image.get("filesize", 0)
image_widget = QtWidgets.QTreeWidgetItem(
[
"",
image["filename"],
human_filesize(image.get("filesize", 0)),
image["status"],
image["version"],
image.get("md5sum", "")
])
for version in self._appliance["versions"]:
top = QtWidgets.QTreeWidgetItem(self.uiApplianceVersionTreeWidget, ["{} {}".format(self._appliance["product_name"], version["name"])])
size = 0
status = "Ready to install"
for image in version["images"].values():
if image["status"] == "Missing":
status = "Missing files"
if image["status"] == "Missing":
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
else:
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
# Associated data stored are col 0: version, col 1: image
image_widget.setData(0, QtCore.Qt.UserRole, version)
image_widget.setData(1, QtCore.Qt.UserRole, image)
image_widget.setData(2, QtCore.Qt.UserRole, self._appliance)
top.addChild(image_widget)
font = top.font(0)
font.setBold(True)
top.setFont(0, font)
expand = True
if status == "Missing files":
top.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
size += image.get("filesize", 0)
image_widget = QtWidgets.QTreeWidgetItem(
[
"",
image["filename"],
human_filesize(image.get("filesize", 0)),
image["status"],
image["version"],
image.get("md5sum", "")
])
if image["status"] == "Missing":
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
else:
expand = False
top.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
top.setData(2, QtCore.Qt.DisplayRole, human_filesize(size))
top.setData(3, QtCore.Qt.DisplayRole, status)
top.setData(2, QtCore.Qt.UserRole, self._appliance)
top.setData(0, QtCore.Qt.UserRole, version)
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
if expand:
top.setExpanded(True)
# Associated data stored are col 0: version, col 1: image
image_widget.setData(0, QtCore.Qt.UserRole, version)
image_widget.setData(1, QtCore.Qt.UserRole, image)
image_widget.setData(2, QtCore.Qt.UserRole, self._appliance)
top.addChild(image_widget)
font = top.font(0)
font.setBold(True)
top.setFont(0, font)
expand = True
if status == "Missing files":
top.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
else:
expand = False
top.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
top.setData(2, QtCore.Qt.DisplayRole, human_filesize(size))
top.setData(3, QtCore.Qt.DisplayRole, status)
top.setData(2, QtCore.Qt.UserRole, self._appliance)
top.setData(0, QtCore.Qt.UserRole, version)
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
# self.uiApplianceVersionTreeWidget.setCurrentItem(top)
if expand:
top.setExpanded(True)
if len(self._appliance["versions"]) > 0:
self.uiApplianceVersionTreeWidget.resizeColumnToContents(0)
self.uiApplianceVersionTreeWidget.resizeColumnToContents(1)
self.uiApplianceVersionTreeWidget.setCurrentItem(self.uiApplianceVersionTreeWidget.topLevelItem(0))
self._refreshing = False
def _resfreshDialogWorker(self):
def _getSymbolsCallback(self, result, error=False, **kwargs):
if error:
log.warning("Cannot load symbols from controller")
else:
self._symbols = result
def _refreshDialogWorker(self):
"""
Scan local directory in order to found the images on disk
"""
# Docker do not have versions
if not "versions" in self._appliance:
if "versions" not in self._appliance:
return
for version in self._appliance["versions"]:
for image in version["images"].values():
img = self._registry.search_image_file(image["filename"], image.get("md5sum"), image.get("filesize"))
img = self._registry.search_image_file(self._appliance.emulator(), image["filename"], image.get("md5sum"), image.get("filesize"))
if img:
image["status"] = "Found"
image["md5sum"] = img.md5sum
image["filesize"] = img.filesize
else:
image["status"] = "Missing"
self._refreshing = False
self.versions_changed_signal.emit()
@qslot
def _applianceVersionCurrentItemChangedSlot(self, current, previous):
"""
Called when user select a different item in the list of appliance files
@@ -285,7 +362,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiImportPushButton.hide()
self.uiExplainDownloadLabel.hide()
if current is None:
if current is None or sip.isdeleted(current):
return
image = current.data(1, QtCore.Qt.UserRole)
@@ -294,14 +371,18 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiDownloadPushButton.show()
self.uiImportPushButton.show()
def _downloadPushButtonClickedSlot(self):
@qslot
def _downloadPushButtonClickedSlot(self, *args):
"""
Called when user want to download an appliance images.
He should have selected the file before.
"""
if self._refreshing:
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
if current is None:
if current is None or sip.isdeleted(current):
return
data = current.data(1, QtCore.Qt.UserRole)
@@ -314,40 +395,52 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.warning(self, "Add appliance", "Download will redirect you where the required file can be downloaded, you may have to be registered with the vendor in order to download the file.")
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["download_url"]))
def _createVersionPushButtonClickedSlot(self):
@qslot
def _createVersionPushButtonClickedSlot(self, *args):
"""
Allow user to create a new version of an appliance
"""
new_version, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Creating a new version allows to import unknown files to use with this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.Normal)
if ok:
self._appliance.create_new_version(new_version)
self._refreshVersions()
try:
self._appliance.create_new_version(new_version)
except ApplianceError as e:
QtWidgets.QMessageBox.critical(self.parent(), "Create new version", str(e))
return
self.images_changed_signal.emit()
def _importPushButtonClickedSlot(self):
@qslot
def _importPushButtonClickedSlot(self, *args):
"""
Called when user want to import an appliance images.
He should have selected the file before.
"""
if self._refreshing:
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
if not current:
return
disk = current.data(1, QtCore.Qt.UserRole)
path, _ = QtWidgets.QFileDialog.getOpenFileName()
if len(path) == 0:
return
image = Image(path)
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct file. The MD5 sum is {} and should be {}. For OVA you need to import the OVA/OVF not the file inside the archive.".format(image.md5sum, disk["md5sum"]))
image = Image(self._appliance.emulator(), path, filename=disk["filename"])
try:
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct file. The MD5 sum is {} and should be {}.".format(image.md5sum, disk["md5sum"]))
return
except OSError as e:
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
return
config = Config()
worker = WaitForLambdaWorker(lambda: image.copy(os.path.join(config.images_dir, self._appliance.image_dir_name()), disk["filename"]), allowed_exceptions=[OSError, ValueError])
progress_dialog = ProgressDialog(worker, "Add appliance", "Importing the appliance...", None, busy=True, parent=self)
if not progress_dialog.exec_():
return
self._refreshVersions()
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):
"""
@@ -368,6 +461,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiQemuListComboBox.addItem("{path}".format(path=qemu["path"]), qemu["path"])
if self.uiQemuListComboBox.count() == 1:
self.next()
else:
i = self.uiQemuListComboBox.findText(self._appliance["qemu"]["arch"], QtCore.Qt.MatchContains)
if i != -1:
self.uiQemuListComboBox.setCurrentIndex(i)
def _install(self, version):
"""
@@ -385,17 +482,26 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if version is None:
appliance_configuration = self._appliance.copy()
else:
appliance_configuration = self._appliance.search_images_for_version(version)
try:
appliance_configuration = self._appliance.search_images_for_version(version)
except ApplianceError as e:
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
return False
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:
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, self._server), allowed_exceptions=[ConfigException, OSError])
worker = WaitForLambdaWorker(
lambda: config.add_appliance(appliance_configuration, self._compute_id, self._symbols),
allowed_exceptions=[ConfigException, OSError])
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
progress_dialog.show()
if not progress_dialog.exec_():
@@ -408,6 +514,26 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
return True
def _uploadImages(self, version):
"""
Upload an image to the compute
"""
appliance_configuration = self._appliance.search_images_for_version(version)
for image in appliance_configuration["images"]:
if image["location"] == "local":
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
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):
self._image_uploading_count -= 1
def nextId(self):
if self.currentPage() == self.uiServerWizardPage:
if "docker" in self._appliance:
@@ -425,8 +551,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
"""
if self.currentPage() == self.uiFilesWizardPage:
if self._refreshing:
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
if current is None or sip.isdeleted(current):
return False
version = current.data(0, QtCore.Qt.UserRole)
if version is None:
return False
appliance = current.data(2, QtCore.Qt.UserRole)
if not self._appliance.is_version_installable(version["name"]):
QtWidgets.QMessageBox.warning(self, "Appliance", "Sorry, you cannot install {} with missing files".format(appliance["name"]))
@@ -435,8 +567,13 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return False
self._uploadImages(version["name"])
elif self.currentPage() == self.uiUsageWizardPage:
if self._image_uploading_count > 0:
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for image uploading")
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
if current:
version = current.data(0, QtCore.Qt.UserRole)
@@ -449,17 +586,18 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if len(ComputeManager.instance().remoteComputes()) == 0:
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
return False
self._server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
self._server = "vm"
self._compute_id = "vm"
else:
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")):
if "qemu" in self._appliance:
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and MacOSX is not supported by the GNS3 team. Are you sur to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return False
if ComputeManager.instance().localPlatform():
if (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
if "qemu" in self._appliance:
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and MacOSX is not supported by the GNS3 team. Are you sur to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return False
self._server = "local"
self._compute_id = "local"
elif self.currentPage() == self.uiQemuWizardPage:
if self.uiQemuListComboBox.currentIndex() == -1:
@@ -471,6 +609,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
return True
@qslot
def _vmToggledSlot(self, checked):
"""
Slot for when the VM radio button is toggled.
@@ -481,6 +620,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiRemoteServersGroupBox.setEnabled(False)
self.uiRemoteServersGroupBox.hide()
@qslot
def _remoteServerToggledSlot(self, checked):
"""
Slot for when the remote server radio button is toggled.
@@ -492,6 +632,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiRemoteServersGroupBox.setEnabled(True)
self.uiRemoteServersGroupBox.show()
@qslot
def _localToggledSlot(self, checked):
"""
Slot for when the local server radio button is toggled.

View File

@@ -49,6 +49,10 @@ class ConfigurationDialog(QtWidgets.QDialog, Ui_configurationDialog):
self._settings = settings
self._configuration_page = configuration_page
@property
def settings(self):
return self._settings
def on_uiButtonBox_clicked(self, button):
"""
Slot called when a button of the uiButtonBox is clicked.

View File

@@ -22,8 +22,8 @@ from gns3.qt import QtWidgets
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_SERIAL_CONSOLE_COMMANDS, \
PRECONFIGURED_VNC_CONSOLE_COMMANDS, \
PRECONFIGURED_SPICE_CONSOLE_COMMANDS, \
CUSTOM_CONSOLE_COMMANDS_SETTINGS
@@ -39,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)
@@ -63,8 +63,8 @@ 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])
else:
self._consoles = copy.copy(PRECONFIGURED_SERIAL_CONSOLE_COMMANDS)
elif self._console_type == "spice":
self._consoles = copy.copy(PRECONFIGURED_SPICE_CONSOLE_COMMANDS)
self._consoles.update(self._settings[self._console_type])
self.uiCommandComboBox.clear()

View File

@@ -95,13 +95,14 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
def checkAVGInstalled(self):
"""Checking if AVG software is not installed"""
for proc in psutil.process_iter():
try:
psinfo = proc.as_dict(["exe"])
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
except psutil.NoSuchProcess:
pass
if sys.platform.startswith("win32"):
for proc in psutil.process_iter():
try:
psinfo = proc.as_dict(["exe"])
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
except psutil.NoSuchProcess:
pass
return (0, None)
def checkFreeRam(self):
@@ -136,20 +137,17 @@ 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"):
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 require CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
else:
# capabilities not supported
request_setuid = True
try:
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: {}".format(e))
if sys.platform.startswith("darwin") or request_setuid:
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 {path} and sudo chmod 4755 {path}".format(path=path))
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)
def checkDynamipsPermission(self):
@@ -164,11 +162,15 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
if not os.path.exists(path):
return (2, "Dynamips path {path} doesn't exists".format(path=path))
if sys.platform.startswith("linux") and "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, "Dynamips require CAP_NET_RAW. Run sudo setcap cap_net_raw,cap_net_admin+eip {path}".format(path=path))
try:
if sys.platform.startswith("linux") and "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, "Dynamips requires CAP_NET_RAW. Run sudo setcap cap_net_raw,cap_net_admin+eip {path}".format(path=path))
except AttributeError:
# 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 Dynamips (Python bug)".format(path=path))
return (0, None)
def checkGNS3InstalledTwice(self):
@@ -220,5 +222,5 @@ if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
main = QtWidgets.QMainWindow()
dialog = DoctorDialog(main, console=True)
#dialog.show()
# dialog.show()
#exit_code = app.exec_()

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()
@@ -89,6 +86,9 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
if not re.match(r"^[a-zA-Z0-9\.{}-]+$".format("\u0370-\u1CDF\u2C00-\u30FF\u4E00-\u9FBF"), host):
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server hostname {}".format(host))
return
if name == "gns3vm":
QtWidgets.QMessageBox.critical(self, "Remote compute", "{} is a reserved name".format(name))
return
if len(name) == 0:
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server name {}".format(name))
return

View File

@@ -15,9 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import re
from ..qt import QtWidgets
from ..topology import Topology
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog

View File

@@ -25,6 +25,7 @@ from gns3.version import __version__
from gns3.qt import QtWidgets, QtCore
from gns3.ui.export_debug_dialog_ui import Ui_ExportDebugDialog
from gns3.local_config import LocalConfig
from gns3.controller import Controller
import logging
log = logging.getLogger(__name__)
@@ -44,16 +45,27 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
def _okButtonClickedSlot(self):
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export debug file", None, "Zip file (*.zip)", "Zip file (*.zip)")
if len(path) == 0:
if Controller.instance().isRemote():
QtWidgets.QMessageBox.critical(self, "Debug", "Export debug information from a remote server is not supported")
self.reject()
return
log.info("Export debug information to %s", path)
self._path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export debug file", None, "Zip file (*.zip)", "Zip file (*.zip)")
if len(self._path) == 0:
self.reject()
return
if Controller.instance().connected():
Controller.instance().post("/debug", self._exportDebugCallback)
else:
self._exportDebugCallback({}, error=True)
def _exportDebugCallback(self, result, error=False, **kwargs):
log.debug("Export debug information to %s", self._path)
try:
with ZipFile(path, 'w') as zip:
with ZipFile(self._path, 'w') as zip:
zip.writestr("debug.txt", self._getDebugData())
dir = LocalConfig.instance().configDirectory()
for filename in os.listdir(dir):
@@ -61,12 +73,20 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
if os.path.isfile(path):
zip.write(path, filename)
dir = self._project.filesDir()
if dir:
dir = os.path.join(LocalConfig.instance().configDirectory(), "debug")
if os.path.exists(dir):
for filename in os.listdir(dir):
path = os.path.join(dir, filename)
if os.path.isfile(path):
zip.write(path, filename)
if self._project:
dir = self._project.filesDir()
if dir:
for filename in os.listdir(dir):
path = os.path.join(dir, filename)
if os.path.isfile(path):
zip.write(path, filename)
except OSError as e:
QtWidgets.QMessageBox.critical(self, "Debug", "Can't export debug information: {}".format(str(e)))
self.accept()

View File

@@ -67,6 +67,6 @@ class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
if not error:
self.uiFileTextEdit.setText(raw_body.decode("utf-8"))
elif result["status"] == 404:
elif result.get("status") == 404:
if self._default:
self.uiFileTextEdit.setText(self._default)

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

@@ -40,7 +40,8 @@ class IdlePCDialog(QtWidgets.QDialog, Ui_IdlePCDialog):
self._idlepcs = idlepcs
for value in self._idlepcs:
match = re.search(r"^(0x[0-9a-f]+)\s+\[(\d+)\]$", value)
# validate idle-pc format, e.g. 0x60c09aa0
match = re.search(r"^(0x[0-9a-f]{8})\s+\[(\d+)\]$", value)
if match:
idlepc = match.group(1)
count = int(match.group(2))

View File

@@ -137,9 +137,17 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
if page != self.uiEmptyPageWidget:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(True)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(True)
else:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(False)
# hide the contextual help button if there is no help text
if page.whatsThis():
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).show()
else:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).hide()
def on_uiButtonBox_clicked(self, button):
"""
@@ -153,6 +161,8 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
self.applySettings()
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset):
self.resetSettings()
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help):
self.showHelp()
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
QtWidgets.QDialog.reject(self)
else:
@@ -215,6 +225,14 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
child = item.child(index)
child.setSettings(child.node().settings().copy())
def showHelp(self):
"""
Show contextual help for the current page.
"""
page = self.uiConfigStackedWidget.currentWidget()
if page != self.uiEmptyPageWidget and page.whatsThis():
QtWidgets.QMessageBox.information(self, "{} help".format(page.windowTitle()), page.whatsThis().strip())
class ConfigurationPageItem(QtWidgets.QTreeWidgetItem):

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

@@ -27,6 +27,9 @@ from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
from ..pages.gns3_vm_preferences_page import GNS3VMPreferencesPage
from ..modules import MODULES
import logging
log = logging.getLogger(__name__)
class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
@@ -41,6 +44,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
super().__init__(parent)
self.setupUi(self)
self._modified_pages = set()
# We adapt the max size to the screen resolution
# We need to manually do that otherwise on small screen the windows
@@ -102,6 +106,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
parent = self.uiTreeWidget
for cls in preference_pages:
preferences_page = cls()
preferences_page.setParent(self)
preferences_page.loadPreferences()
name = preferences_page.windowTitle()
item = QtWidgets.QTreeWidgetItem(parent)
@@ -124,6 +129,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
# Class name, changed signal
widget_to_watch = {
QtWidgets.QLineEdit: "textChanged",
QtWidgets.QPlainTextEdit: "textChanged",
# QtWidgets.QTreeWidget: "itemChanged",
QtWidgets.QComboBox: "currentIndexChanged",
QtWidgets.QSpinBox: "valueChanged",
@@ -140,17 +146,23 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
"""
# Found the page with the change
widget = self.sender()
widget = sender = self.sender()
while widget.parent() != self.uiStackedWidget:
widget = widget.parent()
self.addModifiedPage(widget)
if self.addModifiedPage(widget):
log.debug("%s value has changed", sender.objectName())
def addModifiedPage(self, widget):
"""
:returns: True is the page is initialized and element added
"""
# The widget can trigger signal before the end of init due to async api call
if not hasattr(widget, 'pageInitialized') or widget.pageInitialized():
self._applyButton.setEnabled(True)
self._modified_pages.add(widget)
return True
return False
def _showPreferencesPageSlot(self, current, previous):
"""
@@ -182,7 +194,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
"""
success = True
for preferences_page in self._modified_pages:
for preferences_page in list(self._modified_pages):
ok = preferences_page.savePreferences()
# if page.savePreferences() returns None, assume success
if ok is not None and not ok:

View File

@@ -17,6 +17,7 @@
import os
import sys
import shutil
from gns3.qt import QtWidgets
from gns3.local_config import LocalConfig
@@ -41,6 +42,7 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
self.setupUi(self)
self.uiNewPushButton.clicked.connect(self._newPushButtonSlot)
self.uiDeletePushButton.clicked.connect(self._deletePushButtonSlot)
# Center on screen
screen = QtWidgets.QApplication.desktop().screenGeometry()
@@ -52,15 +54,20 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3")
profiles_path = os.path.join(path, "profiles")
self.profiles_path = os.path.join(path, "profiles")
self.uiShowAtStartupCheckBox.setChecked(LocalConfig.instance().multiProfiles())
self._refresh()
def _refresh(self):
self.uiProfileSelectComboBox.clear()
self.uiProfileSelectComboBox.addItem("default")
try:
if os.path.exists(profiles_path):
for profil in sorted(os.listdir(os.path.join(path, "profiles"))):
self.uiProfileSelectComboBox.addItem(profil)
if os.path.exists(self.profiles_path):
for profil in sorted(os.listdir(self.profiles_path)):
if not profil.startswith("."):
self.uiProfileSelectComboBox.addItem(profil)
except OSError:
pass
@@ -78,8 +85,19 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
self.uiProfileSelectComboBox.setCurrentText(profile)
self.accept()
def _deletePushButtonSlot(self):
profile = self.uiProfileSelectComboBox.currentText()
if profile == "default":
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", "You can't delete the default profile")
else:
try:
shutil.rmtree(os.path.join(self.profiles_path, profile))
self._refresh()
except (OSError, PermissionError) as e:
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", str(e))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
dialog = ProfileSelectDialog()
dialog.show()

View File

@@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from ..qt import QtCore, QtGui, QtWidgets
from ..qt import QtCore, QtGui, QtWidgets, qslot
from ..ui.project_dialog_ui import Ui_ProjectDialog
from ..controller import Controller
from ..topology import Topology
@@ -43,7 +43,6 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
self.setupUi(self)
self._main_window = parent
self._projects = []
self._project_settings = {}
self.uiNameLineEdit.setText(default_project_name)
self.uiLocationLineEdit.setText(os.path.join(Topology.instance().projectsDirPath(), default_project_name))
@@ -65,15 +64,14 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
self.uiLocationLineEdit.setVisible(False)
self.uiLocationBrowserToolButton.setVisible(False)
self.uiOpenProjectPushButton.setVisible(False)
Controller.instance().connected_signal.connect(self._refreshProjects)
self.uiProjectsTreeWidget.itemDoubleClicked.connect(self._projectsTreeWidgetDoubleClickedSlot)
self.uiDeleteProjectButton.clicked.connect(self._deleteProjectSlot)
self.uiRefreshProjectsPushButton.clicked.connect(self._refreshProjects)
self._refreshProjects()
def _refreshProjects(self):
Controller.instance().get("/projects", self._projectListCallback)
self.uiDuplicateProjectPushButton.clicked.connect(self._duplicateProjectSlot)
self.uiRefreshProjectsPushButton.clicked.connect(Controller.instance().refreshProjectList)
Controller.instance().project_list_updated_signal.connect(self._updateProjectListSlot)
self._updateProjectListSlot()
Controller.instance().refreshProjectList()
def _settingsClickedSlot(self):
"""
@@ -85,9 +83,9 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
def _projectsTreeWidgetDoubleClickedSlot(self, item, column):
self.done(True)
def _deleteProjectSlot(self):
current = self.uiProjectsTreeWidget.currentItem()
if current is None:
@qslot
def _deleteProjectSlot(self, *args):
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
QtWidgets.QMessageBox.critical(self, "Delete project", "No project selected")
return
@@ -105,39 +103,75 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
projects_to_delete.add(project_id)
for project_id in projects_to_delete:
Controller.instance().delete("/projects/{}".format(project_id), self._deleteProjectCallback)
Controller.instance().deleteProject(project_id)
def _deleteProjectCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while deleting project: {}".format(result["message"]))
def _duplicateProjectSlot(self):
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
QtWidgets.QMessageBox.critical(self, "Duplicate project", "No project selected")
return
Controller.instance().get("/projects", self._projectListCallback)
def _projectListCallback(self, result, error=False, **kwargs):
if len(self.uiProjectsTreeWidget.selectedItems()) > 1:
QtWidgets.QMessageBox.critical(self, "Duplicate project", "Please select only one project to duplicate")
return
for project in self.uiProjectsTreeWidget.selectedItems():
project_id = project.data(0, QtCore.Qt.UserRole)
project_name = project.data(1, QtCore.Qt.UserRole)
new_project_name = project_name + "-1"
existing_project_name = [p["name"] for p in Controller.instance().projects()]
i = 1
while new_project_name in existing_project_name:
new_project_name = "{}-{}".format(project_name, i)
i += 1
name, reply = QtWidgets.QInputDialog.getText(self,
"Duplicate project",
'Duplicate project "{}"?.'.format(project_name),
QtWidgets.QLineEdit.Normal,
new_project_name)
name = name.strip()
if reply and len(name) > 0:
if Controller.instance().isRemote():
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
self._duplicateCallback,
body={"name": name})
else:
project_location = os.path.join(Topology.instance().projectsDirPath(), name)
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
self._duplicateCallback,
body={"name": name, "path": project_location})
def _duplicateCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while duplicating project: {}".format(result["message"]))
return
Controller.instance().refreshProjectList()
@qslot
def _updateProjectListSlot(self, *args):
self.uiProjectsTreeWidget.clear()
self.uiDeleteProjectButton.setEnabled(False)
if not error:
self._projects = result
self.uiProjectsTreeWidget.setUpdatesEnabled(False)
items = []
for project in result:
path = os.path.join(project["path"], project["filename"])
item = QtWidgets.QTreeWidgetItem([project["name"], project["status"], path])
item.setData(0, QtCore.Qt.UserRole, project["project_id"])
item.setData(1, QtCore.Qt.UserRole, project["name"])
item.setData(2, QtCore.Qt.UserRole, path)
items.append(item)
self.uiProjectsTreeWidget.addTopLevelItems(items)
self.uiProjectsTreeWidget.setUpdatesEnabled(False)
items = []
for project in Controller.instance().projects():
path = os.path.join(project["path"], project["filename"])
item = QtWidgets.QTreeWidgetItem([project["name"], project["status"], path])
item.setData(0, QtCore.Qt.UserRole, project["project_id"])
item.setData(1, QtCore.Qt.UserRole, project["name"])
item.setData(2, QtCore.Qt.UserRole, path)
items.append(item)
self.uiProjectsTreeWidget.addTopLevelItems(items)
if len(result):
self.uiDeleteProjectButton.setEnabled(True)
if len(Controller.instance().projects()):
self.uiDeleteProjectButton.setEnabled(True)
self.uiProjectsTreeWidget.header().setResizeContentsPrecision(100) # How many row is checked for the resize for performance reason
self.uiProjectsTreeWidget.resizeColumnToContents(0)
self.uiProjectsTreeWidget.resizeColumnToContents(1)
self.uiProjectsTreeWidget.resizeColumnToContents(2)
self.uiProjectsTreeWidget.sortItems(0, QtCore.Qt.AscendingOrder)
self.uiProjectsTreeWidget.setUpdatesEnabled(True)
self.uiProjectsTreeWidget.header().setResizeContentsPrecision(100) # How many row is checked for the resize for performance reason
self.uiProjectsTreeWidget.resizeColumnToContents(0)
self.uiProjectsTreeWidget.resizeColumnToContents(1)
self.uiProjectsTreeWidget.resizeColumnToContents(2)
self.uiProjectsTreeWidget.sortItems(0, QtCore.Qt.AscendingOrder)
self.uiProjectsTreeWidget.setUpdatesEnabled(True)
def keyPressEvent(self, e):
"""
@@ -194,51 +228,67 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
menu = QtWidgets.QMenu()
menu.triggered.connect(self._menuTriggeredSlot)
for action in self._main_window._recent_project_actions:
menu.addAction(action)
if Controller.instance().isRemote():
for action in self._main_window.recent_project_actions:
menu.addAction(action)
else:
for action in self._main_window.recent_file_actions:
menu.addAction(action)
menu.exec_(QtGui.QCursor.pos())
def _overwriteProjectCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
log.error("Error while overwrite project: {}".format(result["message"]))
return
self._projects = []
self._refreshProjects()
# A 404 could arrive if someone else as deleted the project
if "status" not in result or result["status"] != 404:
return
elif "message" in result:
QtWidgets.QMessageBox.critical(self,
"New Project",
"Error while overwrite project: {}".format(result["message"]))
Controller.instance().refreshProjectList()
self.done(True)
def _newProject(self):
project_name = self.uiNameLineEdit.text()
self._project_settings["project_name"] = self.uiNameLineEdit.text().strip()
if Controller.instance().isRemote():
self._project_settings.pop("project_path", None)
self._project_settings.pop("project_files_dir", None)
else:
project_location = self.uiLocationLineEdit.text().strip()
if not project_location:
QtWidgets.QMessageBox.critical(self, "New project", "Project location is empty")
return False
if not project_name:
self._project_settings["project_path"] = os.path.join(project_location, self._project_settings["project_name"] + ".gns3")
self._project_settings["project_files_dir"] = project_location
if len(self._project_settings["project_name"]) == 0:
QtWidgets.QMessageBox.critical(self, "New project", "Project name is empty")
return False
for existing_project in self._projects:
if project_name == existing_project["name"]:
for existing_project in Controller.instance().projects():
if self._project_settings["project_name"] == existing_project["name"] \
or ("project_files_dir" in self._project_settings and self._project_settings["project_files_dir"] == existing_project["path"]):
if existing_project["status"] == "opened":
QtWidgets.QMessageBox.critical(self,
"New project",
"Project {} is open you can not overwrite it".format(self._project_settings["project_name"]))
return False
reply = QtWidgets.QMessageBox.warning(self,
"New project",
"Project {} already exists, overwrite it?".format(project_name),
"Project {} already exists, overwrite it?".format(existing_project["name"]),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
Controller.instance().delete("/projects/{}".format(existing_project["project_id"]), self._overwriteProjectCallback)
Controller.instance().deleteProject(existing_project["project_id"], self._overwriteProjectCallback)
# In all cases we cancel the new project and if project success to delete
# we will call done again
return False
self._project_settings["project_name"] = project_name
if not Controller.instance().isRemote():
project_location = self.uiLocationLineEdit.text()
if not project_location:
QtWidgets.QMessageBox.critical(self, "New project", "Project location is empty")
return False
self._project_settings["project_path"] = os.path.join(project_location, project_name + ".gns3")
self._project_settings["project_files_dir"] = project_location
return True
def done(self, result):
@@ -255,5 +305,4 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
self._project_settings["project_id"] = current.data(0, QtCore.Qt.UserRole)
self._project_settings["project_name"] = current.data(1, QtCore.Qt.UserRole)
self._project_settings["project_path"] = current.data(2, QtCore.Qt.UserRole)
super().done(result)

View File

@@ -19,17 +19,21 @@ import sys
import os
import shutil
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork, qslot
from gns3.controller import Controller
from gns3.local_server import LocalServer
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
from gns3.dialogs.new_appliance_dialog import NewApplianceDialog
from ..settings import DEFAULT_LOCAL_SERVER_HOST
from ..ui.setup_wizard_ui import Ui_SetupWizard
from ..version import __version__
import logging
log = logging.getLogger(__name__)
class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
@@ -56,6 +60,8 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
# we want to see the cancel button on OSX
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
self.uiLocalServerToolButton.clicked.connect(self._localServerBrowserSlot)
self.uiGNS3VMDownloadLinkUrlLabel.setText('')
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
@@ -76,15 +82,13 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
# load all available addresses
for address in QtNetwork.QNetworkInterface.allAddresses():
address_string = address.toString()
# if address.protocol() == QtNetwork.QAbstractSocket.IPv6Protocol:
# we do not want the scope id when using an IPv6 address...
# address.setScopeId("")
self.uiLocalServerHostComboBox.addItem(address_string, address.toString())
if address.protocol() != QtNetwork.QAbstractSocket.IPv6Protocol:
self.uiLocalServerHostComboBox.addItem(address_string, address.toString())
if sys.platform.startswith("darwin"):
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.jpg"))
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.png"))
else:
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.jpg"))
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.png"))
if sys.platform.startswith("linux"):
self.uiVMRadioButton.setText("Run the topologies in an isolated and standard VM")
@@ -92,6 +96,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
self.uiLocalRadioButton.setChecked(True)
self.uiLocalLabel.setVisible(False)
Controller.instance().connected_signal.connect(self._refreshLocalServerStatusSlot)
Controller.instance().connection_failed_signal.connect(self._refreshLocalServerStatusSlot)
def _localServerBrowserSlot(self):
"""
Slot to open a file browser and select a local server.
@@ -109,9 +116,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
def _VMwareBannerButtonClickedSlot(self):
if sys.platform.startswith("darwin"):
url = "http://send.onenetworkdirect.net/z/616461/CD225091/"
url = "http://send.onenetworkdirect.net/z/621394/CD225091/"
else:
url = "http://send.onenetworkdirect.net/z/616460/CD225091/"
url = "http://send.onenetworkdirect.net/z/616207/CD225091/"
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
def _listVMwareVMsSlot(self):
@@ -125,7 +132,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
from gns3.modules import VMware
settings = VMware.instance().settings()
if not os.path.exists(settings["vmrun_path"]):
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://www.vmware.com/support/developer/vix-api/")
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://www.vmware.com/support/developer/vix-api/. After installation you need to restart GNS3.")
return
self._refreshVMListSlot()
@@ -141,7 +148,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
from gns3.modules import VirtualBox
settings = VirtualBox.instance().settings()
if not os.path.exists(settings["vboxmanage_path"]):
QtWidgets.QMessageBox.critical(self, "VirtualBox", "VBoxManage could not be found, VirtualBox is probably not installed")
QtWidgets.QMessageBox.critical(self, "VirtualBox", "VBoxManage could not be found, VirtualBox is probably not installed. After installation you need to restart GNS3.")
return
self._refreshVMListSlot()
@@ -176,6 +183,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
super().initializePage(page_id)
if self.page(page_id) == self.uiServerWizardPage:
Controller.instance().setDisplayError(False)
Controller.instance().get("/gns3vm", self._getSettingsCallback)
elif self.page(page_id) == self.uiVMWizardPage:
if self._GNS3VMSettings()["engine"] == "vmware":
@@ -195,15 +203,36 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
self.uiLocalServerHostComboBox.setCurrentIndex(index)
self.uiLocalServerPortSpinBox.setValue(local_server_settings["port"])
elif self.page(page_id) == self.uiRemoteControllerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
if local_server_settings["host"] is None:
self.uiRemoteMainServerHostLineEdit.setText(DEFAULT_LOCAL_SERVER_HOST)
self.uiRemoteMainServerAuthCheckBox.setChecked(False)
self.uiRemoteMainServerUserLineEdit.setText("")
self.uiRemoteMainServerPasswordLineEdit.setText("")
else:
self.uiRemoteMainServerHostLineEdit.setText(local_server_settings["host"])
self.uiRemoteMainServerAuthCheckBox.setChecked(local_server_settings["auth"])
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
self.uiRemoteMainServerPasswordLineEdit.setText(local_server_settings["password"])
self.uiRemoteMainServerPortSpinBox.setValue(local_server_settings["port"])
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
self._refreshLocalServerStatusSlot()
elif self.page(page_id) == self.uiSummaryWizardPage:
use_local_server = self.uiLocalRadioButton.isChecked()
self.uiSummaryTreeWidget.clear()
if use_local_server:
if self.uiLocalRadioButton.isChecked():
local_server_settings = LocalServer.instance().localServerSettings()
self._addSummaryEntry("Server type:", "Local")
self._addSummaryEntry("Path:", local_server_settings["path"])
self._addSummaryEntry("Host:", local_server_settings["host"])
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
elif self.uiRemoteControllerRadioButton.isChecked():
local_server_settings = LocalServer.instance().localServerSettings()
self._addSummaryEntry("Server type:", "Remote")
self._addSummaryEntry("Host:", local_server_settings["host"])
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
self._addSummaryEntry("User:", local_server_settings["user"])
else:
self._addSummaryEntry("Server type:", "GNS3 Virtual Machine")
self._addSummaryEntry("VM engine:", self._GNS3VMSettings()["engine"].capitalize())
@@ -211,6 +240,20 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
self._addSummaryEntry("VM vCPUs:", str(self._GNS3VMSettings()["vcpus"]))
self._addSummaryEntry("VM RAM:", str(self._GNS3VMSettings()["ram"]) + " MB")
@qslot
def _refreshLocalServerStatusSlot(self):
"""
Refresh the local server status page
"""
if Controller.instance().connected():
self.uiLocalServerStatusLabel.setText("Connection to local server successful")
Controller.instance().get("/gns3vm", self._getSettingsCallback)
elif Controller.instance().connecting():
self.uiLocalServerStatusLabel.setText("Please wait connection to the GNS3 server")
else:
local_server_settings = LocalServer.instance().localServerSettings()
self.uiLocalServerStatusLabel.setText("Connection to local server failed.\n* Make sure GNS3 is allowed in your firewall.\n* Go back and try to change the server port\n* Please check with a browser if you can connect to {protocol}://{host}:{port}.\n* Try to run {path} in a terminal to see if you have an error if the above does not work.".format(protocol=local_server_settings["protocol"], host=local_server_settings["host"], port=local_server_settings["port"], path=local_server_settings["path"]))
def _GNS3VMSettings(self):
return self._gns3_vm_settings
@@ -236,6 +279,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
Validates the settings.
"""
Controller.instance().setDisplayError(True)
if self.currentPage() == self.uiVMWizardPage:
vmname = self.uiVMListComboBox.currentText()
if vmname:
@@ -279,26 +323,29 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
return False
LocalServer.instance().updateLocalServerSettings(local_server_settings)
LocalServer.instance().localServerAutoStartIfRequire()
elif self.currentPage() == self.uiRemoteControllerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
local_server_settings["auto_start"] = False
local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
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()
LocalServer.instance().updateLocalServerSettings(local_server_settings)
elif self.currentPage() == self.uiSummaryWizardPage:
use_local_server = self.uiLocalRadioButton.isChecked()
if use_local_server:
if self.uiLocalRadioButton.isChecked():
# deactivate the GNS3 VM if using the local server
vm_settings = self._GNS3VMSettings()
vm_settings["enable"] = False
self._setGNS3VMSettings(vm_settings)
# update the modules so they use the local server
from gns3.modules import Dynamips
Dynamips.instance().setSettings({"use_local_server": use_local_server})
if sys.platform.startswith("linux"):
# IOU only works on Linux
from gns3.modules import IOU
IOU.instance().setSettings({"use_local_server": use_local_server})
from gns3.modules import Qemu
Qemu.instance().setSettings({"use_local_server": use_local_server})
from gns3.modules import VPCS
VPCS.instance().setSettings({"use_local_server": use_local_server})
elif self.currentPage() == self.uiLocalServerStatusWizardPage:
if not Controller.instance().connected():
return False
return True
@@ -345,12 +392,19 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
:param result: ignored
"""
Controller.instance().setDisplayError(True)
settings = self.parentWidget().settings()
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
if result:
settings["hide_setup_wizard"] = True
else:
local_server_settings = LocalServer.instance().localServerSettings()
if local_server_settings["host"] is None:
local_server_settings["host"] = DEFAULT_LOCAL_SERVER_HOST
LocalServer.instance().updateLocalServerSettings(local_server_settings)
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
self.parentWidget().setSettings(settings)
super().done(result)
dialog = NewApplianceDialog(self.parentWidget())
dialog.show()
def nextId(self):
"""
@@ -358,9 +412,21 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
current_id = self.currentId()
if self.page(current_id) == self.uiServerWizardPage and self.uiVMRadioButton.isChecked():
# skip the local server page if using the GNS3 VM
return self.uiLocalServerWizardPage.nextId()
if self.page(current_id) == self.uiLocalServerWizardPage:
return self.uiVMWizardPage.nextId()
if self.page(current_id) == self.uiLocalServerStatusWizardPage and not self.uiVMRadioButton.isChecked():
return self._pageId(self.uiSummaryWizardPage)
if self.page(current_id) == self.uiServerWizardPage and self.uiRemoteControllerRadioButton.isChecked():
return self._pageId(self.uiRemoteControllerWizardPage)
if self.page(current_id) == self.uiVMWizardPage:
return self._pageId(self.uiSummaryWizardPage)
return QtWidgets.QWizard.nextId(self)
def _pageId(self, page):
"""
Return id of the page
"""
for id in self.pageIds():
if self.page(id) == page:
return id
raise KeyError

View File

@@ -19,17 +19,11 @@
Dialog to manage the snapshots.
"""
import shutil
import re
import time
import os
from ..qt import QtCore, QtWidgets
from ..utils.progress_dialog import ProgressDialog
from ..utils.process_files_worker import ProcessFilesWorker
from ..ui.snapshots_dialog_ui import Ui_SnapshotsDialog
from ..node import Node
from ..controller import Controller
from ..utils.progress_dialog import ProgressDialog
from ..utils.create_snapshot_worker import CreateSnapshotWorker
from datetime import datetime
@@ -64,7 +58,8 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
"""
self.uiSnapshotsList.clear()
Controller.instance().get("/projects/{}/snapshots".format(self._project.id()), self._listSnapshotsCallback)
if self._project:
Controller.instance().get("/projects/{}/snapshots".format(self._project.id()), self._listSnapshotsCallback)
def _listSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
@@ -91,16 +86,22 @@ 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:
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()), self._createSnapshotsCallback, {"name": snapshot_name})
if ok and snapshot_name and self._project:
snapshot_worker = CreateSnapshotWorker(self._project, snapshot_name)
snapshot_worker.finished.connect(self._createSnapshotsCallback)
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])
return
progress_dialog = ProgressDialog(snapshot_worker, "Snapshot progress", "Creation of snapshot in progress...",
"Cancel", busy=True, parent=self, create_thread=False, cancelable=True)
progress_dialog.show()
progress_dialog.exec_()
def _createSnapshotsCallback(self):
self._listSnapshots()
def _createSnapshotsErrorCallback(self, message, error):
log.error(message)
def _deleteSnapshotSlot(self):
"""
Slot to delete a snapshot.
@@ -138,7 +139,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

@@ -19,7 +19,7 @@
Text editor to edit Note items.
"""
from ..qt import QtCore, QtWidgets
from ..qt import QtCore, QtWidgets, qslot
from ..ui.text_editor_dialog_ui import Ui_TextEditorDialog
@@ -70,16 +70,19 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
color.blue(),
color.alpha()))
def _setFontSlot(self):
@qslot
def _setFontSlot(self, *args):
"""
Slot to select the font.
"""
selected_font, ok = QtWidgets.QFontDialog.getFont(self.uiPlainTextEdit.font(), self)
selected_font, ok = QtWidgets.QFontDialog.getFont(self.uiPlainTextEdit.font(), self,
options=QtWidgets.QFontDialog.DontUseNativeDialog)
if ok:
self.uiPlainTextEdit.setFont(selected_font)
def _setColorSlot(self):
@qslot
def _setColorSlot(self, *args):
"""
Slot to select the color.
"""
@@ -88,7 +91,8 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
if color.isValid():
self._setColor(color)
def _applyPreferencesSlot(self):
@qslot
def _applyPreferencesSlot(self, *args):
"""
Applies the new text settings.
"""

View File

@@ -21,24 +21,22 @@ from gns3.qt import QtWidgets
from gns3.controller import Controller
class VMWithImagesWizard(VMWizard):
"""
Base class for VM wizard with image management (Qemu, IOU...)
:param devices: List of existing device for this type
:param use_local_server: Value the use_local_server settings for this module
:param parent: parent widget
"""
def __init__(self, devices, use_local_server, parent):
def __init__(self, devices, parent):
# The list of images combo box (Qemu support multiple images)
self._images_combo_boxes = set()
# The list of radio button for existing image or new images
self._radio_existing_images_buttons = set()
super().__init__(devices, use_local_server, parent)
super().__init__(devices, parent)
def refreshImageStepsButtons(self):
"""
@@ -144,7 +142,7 @@ class VMWithImagesWizard(VMWizard):
:param endpoint: server endpoint with the list of Images
"""
Controller.instance().get("/computes/{}{}".format(self._compute_id, endpoint), self._getImagesFromServerCallback)
Controller.instance().getCompute(endpoint, self._compute_id, self._getImagesFromServerCallback)
def _getImagesFromServerCallback(self, result, error=False, **kwargs):
"""
@@ -183,10 +181,8 @@ class VMWithImagesWizard(VMWizard):
for vm in result:
combo_box.addItem(vm["path"], vm)
def _widgetOnCurrentPage(self, widget):
"""
:returns Boolean True if widget is current active Wizard page
"""
return self.currentPage().findChild(widget.__class__, widget.objectName()) is not None

View File

@@ -19,6 +19,7 @@ import sys
from gns3.qt import QtWidgets
from gns3.compute_manager import ComputeManager
from gns3.controller import Controller
class VMWizard(QtWidgets.QWizard):
@@ -26,18 +27,17 @@ class VMWizard(QtWidgets.QWizard):
Base class for VM wizard.
:param devices: List of existing device for this type
:param use_local_server: Value the use_local_server settings for this module
:param parent: parent widget
"""
def __init__(self, devices, use_local_server, parent):
def __init__(self, devices, parent):
super().__init__(parent)
self.setupUi(self)
self.setModal(True)
self._devices = devices
self._use_local_server = use_local_server
self._local_server_disable = False
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
if sys.platform.startswith("darwin"):
@@ -49,9 +49,11 @@ class VMWizard(QtWidgets.QWizard):
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
if Controller.instance().isRemote():
self.uiLocalRadioButton.setText("Run device on the main server")
# By default we use the local server
self._compute_id = ComputeManager.instance().computes()[0].id()
self._compute_id = "local"
self.uiLocalRadioButton.setChecked(True)
self._localToggledSlot(True)
@@ -103,15 +105,16 @@ class VMWizard(QtWidgets.QWizard):
for compute in ComputeManager.instance().computes():
if compute.id() == "local":
self.uiLocalRadioButton.setEnabled(True)
elif compute.id() == "vm" and hasattr(self, "uiVMRadioButton"):
self.uiVMRadioButton.setEnabled(True)
elif compute.id() == "vm":
if hasattr(self, "uiVMRadioButton"):
self.uiVMRadioButton.setEnabled(True)
else:
self.uiRemoteRadioButton.setEnabled(True)
self.uiRemoteServersComboBox.addItem(compute.name(), compute.id())
if self._use_local_server and self.uiLocalRadioButton.isEnabled() and self.uiLocalRadioButton.isVisible():
if self.uiLocalRadioButton.isEnabled() and not self._local_server_disable:
self.uiLocalRadioButton.setChecked(True)
elif self.uiVMRadioButton.isEnabled():
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isEnabled():
self.uiVMRadioButton.setChecked(True)
else:
if self.uiRemoteRadioButton.isEnabled():
@@ -123,6 +126,7 @@ class VMWizard(QtWidgets.QWizard):
"""
Turn off the local server
"""
self._local_server_disable = True
self.uiLocalRadioButton.hide()
self.uiLocalRadioButton.setEnabled(False)
self.setStartId(0)

View File

@@ -22,19 +22,19 @@ Graphical view on the scene where items are drawn.
import logging
import os
import sip
import pickle
import sys
from .qt import QtCore, QtGui, QtSvg, QtNetwork, QtWidgets, qpartial
from .qt import QtCore, QtGui, QtNetwork, QtWidgets, qpartial, qslot
from .items.node_item import NodeItem
from .dialogs.node_properties_dialog import NodePropertiesDialog
from .link import Link
from .node import Node
from .modules import MODULES
from .modules.builtin.cloud import Cloud
from .modules.module_error import ModuleError
from .modules.builtin import Builtin
from .settings import GRAPHICS_VIEW_SETTINGS
from .topology import Topology
from .ports.port import Port
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
@@ -44,7 +44,6 @@ from .dialogs.file_editor_dialog import FileEditorDialog
from .local_config import LocalConfig
from .progress import Progress
from .utils.server_select import server_select
from .utils.normalize_filename import normalize_filename
from .compute_manager import ComputeManager
# link items
@@ -58,6 +57,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
@@ -85,6 +85,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
@@ -116,6 +117,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:
@@ -125,6 +136,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
@@ -236,6 +249,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.
@@ -280,6 +307,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
link = self._topology.getLink(link_id)
if not link:
return
source_item = None
destination_item = None
source_port = link.sourcePort()
@@ -300,41 +329,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
self.deleteLinkSlot(link_id)
return
# Multi-link management
#
# multi is the offset of the link
# +------+ multi = -1 Link 2 +-------+
# | +-----------------------------+ |
# | R1 | | R2 |
# | | multi = 0 Link 1 | |
# | +-----------------------------+ |
# | | multi = 1 Link 3 | |
# +------+-----------------------------+-------+
if source_item == destination_item:
multi = 0
else:
multi = 0
link_items = source_item.links()
for link_item in link_items:
if link_item.destinationItem().node().id() == destination_item.node().id():
multi += 1
if link_item.sourceItem().node().id() == destination_item.node().id():
multi += 1
# MAX 7 links on the scene between 2 nodes
if multi > 7:
multi = 0
# Pair item represent the bottom links
elif multi % 2 == 0:
multi = multi // 2
else:
multi = -multi // 2
if link.sourcePort().linkType() == "Serial":
link_item = SerialLinkItem(source_item, source_port, destination_item, destination_port, link, multilink=multi)
link_item = SerialLinkItem(source_item, source_port, destination_item, destination_port, link)
else:
link_item = EthernetLinkItem(source_item, source_port, destination_item, destination_port, link, multilink=multi)
link_item = EthernetLinkItem(source_item, source_port, destination_item, destination_port, link)
self.scene().addItem(link_item)
def deleteLinkSlot(self, link_id):
@@ -362,12 +360,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
source_port = source_item.connectToPort()
if not source_port:
return
if not source_item.node().initialized():
QtWidgets.QMessageBox.critical(self, "Connection", "This node hasn't been initialized correctly")
return
if not source_port.isFree():
QtWidgets.QMessageBox.critical(self, "Connection", "Port {} isn't free".format(source_port.name()))
if source_port.link() is not None:
QtWidgets.QMessageBox.warning(self, "Create link", "Can't create the link the port is not free")
return
if source_port.linkType() == "Serial":
self._newlink = SerialLinkItem(source_item, source_port, self.mapToScene(event.pos()), None, adding_flag=True)
else:
@@ -377,24 +374,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
source_item = self._newlink.sourceItem()
source_port = self._newlink.sourcePort()
destination_item = item
if source_item == destination_item:
QtWidgets.QMessageBox.critical(self, "Connection", "Cannot connect to itself!")
return
destination_port = destination_item.connectToPort()
if not destination_port:
return
if not destination_item.node().initialized():
QtWidgets.QMessageBox.critical(self, "Connection", "This node hasn't been initialized correctly")
return
if not destination_port.isFree():
QtWidgets.QMessageBox.critical(self, "Connection", "Port {} isn't free".format(destination_port.name()))
return
elif source_port.linkType() != destination_port.linkType():
QtWidgets.QMessageBox.critical(self, "Connection", "Cannot connect this port!")
return
if isinstance(source_item.node(), Cloud) and isinstance(destination_item.node(), Cloud):
QtWidgets.QMessageBox.critical(self, "Connection", "Sorry, you cannot connect a cloud to another cloud!")
if destination_port.link() is not None:
QtWidgets.QMessageBox.warning(self, "Create link", "Can't create the link the destination port is not free")
return
if self._newlink in self.scene().items():
@@ -436,7 +421,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
else:
item.setSelected(True)
elif is_not_link and event.button() == QtCore.Qt.RightButton and not self._adding_link:
if item:
if item and not sip.isdeleted(item):
# Prevent right clicking on a selected item from de-selecting all other items
if not item.isSelected():
if not event.modifiers() & QtCore.Qt.ControlModifier:
@@ -446,7 +431,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
item.setFlag(item.ItemIsSelectable, True)
item.setSelected(True)
self._showDeviceContextualMenu(QtGui.QCursor.pos())
if item.zValue() < 0:
if not sip.isdeleted(item) and item.zValue() < 0:
item.setFlag(item.ItemIsSelectable, False)
else:
@@ -462,7 +447,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._userNodeLinking(event, item)
elif event.button() == QtCore.Qt.LeftButton and self._adding_note:
pos = self.mapToScene(event.pos())
note = self.createDrawingItem("text", pos.x(), pos.y(), 0)
note = self.createDrawingItem("text", pos.x(), pos.y(), 1)
pos_x = note.pos().x()
pos_y = note.pos().y() - (note.boundingRect().height() / 2)
note.setPos(pos_x, pos_y)
@@ -482,9 +467,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.
@@ -507,6 +500,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.
@@ -519,6 +514,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)
@@ -531,6 +528,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):
"""
@@ -595,10 +593,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
if not self._adding_link:
if isinstance(item, NodeItem) and item.node().initialized():
item.setSelected(True)
if item.node().status() == Node.stopped:
if item.node().status() == Node.stopped or item.node().isAlwaysOn():
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):
@@ -632,7 +632,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:
@@ -646,10 +647,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:
@@ -660,12 +659,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:
@@ -674,7 +673,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
QtWidgets.QMessageBox.critical(self, "Project files", "Please drop only one file")
return
path = event.mimeData().urls()[0].toLocalFile()
if os.path.isfile(path) and self._main_window.checkForUnsavedChanges():
if os.path.isfile(path):
self._main_window.loadPath(path)
event.acceptProposedAction()
else:
@@ -723,6 +722,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)
@@ -748,6 +753,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'))
@@ -802,12 +814,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'))
@@ -820,7 +826,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)
@@ -917,7 +923,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
items = []
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and item.node().initialized():
if isinstance(item, NodeItem) and item.node().initialized() and hasattr(item.node(), "configPage"):
items.append(item)
if items:
@@ -969,12 +975,14 @@ class GraphicsView(QtWidgets.QGraphicsView):
break
if os.path.exists(node_dir):
log.debug("Open %s in file manage")
log.debug("Open %s in file manager")
if QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(node_dir)) is False:
QtWidgets.QMessageBox.critical(self, "Show in file manager", "Failed to open {}".format(node_dir))
break
else:
QtWidgets.QMessageBox.information(self, "Show in file manager", "The device directory is located in {} on {}".format(node_dir, node.compute().name()))
reply = QtWidgets.QMessageBox.information(self, "Show in file manager", "The device directory is located in {} on {}\n\nCopy path to clipboard?".format(node_dir, node.compute().name()), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
QtWidgets.QApplication.clipboard().setText(node_dir)
break
def consoleToNode(self, node, aux=False):
@@ -995,6 +1003,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:
@@ -1010,12 +1023,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
nodes = {}
node_initialized = False
for item in items:
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
node = item.node()
nodes[node.name()] = node
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized():
node_initialized = True
if item.node().status() == Node.started:
node = item.node()
nodes[node.name()] = node
if not nodes:
if not nodes and node_initialized:
if len(items) > 1:
QtWidgets.QMessageBox.warning(self, "Console", "At least one node must be started before a console can be opened")
else:
@@ -1029,6 +1045,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._main_window.run_later(counter, callback)
counter += delay
def consoleFromAllItems(self):
"""
Console from all scene items, except builtin devices.
"""
items = [item for item in self.scene().items()
if not (isinstance(item, NodeItem) and isinstance(item.node().module(), Builtin))]
self.consoleFromItems(items)
def consoleActionSlot(self):
"""
Slot to receive events from the console action in the
@@ -1046,7 +1071,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()
@@ -1056,7 +1081,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)
@@ -1071,12 +1096,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
nodes = {}
node_initialized = False
for item in items:
if isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole") and item.node().initialized() and item.node().status() == Node.started:
node = item.node()
nodes[node.name()] = node
if isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole") and item.node().initialized():
node_initialized = True
if item.node().status() == Node.started:
node = item.node()
nodes[node.name()] = node
if not nodes:
if not nodes and node_initialized:
if len(items) > 1:
QtWidgets.QMessageBox.warning(self, "Console", "At least one node must be started before a console can be opened")
else:
@@ -1128,6 +1156,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)
@@ -1175,12 +1205,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):
@@ -1208,6 +1235,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
@@ -1232,7 +1269,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)
@@ -1266,7 +1303,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"])
@@ -1296,6 +1333,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):
"""
@@ -1305,7 +1344,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)
@@ -1356,7 +1395,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
if item.parentItem() is None:
if horizontal_pos is None:
horizontal_pos = item.y() + item.boundingRect().height() / 2
item.setPos(item.x(), horizontal_pos - item.boundingRect().height() / 2)
item.setX(item.x())
item.setY(horizontal_pos - item.boundingRect().height() / 2)
item.updateNode()
def verticalAlignmentSlot(self):
"""
@@ -1369,7 +1410,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
if item.parentItem() is None:
if vertical_position is None:
vertical_position = item.x() + item.boundingRect().width() / 2
item.setPos(vertical_position - item.boundingRect().width() / 2, item.y())
item.setX(vertical_position - item.boundingRect().width() / 2)
item.setY(item.y())
item.updateNode()
def raiseLayerActionSlot(self):
"""
@@ -1381,6 +1424,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
if item.parentItem() is None:
current_zvalue = item.zValue()
item.setZValue(current_zvalue + 1)
item.updateNode()
item.update()
def lowerLayerActionSlot(self):
@@ -1393,6 +1437,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
if item.parentItem() is None:
current_zvalue = item.zValue()
item.setZValue(current_zvalue - 1)
item.updateNode()
item.update()
if item.zValue() == -1:
@@ -1424,6 +1469,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.
@@ -1434,75 +1482,47 @@ class GraphicsView(QtWidgets.QGraphicsView):
mainwindow = MainWindow.instance()
if "server" in node_data:
return ComputeManager.instance().getCompute(node_data["server"])
try:
return ComputeManager.instance().getCompute(node_data["server"])
except KeyError:
raise ModuleError("Compute {} doesn't exists".format(node_data["server"]))
if "builtin" in node_data:
allow_local_server = True
else:
allow_local_server = module_instance.settings()["use_local_server"]
server = server_select(mainwindow, node_data.get("node_type"), allow_local_server=allow_local_server)
server = server_select(mainwindow, node_data.get("node_type"))
if server is None:
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))
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)
node_module.createNode(node, node_data["name"])
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)
node.error_signal.connect(self._main_window.uiConsoleTextEdit.writeError)
node.error_signal.connect(self._displayNodeErrorSlot)
node.warning_signal.connect(self._main_window.uiConsoleTextEdit.writeWarning)
node.server_error_signal.connect(self._main_window.uiConsoleTextEdit.writeServerError)
node.server_error_signal.connect(self._displayNodeErrorSlot)
return node_item
def _displayNodeErrorSlot(self, node_id, message):
@qslot
def _displayNodeErrorSlot(self, node_id, message, *args):
"""
Show error send by a node to the user
"""
node = Topology.instance().getNode(node_id)
name = "Node"
if node:
if node.name():
name = node.name()
QtWidgets.QMessageBox.critical(self._main_window, name, message.strip())
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())
def createDrawingItem(self, type, x, y, z, rotation=0, svg=None, drawing_id=None):
@@ -1510,6 +1530,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":
@@ -1541,3 +1563,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

@@ -15,18 +15,21 @@
# 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 sip
import json
import copy
import ipaddress
import http
import uuid
import pathlib
import urllib.request
import base64
import datetime
import ipaddress
import urllib.request
import urllib.parse
from .version import __version__, __version_info__
from .qt import QtCore, QtNetwork, qpartial
from .qt import QtCore, QtNetwork, qpartial, sip_is_deleted, QtWebSockets
from .utils import parse_version
import logging
@@ -48,42 +51,53 @@ 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")
self._host = settings["host"]
if self._host == "0.0.0.0":
self._host = "127.0.0.1"
try:
if self._host is None or self._host == "0.0.0.0":
self._host = "127.0.0.1"
elif ":" in self._host and ipaddress.IPv6Address(self._host) and str(ipaddress.IPv6Address(self._host)) == "::":
self._host = "::1"
except ipaddress.AddressValueError:
log.error("Invalid host name %s", self._host)
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)
# In order to detect computer hibernation we detect the date of the last
# 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:
self._network_manager = QtNetwork.QNetworkAccessManager()
# A buffer used by progress download
self._buffer = {}
# 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
def host(self):
"""
Host display to user
@@ -125,15 +139,20 @@ class HTTPClient(QtCore.QObject):
def url(self):
"""Returns current server url"""
if ":" in self.host():
return "{}://[{}]:{}".format(self.protocol(), self.host(), self.port())
return "{}://{}:{}".format(self.protocol(), self.host(), self.port())
def fullUrl(self):
"""Returns current server url including user and password"""
host = self.host()
if ":" in self.host():
host = "[{}]".format(host)
if self._user:
return "{}://{}:{}@{}:{}".format(self.protocol(), self._user, self._password, self.host(), self.port())
return "{}://{}:{}@{}:{}".format(self.protocol(), self._user, self._password, host, self.port())
else:
return "{}://{}:{}".format(self.protocol(), self.host(), self.port())
return "{}://{}:{}".format(self.protocol(), host, self.port())
def password(self):
return self._password
@@ -148,11 +167,30 @@ 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
"""
if HTTPClient._progress_callback:
if not sip_is_deleted(HTTPClient._progress_callback):
if progress_text:
HTTPClient._progress_callback.add_query_signal.emit(query_id, progress_text, response)
else:
@@ -163,22 +201,24 @@ class HTTPClient(QtCore.QObject):
Called when a query is over
"""
if HTTPClient._progress_callback:
if not sip_is_deleted(HTTPClient._progress_callback):
HTTPClient._progress_callback.remove_query_signal.emit(query_id)
def _notify_progress_upload(self, query_id, sent, total):
"""
Called when a query upload progress
"""
if HTTPClient._progress_callback:
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
if not sip_is_deleted(HTTPClient._progress_callback):
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(total))
def _notify_progress_download(self, query_id, sent, total):
"""
Called when a query download progress
"""
if HTTPClient._progress_callback:
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
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, str(sent), str(abs(total)))
@classmethod
def setProgressCallback(cls, progress_callback):
@@ -201,6 +241,7 @@ class HTTPClient(QtCore.QObject):
Closes the connection with the server.
"""
self._connected = False
self._progress_callback.reset()
def _request(self, url):
"""
@@ -221,7 +262,18 @@ class HTTPClient(QtCore.QObject):
:param query: The Server to connect
"""
def createHTTPQuery(self, method, path, callback, body={}, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, timeout=120, server=None, prefix="/v2", params={}, **kwargs):
def createHTTPQuery(self, method, path, callback, body={}, context={},
downloadProgressCallback=None,
showProgress=True,
ignoreErrors=False,
progressText=None,
timeout=120,
server=None,
prefix="/v2",
params={},
networkManager=None,
eventsHandler=None,
**kwargs):
"""
Call the remote server, if not connected, check connection before
@@ -237,24 +289,51 @@ class HTTPClient(QtCore.QObject):
:param server: The server where the query will run
:param timeout: Delay in seconds before raising a timeout
:param prefix: Prefix to the path
:param networkManager: QNetworkAccessManager None use the default
:param eventsHandler: Handler receiving and triggering events like `updated`, `cancelled`.
If not specified and showProgress is `True` then `ProgressDialog` receives them.
:param params: Query arguments parameters
:returns: QNetworkReply
"""
if "dev" in __version__:
assert QtCore.QThread.currentThread() == self.thread(), "HTTP request not started from the main thread"
# Shutdown in progress do not execute the query
if self._shutdown:
return
request = qpartial(self._executeHTTPQuery, method, path, qpartial(callback), body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText, server=server, timeout=timeout, prefix=prefix, params=params)
# We try to detect computer hibernation
# if time between two query is too long we trigger a disconnect
if self._max_time_difference_between_queries:
now = datetime.datetime.now().timestamp()
if self._last_query_timestamp is not None and now > self._last_query_timestamp + self._max_time_difference_between_queries:
log.warning("Synchronisation lost with the server.")
self.disconnect()
self._last_query_timestamp = None
return
self._last_query_timestamp = now
request = qpartial(self._executeHTTPQuery, method, path, qpartial(callback), body, context,
downloadProgressCallback=downloadProgressCallback,
showProgress=showProgress,
ignoreErrors=ignoreErrors,
progressText=progressText,
networkManager=networkManager,
server=server,
timeout=timeout,
prefix=prefix,
eventsHandler=eventsHandler,
params=params)
if self._connected:
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)
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):
"""
@@ -268,10 +347,10 @@ class HTTPClient(QtCore.QObject):
if len(msg) > 0:
msg = "Cannot connect to server {}: {}".format(self.url(), msg)
else:
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall.".format(self.url())
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):
@@ -291,7 +370,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:
@@ -300,7 +379,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())
@@ -311,22 +390,23 @@ class HTTPClient(QtCore.QObject):
self._query_waiting_connections = []
return
if params["version"] != __version__:
msg = "Client version {} differs with server version {}".format(__version__, params["version"])
log.error(msg)
if params["version"].split("-")[0] != __version__.split("-")[0]:
msg = "Client version {} is not the same as server version {}".format(__version__, params["version"])
# Stable release
if __version_info__[3] == 0:
log.error(msg)
for request, callback in self._query_waiting_connections:
if callback is not None:
callback({"message": msg}, error=True, server=server)
return
# We don't allow different major version to interact even with dev build
elif parse_version(__version__)[:2] != parse_version(params["version"])[:2]:
log.error(msg)
for request, callback in self._query_waiting_connections:
if callback is not None:
callback({"message": msg}, error=True, server=server)
return
log.warning("Use a different client and server version can create bugs. Use it at your own risk.")
log.warning("{}\nUsing different versions may result in unexpected problems. Please use at your own risk.".format(msg))
self._connected = True
self._retry = 0
@@ -385,7 +465,47 @@ class HTTPClient(QtCore.QObject):
request.setRawHeader(b"Authorization", auth_string.encode())
return request
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, **kwargs):
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, eventsHandler=None, **kwargs):
"""
Call the remote server
@@ -396,30 +516,19 @@ class HTTPClient(QtCore.QObject):
:param context: Pass a context to the response callback
:param downloadProgressCallback: Callback called when received something, it can be an incomplete response
:param showProgress: Display progress to the user
:param networkManager: The network manager to use. If None use default
:param progressText: Text display to user in progress dialog. None for auto generated
:param ignoreErrors: Ignore connection error (usefull to not closing a connection when notification feed is broken)
:param server: The server where the query is executed
:param timeout: Delay in seconds before raising a timeout
:param eventsHandler: Handler receiving and triggering events like `updated`, `cancelled`.
If not specified and showProgress is `True` then `ProgressDialog` receives them.
:param params: Query arguments parameters
:returns: QNetworkReply
"""
# TODO: remove it when all call are migrated
if "compute/" in path:
log.warning("Legacy compute direct call %s", path)
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:
@@ -435,18 +544,29 @@ class HTTPClient(QtCore.QObject):
# By default QT doesn't support GET with body even if it's in the RFC that's why we need to use sendCustomRequest
body = self._addBodyToRequest(body, request)
response = self._network_manager.sendCustomRequest(request, method.encode(), body)
if not networkManager:
networkManager = self._network_manager
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())
response.finished.connect(qpartial(self._processResponse, response, server, callback, context, body, ignoreErrors))
response.error.connect(qpartial(self._processError, response, server, callback, context, body, ignoreErrors))
if downloadProgressCallback is not None:
response.readyRead.connect(qpartial(self._readyReadySlot, response, downloadProgressCallback, context, server))
if HTTPClient._progress_callback and HTTPClient._progress_callback.progress_dialog():
request_canceled = qpartial(self._requestCanceled, response, context)
request_canceled = qpartial(self._requestCanceled, response, context)
if eventsHandler is not None:
eventsHandler.canceled.connect(request_canceled)
elif not sip_is_deleted(HTTPClient._progress_callback) and HTTPClient._progress_callback.progress_dialog():
HTTPClient._progress_callback.progress_dialog().canceled.connect(request_canceled)
if showProgress:
@@ -457,7 +577,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
@@ -492,45 +612,48 @@ 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 len(response.rawHeaderList()) > 0:
response.abort()
if not sip.isdeleted(response) and response.isRunning() and not len(response.rawHeaderList()) > 0:
if not response.error() != QtNetwork.QNetworkReply.NoError:
log.warning("Timeout after {} seconds for request {}".format(timeout, response.url().toString()))
response.abort()
def disconnect(self):
"""
Disconnect from the remote server
"""
self.connection_disconnected_signal.emit()
self.close()
def _requestCanceled(self, response, context):
if response.isRunning():
log.warn("Aborting request for {}".format(response.url()))
if response.isRunning() and not response.error() != QtNetwork.QNetworkReply.NoError:
log.warn("Aborting request for {}".format(response.url().toString()))
response.abort()
if "query_id" in context:
self._notify_progress_end_query(context["query_id"])
def _processResponse(self, response, server, callback, context, request_body, ignore_errors):
if request_body is not None:
request_body.close()
status = None
body = None
if "query_id" in context:
self._notify_progress_end_query(context["query_id"])
if response.error() != QtNetwork.QNetworkReply.NoError:
error_code = response.error()
def _processError(self, response, server, callback, context, request_body, ignore_errors, error_code):
if error_code != QtNetwork.QNetworkReply.NoError:
error_message = response.errorString()
if not ignore_errors:
log.debug("Response error: %s (error: %d)", error_message, error_code)
log.debug("Response error: %s for %s (error: %d)", error_message, response.url().toString(), error_code)
if error_code < 200:
if not ignore_errors:
self.close()
if callback is not None:
callback({"message": error_message}, error=True, server=server, context=context)
if "query_id" in context:
self._notify_progress_end_query(context["query_id"])
if error_code < 200 or error_code == 403:
if error_code == QtNetwork.QNetworkReply.OperationCanceledError: # It's legit to cancel do not disconnect
error_message = "Operation timeout" # It's more clear than cancel, because cancel is trigger by us when we timeout
elif not ignore_errors:
self.disconnect()
if callback is not None:
callback({"message": error_message}, error=True, server=server, context=context)
return
else:
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
@@ -547,7 +670,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:
@@ -559,7 +682,15 @@ class HTTPClient(QtCore.QObject):
log.error(json.loads(body)["message"])
except (ValueError, KeyError):
log.error(error_message)
else:
def _processResponse(self, response, server, callback, context, request_body, ignore_errors):
if request_body is not None:
request_body.close()
if "query_id" in context:
self._notify_progress_end_query(context["query_id"])
if response.error() == QtNetwork.QNetworkReply.NoError:
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
log.debug("Decoding response from {} response {}".format(response.url().toString(), status))
try:
@@ -569,9 +700,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:
@@ -579,16 +713,15 @@ class HTTPClient(QtCore.QObject):
callback(params, error=True, server=server, context=context)
else:
callback(params, server=server, context=context, raw_body=raw_body)
# response.deleteLater()
if status == 400:
try:
params = json.loads(body)
e = HttpBadRequest(body)
e.fingerprint = params["path"]
# If something goes wrong for a any reason just raise the bad request
except Exception:
e = HttpBadRequest(body)
raise e
if status == 400:
try:
params = json.loads(body)
e = HttpBadRequest(body)
e.fingerprint = params["path"]
# If something goes wrong for a any reason just raise the bad request
except Exception:
e = HttpBadRequest(body)
raise e
def getSynchronous(self, endpoint, timeout=2):
"""
@@ -610,7 +743,6 @@ class HTTPClient(QtCore.QObject):
urllib.request.install_opener(opener)
else:
log.debug("Synchronous get {} (no authentication)".format(url))
response = urllib.request.urlopen(url, timeout=timeout)
content_type = response.getheader("CONTENT-TYPE")
if response.status == 200:
@@ -632,3 +764,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

@@ -18,7 +18,6 @@
import os
import copy
import pathlib
import glob
from gns3.qt import QtWidgets
from gns3.local_server_config import LocalServerConfig
@@ -26,6 +25,7 @@ from gns3.settings import LOCAL_SERVER_SETTINGS
from gns3.controller import Controller
from gns3.utils.file_copy_worker import FileCopyWorker
from gns3.utils.progress_dialog import ProgressDialog
from gns3.registry.image import Image
class ImageManager:
@@ -34,7 +34,29 @@ class ImageManager:
# Remember if we already ask the user about this image for this server
self._asked_for_this_image = {}
def askCopyUploadImage(self, parent, path, server, node_type):
def _getUniqueDestinationPath(self, source_image, node_type, path):
"""
Get a unique destination path (with counter).
"""
if not os.path.exists(path):
return path
path, extension = os.path.splitext(path)
counter = 1
new_path = "{}-{}{}".format(path, counter, extension)
while os.path.exists(new_path):
destination_image = Image(node_type, new_path, filename=os.path.basename(new_path))
try:
if source_image.md5sum == destination_image.md5sum:
# the source and destination images are identical
return new_path
except OSError:
continue
counter += 1
new_path = "{}-{}{}".format(path, counter, extension)
return new_path
def askCopyUploadImage(self, parent, source_path, server, node_type):
"""
Ask user for copying the image to the default directory or upload
it to remote server.
@@ -46,35 +68,52 @@ class ImageManager:
:returns path: Final path
"""
if server and server != "local":
return self._uploadImageToRemoteServer(path, server, node_type)
if (server and server != "local") or Controller.instance().isRemote():
return self._uploadImageToRemoteServer(source_path, server, node_type)
else:
destination_directory = self.getDirectoryForType(node_type)
if os.path.normpath(os.path.dirname(path)) != destination_directory:
# the IOS image is not in the default images directory
destination_path = os.path.join(destination_directory, os.path.basename(source_path))
source_filename = os.path.basename(source_path)
destination_filename = os.path.basename(destination_path)
if os.path.normpath(os.path.dirname(source_path)) != destination_directory:
# the image is not in the default images directory
if source_filename == destination_filename:
# the filename already exists in the default images directory
source_image = Image(node_type, source_path, filename=source_filename)
destination_image = Image(node_type, destination_path, filename=destination_filename)
try:
if source_image.md5sum == destination_image.md5sum:
# the source and destination images are identical
return source_path
except OSError as e:
QtWidgets.QMessageBox.critical(parent, 'Image', 'Cannot compare image file {} with {}: {}.'.format(source_path, destination_path, str(e)))
return source_path
# find a new unique path to avoid overwriting existing destination file
destination_path = self._getUniqueDestinationPath(source_image, node_type, destination_path)
reply = QtWidgets.QMessageBox.question(parent,
'Image',
'Would you like to copy {} to the default images directory'.format(os.path.basename(path)),
'Would you like to copy {} to the default images directory'.format(source_filename),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
destination_path = os.path.join(destination_directory, os.path.basename(path))
try:
os.makedirs(destination_directory, exist_ok=True)
except OSError as e:
QtWidgets.QMessageBox.critical(parent, 'Image', 'Could not create destination directory {}: {}'.format(destination_directory, str(e)))
return path
worker = FileCopyWorker(path, destination_path)
progress_dialog = ProgressDialog(worker, 'Image', 'Copying {}'.format(os.path.basename(path)), 'Cancel', busy=True, parent=parent)
return source_path
worker = FileCopyWorker(source_path, destination_path)
progress_dialog = ProgressDialog(worker, 'Image', 'Copying {}'.format(source_filename), 'Cancel', busy=True, parent=parent)
progress_dialog.show()
progress_dialog.exec_()
errors = progress_dialog.errors()
if errors:
QtWidgets.QMessageBox.critical(parent, 'Image', '{}'.format(''.join(errors)))
return path
return source_path
else:
path = destination_path
return path
source_path = destination_path
return source_path
def _uploadImageToRemoteServer(self, path, server, node_type):
"""
@@ -96,22 +135,9 @@ class ImageManager:
raise Exception('Invalid node type')
filename = self._getRelativeImagePath(path, node_type).replace("\\", "/")
Controller.instance().post(r'/computes/{}{}/{}'.format(server, upload_endpoint, filename), None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
Controller.instance().postCompute('{}/{}'.format(upload_endpoint, filename), server, None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
return filename
def _askForUploadMissingImage(self, filename, server):
from gns3.main_window import MainWindow
parent = MainWindow.instance()
reply = QtWidgets.QMessageBox.warning(parent,
'Image',
'{} is missing on server {} but exist on your computer. Do you want to upload it?'.format(filename, server.url()),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
return True
return False
def _getRelativeImagePath(self, path, node_type):
"""
Get a path relative to images directory path

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, QtGui, QtWidgets, QtSvg
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
@@ -58,7 +68,8 @@ class DrawingItem:
return self._id
def create(self):
self._project.post("/drawings", self._createDrawingCallback, body=self.__json__())
if self._project:
self._project.post("/drawings", self._createDrawingCallback, body=self.__json__())
def _createDrawingCallback(self, result, error=False, **kwargs):
"""
@@ -77,8 +88,9 @@ 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):
"""
Callback for update.
@@ -153,6 +165,7 @@ class DrawingItem:
"""
QtWidgets.QGraphicsItem.setZValue(self, value)
if self.zValue() < 0:
self.setFlag(self.ItemIsSelectable, False)
self.setFlag(self.ItemIsMovable, False)
@@ -188,6 +201,9 @@ class DrawingItem:
self.updateDrawing()
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
def updateNode(self):
self.updateDrawing()
def drawLayerInfo(self, painter):
"""
Draws the layer position.
@@ -210,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

@@ -36,12 +36,11 @@ class EthernetLinkItem(LinkItem):
:param destination_port: destination Port instance
:param link: Link instance (contains back-end stuff for this link)
:param adding_flag: indicates if this link is being added (no destination yet)
:param multilink: used to draw multiple link between the same source and destination
"""
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag, multilink)
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag)
self._source_collision_offset = 0.0
self._destination_collision_offset = 0.0
@@ -113,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
@@ -143,8 +142,7 @@ class EthernetLinkItem(LinkItem):
if source_port_label is None:
source_port_label = NoteItem(self._source_item)
source_port_name = self._source_port.name()
source_port_label.setPlainText(source_port_name)
source_port_label.setPlainText(self._source_port.shortName())
source_port_label.setPos(self.mapToItem(self._source_item, point1))
self._source_port.setLabel(source_port_label)
@@ -155,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
@@ -185,8 +183,7 @@ class EthernetLinkItem(LinkItem):
if destination_port_label is None:
destination_port_label = NoteItem(self._destination_item)
destination_port_name = self._destination_port.name()
destination_port_label.setPlainText(destination_port_name)
destination_port_label.setPlainText(self._destination_port.shortName())
destination_port_label.setPos(self.mapToItem(self._destination_item, point2))
self._destination_port.setLabel(destination_port_label)
@@ -197,4 +194,4 @@ class EthernetLinkItem(LinkItem):
painter.drawPoint(point2)
self._drawCaptureSymbol()
self._drawSymbol()

View File

@@ -19,9 +19,7 @@
Graphical representation of an image on the QGraphicsScene.
"""
import xml.etree.ElementTree as ET
from ..qt import QtWidgets, QtCore, QtSvg
from ..qt import QtSvg
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from .drawing_item import DrawingItem
@@ -32,8 +30,7 @@ class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
Class to insert an image on the scene.
"""
def __init__(self, image_path=None, pos=None, svg=None, **kws):
def __init__(self, image_path=None, pos=None, svg=None, **kws):
self._image_path = image_path
# Because we call the Qt C++ code we need to handle the case of pos is None otherwise we will get a conversion error
@@ -42,7 +39,6 @@ class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
else:
super().__init__(**kws)
if self._image_path:
renderer = QImageSvgRenderer(image_path)
self.setSharedRenderer(renderer)
@@ -68,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)
@@ -77,4 +80,3 @@ class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
Return an SVG version of the shape
"""
return self.renderer().svg()

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

@@ -21,13 +21,13 @@ Link items are graphical representation of a link on the QGraphicsScene
"""
import math
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
from ..node import Node
from ..packet_capture import PacketCapture
from ..dialogs.filter_dialog import FilterDialog
class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
class SvgIconItem(QtSvg.QGraphicsSvgItem):
def __init__(self, symbol, parent):
@@ -50,12 +50,12 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
:param destination_port: destination Port instance
:param link: Link instance (contains back-end stuff for this link)
:param adding_flag: indicates if this link is being added (no destination yet)
:param multilink: used to draw multiple link between the same source and destination
"""
_draw_port_labels = False
delete_link_item_signal = QtCore.pyqtSignal(str)
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
super().__init__()
self.setAcceptHoverEvents(True)
@@ -76,10 +76,6 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
# default pen size
self._pen_width = 2.0
# indicates the link position when there are multiple links
# between the same source and destination
self._multilink = multilink
# source & destination items and ports
self._source_item = source_item
self._destination_item = destination_item
@@ -91,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)
@@ -108,8 +110,10 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self.adjust()
def _linkDeletedSlot(self, link_id):
@qslot
def _linkDeletedSlot(self, link_id, *args):
# first delete the port labels if any
if self._source_port.label():
self._source_port.label().setParentItem(None)
self.scene().removeItem(self._source_port.label())
@@ -117,11 +121,19 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self._destination_port.label().setParentItem(None)
self.scene().removeItem(self._destination_port.label())
self._source_item.removeLink(self)
self._destination_item.removeLink(self)
if self.scene():
if self in self.scene().items():
self.scene().removeItem(self)
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):
"""
@@ -233,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.
@@ -349,6 +381,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self.setHovered(False)
@qslot
def adjust(self):
"""
Computes the source point and destination point.
@@ -376,15 +409,54 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
# compute the length of the line
self.length = math.sqrt(self.dx * self.dx + self.dy * self.dy)
multilink = self._computeMultiLink()
# multi-link management
if not self._adding_flag and self._multilink and self.length:
if not self._adding_flag and multilink and self.length:
angle = math.radians(90)
self.dxrot = math.cos(angle) * self.dx - math.sin(angle) * self.dy
self.dyrot = math.sin(angle) * self.dx + math.cos(angle) * self.dy
offset = QtCore.QPointF((self.dxrot * (self._multilink * 5)) / self.length, (self.dyrot * (self._multilink * 5)) / self.length)
offset = QtCore.QPointF((self.dxrot * (multilink * 5)) / self.length, (self.dyrot * (multilink * 5)) / self.length)
self.source = QtCore.QPointF(self.source + offset)
self.destination = QtCore.QPointF(self.destination + offset)
def _computeMultiLink(self):
# Multi-link management
#
# multi is the offset of the link
# +------+ multi = -1 Link 2 +-------+
# | +-----------------------------+ |
# | R1 | | R2 |
# | | multi = 0 Link 1 | |
# | +-----------------------------+ |
# | | multi = 1 Link 3 | |
# +------+-----------------------------+-------+
if self._source_item == self._destination_item:
multi = 0
elif not hasattr(self._destination_item, "node"): # Could be temporary a qpointf during link creation
multi = 0
else:
multi = 0
link_items = self._source_item.links()
for link_item in link_items:
if link_item == self:
break
if link_item.destinationItem().node().id() == self._destination_item.node().id():
multi += 1
if link_item.sourceItem().node().id() == self._destination_item.node().id():
multi += 1
# MAX 7 links on the scene between 2 nodes
if multi > 7:
multi = 0
# Pair item represent the bottom links
elif multi % 2 == 0:
multi = multi // 2
else:
multi = -multi // 2
return multi
def setMousePoint(self, scene_point):
"""
Sets new mouse point coordinates.
@@ -397,19 +469,91 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self.adjust()
self.update()
def _drawCaptureSymbol(self):
@qslot
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

@@ -19,7 +19,9 @@
Graphical representation of a node on the QGraphicsScene.
"""
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
import sip
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from .note_item import NoteItem
from ..symbol import Symbol
@@ -39,6 +41,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
"""
show_layer = False
GRID_SIZE = 75
def __init__(self, node):
super().__init__()
@@ -56,9 +59,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
# node label
self._node_label = None
# Temporary symbol during loading
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
self.setZValue(self._node.z())
# Temporary symbol during loading
renderer = QImageSvgRenderer(":/icons/reload.svg")
renderer.setObjectName("symbol_loading")
self.setSharedRenderer(renderer)
@@ -96,18 +100,27 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
from ..main_window import MainWindow
self._main_window = MainWindow.instance()
if self._main_window.uiSnapToGridAction.isChecked():
self._snapToGrid()
self._settings = self._main_window.uiGraphicsView.settings()
if node.initialized():
self.createdSlot(node.id())
def _updateNode(self):
def _snapToGrid(self):
mid_x = self.boundingRect().width() / 2
x = (self.GRID_SIZE * round((self.x() + mid_x) / self.GRID_SIZE)) - mid_x
mid_y = self.boundingRect().height() / 2
y = (self.GRID_SIZE * round((self.y() + mid_y) / self.GRID_SIZE)) - mid_y
self.setPos(x, y)
def updateNode(self):
"""
Sync change to the node
"""
if self._initialized:
self._node.setGraphics(self)
self._node.setGraphics(self)
@qslot
def setSymbol(self, symbol):
"""
:param symbol: Change the symbol path
@@ -128,16 +141,17 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
def symbol(self):
return self._symbol
def _symbolLoadedCallback(self, path):
renderer = QImageSvgRenderer(path)
@qslot
def _symbolLoadedCallback(self, path, *args):
renderer = QImageSvgRenderer(path, fallback=":/icons/cancel.svg")
renderer.setObjectName(path)
self.setSharedRenderer(renderer)
if self._node.settings().get("symbol") != self._symbol:
self._updateNode()
self.updateNode()
if not self._initialized:
self._showLabel()
self._initialized = True
self._updateNode()
self.updateNode()
def node(self):
"""
@@ -153,25 +167,39 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self._node.setSettingValue("x", int(self.x()))
self._node.setSettingValue("y", int(self.y()))
def addLink(self, link):
@qslot
def addLink(self, link_item, *args):
"""
Adds a link items to this node item.
:param link: LinkItem instance
"""
self._links.append(link)
if not sip.isdeleted(link_item):
self._links.append(link_item)
link_item.link().delete_link_signal.connect(self._removeLink)
link_item.link().updated_link_signal.connect(self._linkUpdatedSlot)
self._node.updated_signal.emit()
@qslot
def _linkUpdatedSlot(self, *args):
"""
When a link change we also notify the listener of the node
"""
self._node.updated_signal.emit()
def removeLink(self, link):
@qslot
def _removeLink(self, link_id, *args):
"""
Removes a link items from this node item.
:param link: LinkItem instance
"""
if link in self._links:
self._links.remove(link)
for link_item in self._links:
if link_item.link().id() == link_id:
self._links.remove(link_item)
return
def links(self):
"""
@@ -182,7 +210,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
return self._links
def createdSlot(self, base_node_id):
@qslot
def createdSlot(self, base_node_id, *args):
"""
Slot to receive events from the attached Node instance
when a the node has been created/initialized.
@@ -190,54 +219,47 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
:param base_node_id: base node identifier (integer)
"""
if self is None:
return
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
self.setSymbol(self._node.symbol())
self.update()
def startedSlot(self):
@qslot
def startedSlot(self, *args):
"""
Slot to receive events from the attached Node instance
when a the node has started.
"""
if self is None:
return
for link in self._links:
link.update()
def stoppedSlot(self):
@qslot
def stoppedSlot(self, *args):
"""
Slot to receive events from the attached Node instance
when a the node has stopped.
"""
if self is None:
return
for link in self._links:
link.update()
def suspendedSlot(self):
@qslot
def suspendedSlot(self, *args):
"""
Slot to receive events from the attached Node instance
when a the node has suspended.
"""
if self is None:
return
for link in self._links:
link.update()
def updatedSlot(self):
@qslot
def updatedSlot(self, *args):
"""
Slot to receive events from the attached Node instance
when a the node has been updated.
"""
if self is None:
return
self.setSymbol(self._node.settings().get("symbol"))
self.setPos(self._node.settings().get("x", 0), self._node.settings().get("y", 0))
self.setZValue(self._node.settings().get("z", 0))
@@ -249,18 +271,20 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
for link in self._links:
link.setCustomToolTip()
def deletedSlot(self):
@qslot
def deletedSlot(self, *args):
"""
Slot to receive events from the attached Node instance
when the node has been deleted.
"""
if self is None or not self.scene():
if not self.scene():
return
if self in self.scene().items():
self.scene().removeItem(self)
def serverErrorSlot(self, base_node_id, message):
@qslot
def serverErrorSlot(self, base_node_id, message, *args):
"""
Slot to receive events from the attached Node instance
when the node has received an error from the server.
@@ -269,10 +293,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
:param message: error message
"""
if self:
self._last_error = "{message}".format(message=message)
self._last_error = "{message}".format(message=message)
def errorSlot(self, base_node_id, message):
@qslot
def errorSlot(self, base_node_id, message, *args):
"""
Slot to receive events from the attached Node instance
when the node wants to report an error.
@@ -281,8 +305,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
:param message: error message
"""
if self:
self._last_error = "{message}".format(message=message)
self._last_error = "{message}".format(message=message)
def setCustomToolTip(self):
"""
@@ -311,7 +334,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
"""
Called when user unselect the label
"""
self._updateNode()
self.updateNode()
def _centerLabel(self):
"""
@@ -350,11 +373,11 @@ 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()
self.updateNode()
else:
self._node_label.setPos(label_data["x"], label_data["y"])
@@ -433,14 +456,11 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
:param value: value of the change
"""
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
GRID_SIZE = 75
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
mid_x = self.boundingRect().width() / 2
tmp_x = (GRID_SIZE * round((self.x() + mid_x) / GRID_SIZE)) - mid_x
value.setX((self.GRID_SIZE * round((value.x() + mid_x) / self.GRID_SIZE)) - mid_x)
mid_y = self.boundingRect().height() / 2
tmp_y = (GRID_SIZE * round((self.y() + mid_y) / GRID_SIZE)) - mid_y
if tmp_x != self.x() and tmp_y != self.y():
self.setPos(tmp_x, tmp_y)
value.setY((self.GRID_SIZE * round((value.y() + mid_y) / self.GRID_SIZE)) - mid_y)
# dynamically change the renderer when this node item is selected/unselected.
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
@@ -448,7 +468,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self.graphicsEffect().setEnabled(True)
else:
self.graphicsEffect().setEnabled(False)
self._updateNode()
self.updateNode()
# adjust link item positions when this node is moving or has changed.
if change == QtWidgets.QGraphicsItem.ItemPositionChange or change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
@@ -508,7 +528,6 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self._node_label.setFlag(self.ItemIsMovable, True)
for link in self._links:
link.adjust()
self._node.setSettingValue("z", int(value))
def hoverEnterEvent(self, event):
"""
@@ -537,4 +556,4 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
It the item is select but mouse is not on it the event
is send also
"""
self._updateNode()
self.updateNode()

View File

@@ -201,13 +201,17 @@ 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":
font.setItalic(True)
elif key == "font-weight" and val == "bold":
font.setBold(True)
elif key == "text-decoration" and val == "underline":
font.setUnderline(True)
elif key == "text-decoration" and val == "line-through":
font.setStrikeOut(True)
elif key == "fill":
new_color = colorFromSvg(val)
color = self.defaultTextColor()
@@ -248,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;"
@@ -256,6 +260,11 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
if self.font().bold():
style += "font-weight: bold;"
if self.font().strikeOut():
style += "text-decoration: line-through;"
elif self.font().underline():
style += "text-decoration: underline;"
style += "fill: {};".format("#" + hex(self.defaultTextColor().rgba())[4:])
style += "fill-opacity: {};".format(self.defaultTextColor().alphaF())

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

@@ -37,12 +37,11 @@ class SerialLinkItem(LinkItem):
:param destination_port: destination Port instance
:param link: Link instance (contains back-end stuff for this link)
:param adding_flag: indicates if this link is being added (no destination yet)
:param multilink: used to draw multiple link between the same source and destination
"""
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag, multilink)
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag)
def adjust(self):
"""
@@ -115,25 +114,24 @@ 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:
source_port_label = NoteItem(self._source_item)
source_port_name = self._source_port.name()
source_port_label.setPlainText(source_port_name)
source_port_label.setPlainText(self._source_port.shortName())
source_port_label.setPos(self.mapToItem(self._source_item, self.source))
self._source_port.setLabel(source_port_label)
@@ -145,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
@@ -163,8 +161,7 @@ class SerialLinkItem(LinkItem):
if destination_port_label is None:
destination_port_label = NoteItem(self._destination_item)
destination_port_name = self._destination_port.name()
destination_port_label.setPlainText(destination_port_name)
destination_port_label.setPlainText(self._destination_port.shortName())
destination_port_label.setPos(self.mapToItem(self._destination_item, self.destination))
self._destination_port.setLabel(destination_port_label)
@@ -175,4 +172,4 @@ class SerialLinkItem(LinkItem):
painter.drawPoint(self.destination_point)
self._drawCaptureSymbol()
self._drawSymbol()

View File

@@ -20,7 +20,7 @@ Base class for shape items (Rectangle, ellipse etc.).
"""
import xml.etree.ElementTree as ET
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
from ..qt import QtCore, QtGui, QtWidgets
from .drawing_item import DrawingItem
from .utils import colorFromSvg
@@ -30,17 +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.
"""
@@ -181,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
@@ -215,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()
@@ -231,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

@@ -26,10 +26,15 @@ from .drawing_item import DrawingItem
from .utils import colorFromSvg
import logging
log = logging.getLogger(__name__)
class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
"""
Text item for the QGraphicsView.
"""
def __init__(self, svg=None, **kws):
super().__init__(**kws)
@@ -44,7 +49,10 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
self.setFont(qt_font)
if svg:
svg = self.fromSvg(svg)
try:
svg = self.fromSvg(svg)
except ET.ParseError as e:
log.warning(str(e))
if self._id is None:
self.create()
@@ -103,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
@@ -113,11 +128,15 @@ 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():
text.set("font-weight", "bold")
if self.font().strikeOut():
text.set("text-decoration", "line-through")
elif self.font().underline():
text.set("text-decoration", "underline")
text.set("fill", "#" + hex(self.defaultTextColor().rgba())[4:])
text.set("fill-opacity", str(self.defaultTextColor().alphaF()))
text.text = self.toPlainText()
@@ -126,7 +145,19 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
return svg
def fromSvg(self, svg):
svg = ET.fromstring(svg)
# sometimes we receive \0 at the end of string inside <svg> element
try:
svg = svg.replace("\u0000", "")
except AttributeError:
pass
try:
svg = ET.fromstring(svg)
except ET.ParseError:
self.setPlainText("Unable to parse `text_item`")
return
text = svg[0]
font = QtGui.QFont()
@@ -145,12 +176,16 @@ 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)
if text.get("font-weight") == "bold":
font.setBold(True)
if text.get("text-decoration") == "underline":
font.setUnderline(True)
if text.get("text-decoration") == "line-through":
font.setStrikeOut(True)
self.setFont(font)
self.setPlainText(text.text)

View File

@@ -23,7 +23,9 @@ def colorFromSvg(value):
Transform a color coming from a SVG file to a Qcolor
"""
value = value.strip('#')
if len(value) == 6: # If alpha channel is missing
if value == "":
value = "000000"
if len(value) == 6: # If alpha channel is missing
value = "ff" + value
value = int(value, base=16)
return QtGui.QColor.fromRgba(value)

View File

@@ -21,8 +21,8 @@ Manages and stores everything needed for a connection between 2 devices.
import os
import re
import sip
import uuid
import tempfile
from .qt import QtCore, QtWidgets
from .controller import Controller
@@ -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
@@ -77,11 +77,14 @@ class Link(QtCore.QObject):
self._link_id = link_id
self._capturing = False
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 = []
@@ -102,28 +105,44 @@ class Link(QtCore.QObject):
self._capturing = result.get("capturing", False)
# If the controller is remote the capture path should be rewrite to something local
if Controller.instance().isRemote():
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
(handle, self._capture_file_path) = tempfile.mkstemp()
Controller.instance().get(
"/projects/{project_id}/links/{link_id}/pcap".format(
project_id=self.project().id(),
link_id=self._link_id),
None,
showProgress=False,
downloadProgressCallback=self._downloadPcapProgress,
timeout=None)
else:
self._capture_file_path = result["capture_file_path"]
if self._capturing:
if Controller.instance().isRemote():
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
self._capture_file = QtCore.QTemporaryFile()
self._capture_file.open(QtCore.QFile.WriteOnly)
self._capture_file.setAutoRemove(True)
self._capture_file_path = self._capture_file.fileName()
Controller.instance().get(
"/projects/{project_id}/links/{link_id}/pcap".format(
project_id=self.project().id(),
link_id=self._link_id),
None,
showProgress=False,
downloadProgressCallback=self._downloadPcapProgress,
ignoreErrors=True, # If something is wrong avoid disconnect us from server
timeout=None)
else:
self._capture_file_path = result["capture_file_path"]
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
@@ -144,6 +163,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"]))
@@ -160,12 +185,16 @@ class Link(QtCore.QObject):
raise NotImplementedError
def _updateLabel(self, label, label_data):
if not label:
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 = {
@@ -180,7 +209,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()
@@ -191,6 +222,7 @@ class Link(QtCore.QObject):
def _linkCreatedCallback(self, result, error=False, **kwargs):
if error:
QtWidgets.QMessageBox.warning(None, "Create link", "Error while creating link: {}".format(result["message"]))
self.deleteLink(skip_controller=True)
return
self._initialized = True
@@ -237,10 +269,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):
"""
@@ -258,10 +298,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({})
@@ -311,20 +351,19 @@ class Link(QtCore.QObject):
"""
if not self._capture_file_path:
return
try:
with open(self._capture_file_path, 'ab') as f:
f.write(content)
except OSError as e:
log.error("Can't write file {}: {}".format(self._capture_file_path, e), True)
return
self._capture_file.write(content)
self._capture_file.flush()
def stopCapture(self):
if Controller.instance().isRemote():
if self._capture_file_path:
if self._capture_file:
self._capture_file.close()
self._capture_file = None
if self._capture_file_path and os.path.exists(self._capture_file_path):
try:
os.remove(self._capture_file_path)
except OSError as e:
log.error("Can't remove file {}".format(self._capture_file_path))
log.error("Can't remove file {}: {}".format(self._capture_file_path, e))
self._capture_file_path = None
Controller.instance().post(
"/projects/{project_id}/links/{link_id}/stop_capture".format(
@@ -404,3 +443,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

@@ -23,7 +23,7 @@ import copy
import psutil
from .qt import QtCore, QtWidgets
from .qt import QtCore, QtWidgets, qslot
from .version import __version__
from .utils import parse_version
from .controller import Controller
@@ -39,6 +39,8 @@ class LocalConfig(QtCore.QObject):
"""
config_changed_signal = QtCore.Signal()
# When this signal is emit the config is saved on controller
save_on_controller_signal = QtCore.Signal()
def __init__(self, config_file=None):
"""
@@ -48,8 +50,27 @@ class LocalConfig(QtCore.QObject):
super().__init__()
self._profile = None
self._config_file = config_file
# Security to avoid pushing to the controller settings before
# we get the original settings from controller
self._settings_retrieved_from_controller = False
self._migrateOldConfigPath()
self._resetLoadConfig()
self._monitoring_changes = False
Controller.instance().connected_signal.connect(self.refreshConfigFromController)
self.save_on_controller_signal.connect(self._saveOnController)
def _monitorChanges(self):
"""
Poll the remote server waiting for settings update
"""
if self._monitoring_changes:
return
self._monitoring_changes = True
self._timer = QtCore.QTimer()
self._timer.setInterval(5000)
self._refreshingSettings = False
self._timer.timeout.connect(self.refreshConfigFromController)
self._timer.start()
def _resetLoadConfig(self):
"""
@@ -97,8 +118,7 @@ class LocalConfig(QtCore.QObject):
# overwrite system wide settings with user specific ones
self._settings.update(user_settings)
self._migrateOldConfig()
self._writeConfig()
Controller.instance().connected_signal.connect(self.refreshConfigFromController)
self.writeConfig()
def profile(self):
"""
@@ -116,28 +136,36 @@ class LocalConfig(QtCore.QObject):
self._config_file = None
self._resetLoadConfig()
@qslot
def refreshConfigFromController(self):
"""
Refresh the configuration from the controller
"""
controller = Controller.instance()
if controller.connected():
controller.get("/settings", self._getSettingsCallback)
self._refreshingSettings = True
controller.get("/settings", self._getSettingsCallback, showProgress=False)
self._monitorChanges()
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._saveOnController()
self._settings_retrieved_from_controller = True
self.save_on_controller_signal.emit()
return
self._settings.update(result)
# Update already loaded section
for section in self._settings.keys():
if isinstance(self._settings[section], dict):
self.loadSectionSettings(section, self._settings[section])
self.config_changed_signal.emit()
# The server return an uuid to keep track of settings version
if self._settings.get("modification_uuid") != result.get("modification_uuid"):
self._settings.update(result)
# Update already loaded section
for section in self._settings.keys():
if isinstance(self._settings[section], dict):
self.loadSectionSettings(section, self._settings[section])
self.config_changed_signal.emit()
self._settings_retrieved_from_controller = True
def configDirectory(self):
"""
@@ -155,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
@@ -181,8 +216,8 @@ class LocalConfig(QtCore.QObject):
# settings from 1.6.1 with 1.5.1 you will have an error
if "version" in self._settings:
if parse_version(self._settings["version"])[:2] > parse_version(__version__)[:2]:
app = QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
QtWidgets.QMessageBox.critical(None, "Version error", "Your settings are for version {} of GNS3. You cannot use a previous version of GNS3 without risking losing data.".format(self._settings["version"]))
QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
QtWidgets.QMessageBox.critical(None, "Version error", "Your settings are for version {} of GNS3. You cannot use a previous version of GNS3 without risking losing data. If you want to reset delete the settings in {}".format(self._settings["version"], self.configDirectory()))
# Exit immediately not clean but we want to avoid any side effect that could corrupt the file
sys.exit(1)
@@ -212,7 +247,7 @@ class LocalConfig(QtCore.QObject):
from .settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, DEFAULT_TELNET_CONSOLE_COMMAND
if "MainWindow" in self._settings:
if self._settings["MainWindow"]["telnet_console_command"] not in PRECONFIGURED_TELNET_CONSOLE_COMMANDS.values():
if self._settings["MainWindow"].get("telnet_console_command") not in PRECONFIGURED_TELNET_CONSOLE_COMMANDS.values():
self._settings["MainWindow"]["telnet_console_command"] = DEFAULT_TELNET_CONSOLE_COMMAND
# Migrate 1.X to 2.0
@@ -226,12 +261,24 @@ class LocalConfig(QtCore.QObject):
vms.append(vm)
self._settings["Qemu"]["vms"] = vms
# Starting with 2.0.0dev5 IOU licence is stored in the settings
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
if "IOU" in self._settings and "iourc_path" in self._settings["IOU"] and "iourc_content" not in self._settings["IOU"]:
try:
with open(self._settings["IOU"]["iourc_path"], "r", encoding="utf-8") as f:
self._settings["IOU"]["iourc_content"] = f.read().replace("\r\n", "\n")
del self._settings["IOU"]["iourc_path"]
except OSError as e:
log.warning("Can't import IOU licence {}: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
except UnicodeDecodeError as e:
log.warning("Non ascii characters in iourc file {}, please remove them: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
def _readConfig(self, config_path):
"""
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
@@ -247,7 +294,7 @@ class LocalConfig(QtCore.QObject):
return dict()
def _writeConfig(self):
def writeConfig(self):
"""
Write the configuration file.
"""
@@ -258,20 +305,21 @@ 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))
self._saveOnController()
self.save_on_controller_signal.emit()
def _saveOnController(self):
@qslot
def _saveOnController(self, *args):
"""
Save some settings on controller for the transition from
GUI to a central controller. Will be removed later
"""
if Controller.instance().connected():
if Controller.instance().connected() and self._settings_retrieved_from_controller:
# We save only non user specific sections
section_to_save_on_controller = ["Builtin", "Docker", "IOU", "Qemu", "VMware", "VPCS", "VirtualBox", "GraphicsView"]
section_to_save_on_controller = ["Builtin", "Docker", "IOU", "Qemu", "VMware", "VPCS", "VirtualBox", "GraphicsView", "Dynamips"]
controller_settings = {}
for key, val in self._settings.items():
if key in section_to_save_on_controller:
@@ -285,7 +333,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:
@@ -328,7 +376,8 @@ class LocalConfig(QtCore.QObject):
if self._settings != settings:
self._settings.update(settings)
self._writeConfig()
self.writeConfig()
self.config_changed_signal.emit()
def loadSectionSettings(self, section, default_settings):
"""
@@ -362,9 +411,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())))
self._writeConfig()
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):
@@ -380,8 +428,8 @@ 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)
self._writeConfig()
log.debug("Section %s has changed. Saving configuration", section)
self.writeConfig()
else:
log.debug("Section %s has not changed. Skip saving configuration", section)
@@ -393,6 +441,14 @@ class LocalConfig(QtCore.QObject):
from gns3.settings import GENERAL_SETTINGS
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["experimental_features"]
def hdpi(self):
"""
:returns: Boolean. True if hdpi is allowed
"""
from gns3.settings import GENERAL_SETTINGS
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["hdpi"]
def multiProfiles(self):
"""
:returns: Boolean. True if multi_profiles is enabled
@@ -407,6 +463,35 @@ 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)
def showInterfaceLabelsOnNewProject(self):
"""
:returns: Boolean. True if show_interface_labels_on_new_project is enabled
"""
from gns3.settings import GRAPHICS_VIEW_SETTINGS
return self.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS) \
.get("show_interface_labels_on_new_project", False)
def setShowInterfaceLabelsOnNewProject(self, value):
from gns3.settings import GRAPHICS_VIEW_SETTINGS
settings = self.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS)
settings["show_interface_labels_on_new_project"] = value
self.saveSectionSettings("GraphicsView", 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
@@ -81,22 +84,30 @@ class LocalServer(QtCore.QObject):
"""
def __init__(self, parent=None):
# Remember if the server was started by us or not
self._server_started_by_me = False
self._local_server_path = ""
self._local_server_process = None
super().__init__()
self._parent = parent
self._local_server_path = ""
self._local_server_process = None
self._config_directory = LocalConfig.instance().configDirectory()
self._settings = {}
self.localServerSettings()
self._port = self._settings.get("port", 3080)
if not self._settings.get("auto_start", True):
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
if self._settings.get("host") is None:
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
else:
self._http_client = None
self._stopping = False
self._timer = QtCore.QTimer()
self._timer.setInterval(5000)
self._timer.timeout.connect(self._checkLocalServerRunningSlot)
self._timer.start()
def _pid_path(self):
"""
:returns: Path of the PID file
@@ -121,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))
@@ -134,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"):
@@ -149,24 +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?",
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:
@@ -174,12 +184,11 @@ 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?",
"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:
sudo(["chmod", "4755", path])
sudo(["chown", "root", path])
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set root permissions to uBridge {}: {}".format(path, str(e)))
return False
@@ -243,13 +252,25 @@ class LocalServer(QtCore.QObject):
# Settings have changed we need to restart the server
if old_settings != self._settings:
if self._settings["auto_start"]:
self.stopLocalServer(wait=True)
# We restart the local server only if we really need. Auth can be hot change
settings_require_restart = ('host', 'port', 'path')
need_restart = False
for s in settings_require_restart:
if old_settings.get(s) != self._settings.get(s):
need_restart = True
if need_restart:
self.stopLocalServer(wait=True)
self.localServerAutoStartIfRequire()
# If the controller is remote:
else:
self.stopLocalServer(wait=True)
self._http_client = HTTPClient(self._settings)
if self._settings.get("host") is None:
self._http_client = None
else:
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
def shouldLocalServerAutoStart(self):
@@ -260,7 +281,7 @@ class LocalServer(QtCore.QObject):
:returns: boolean
"""
return self._settings["auto_start"]
return self._settings["auto_start"] and self._settings["host"] is not None
def localServerPath(self):
"""
@@ -293,8 +314,13 @@ class LocalServer(QtCore.QObject):
"""
if not self.shouldLocalServerAutoStart():
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
return
if self.isLocalServerRunning() and self._server_started_by_me:
return True
# We check if two gui are not launched at the same time
# to avoid killing the server of the other GUI
if not LocalConfig.isMainGui():
@@ -304,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()
@@ -326,7 +352,7 @@ class LocalServer(QtCore.QObject):
progress_dialog.show()
if not progress_dialog.exec_():
return False
self._server_started_by_me = True
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
@@ -342,7 +368,7 @@ class LocalServer(QtCore.QObject):
if sys.platform.startswith('win'):
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
QtWidgets.QMessageBox.critical(self.parent(), "Error", "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
return
return False
self._port = self._settings["port"]
@@ -350,13 +376,13 @@ class LocalServer(QtCore.QObject):
local_server_path = self.localServerPath()
if not local_server_path:
log.warn("No local server is configured")
return
return False
if not os.path.isfile(local_server_path):
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not find local server {}".format(local_server_path))
return
return False
elif not os.access(local_server_path, os.X_OK):
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "{} is not an executable".format(local_server_path))
return
return False
try:
# check if the local address still exists
@@ -411,6 +437,7 @@ class LocalServer(QtCore.QObject):
Starts the local server process.
"""
self._stopping = False
path = self.localServerPath()
command = '"{executable}" --local'.format(executable=path)
@@ -438,22 +465,32 @@ 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
self._local_server_process = subprocess.Popen(command, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
self._local_server_process = subprocess.Popen(command, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, stderr=subprocess.PIPE)
else:
# use arguments on other platforms
args = shlex.split(command)
self._local_server_process = subprocess.Popen(args)
self._local_server_process = subprocess.Popen(args, stderr=subprocess.PIPE)
except (OSError, subprocess.SubprocessError) as e:
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):
if self._local_server_process and not self._stopping:
if not self.localServerProcessIsRunning():
log.error("Local server process has stopped")
try:
log.error(self._local_server_process.stderr.read().decode())
except (OSError, UnicodeDecodeError):
pass
self._local_server_process = None
def localServerProcessIsRunning(self):
"""
Returns either the local server is running.
@@ -475,10 +512,14 @@ class LocalServer(QtCore.QObject):
:returns: boolean
"""
status, json_data = getSynchronous(self._settings["host"], self._port, "version",
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)
@@ -495,7 +536,8 @@ class LocalServer(QtCore.QObject):
"""
if self.localServerProcessIsRunning():
log.info("Stopping local server (PID={})".format(self._local_server_process.pid))
self._stopping = True
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()
@@ -506,6 +548,7 @@ class LocalServer(QtCore.QObject):
progress_dialog.exec_()
if self._local_server_process.returncode is None:
self._killLocalServer()
self._server_started_by_me = False
def _killLocalServer(self):
# the local server couldn't be stopped with the normal procedure
@@ -516,11 +559,11 @@ class LocalServer(QtCore.QObject):
self._local_server_process.send_signal(signal.SIGINT)
# If the process is already dead we received a permission error
# it's a race condition between the timeout and send signal
except PermissionError:
except (PermissionError, SystemError):
pass
try:
# wait for the server to stop for maximum 2 seconds
self._local_server_process.wait(timeout=2)
# wait for the server to stop for maximum x seconds
self._local_server_process.wait(timeout=60)
except subprocess.TimeoutExpired:
proceed = QtWidgets.QMessageBox.question(self.parent(),
"Local server",
@@ -553,5 +596,6 @@ def main():
local_server.localServerAutoStart()
local_server.stopLocalServer()
if __name__ == '__main__':
main()

View File

@@ -30,21 +30,25 @@ class LocalServerConfig:
Local server configuration.
"""
def __init__(self):
def __init__(self, config_file=None):
appname = "GNS3"
self._config = configparser.RawConfigParser()
if sys.platform.startswith("win"):
filename = "gns3_server.ini"
else:
filename = "gns3_server.conf"
from .local_config import LocalConfig
if sys.platform.startswith("win"):
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
if config_file:
self._config_file = config_file
else:
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
if sys.platform.startswith("win"):
filename = "gns3_server.ini"
else:
filename = "gns3_server.conf"
from .local_config import LocalConfig
if sys.platform.startswith("win"):
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
else:
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
try:
# create the config file if it doesn't exist
@@ -106,6 +110,8 @@ class LocalServerConfig:
settings[name] = self._config[section].getfloat(name, default)
else:
settings[name] = self._config[section].get(name, default)
if settings[name] == "None":
settings[name] = None
# sync with the config file
self.saveSettings(section, settings)

View File

@@ -85,14 +85,33 @@ class ColouredStreamHandler(logging.StreamHandler):
def init_logger(level, logfile, quiet=False):
if sys.platform.startswith("win"):
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {name}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
else:
stream_handler = ColouredStreamHandler(sys.stdout)
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{")
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {name}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{")
logging.basicConfig(level=level, handlers=[stream_handler])
log = logging.getLogger()
log.addHandler(stream_handler)
log_factory = logging.getLogRecordFactory()
def factory(name, level, fn, lno, msg, args, exc_info, func=None, sinfo=None, **kwargs):
"""
Reformat the log message to get something more clean
"""
# When qt message box is display the correct line number is a part of
# the name
if ":" in name:
name, lno = name.split(":")
lno = int(lno)
name = name.replace("gns3.", "")
try:
return log_factory(name, level, fn, lno, msg, args, exc_info, func=func, sinfo=sinfo, **kwargs)
except Exception as e: # To avoid recursion we just print the message if something is wrong when logging
print(msg)
return
logging.setLogRecordFactory(factory)
try:
try:
os.makedirs(os.path.dirname(logfile))

View File

@@ -18,6 +18,7 @@
import sys
import os
import faulthandler
# Try to install updates & restart application if an update is installed
try:
@@ -48,7 +49,7 @@ import signal
import psutil
try:
from gns3.qt import QtCore, QtGui, QtWidgets
from gns3.qt import QtCore, QtWidgets
except ImportError:
raise SystemExit("Can't import Qt modules: Qt and/or PyQt is probably not installed correctly...")
from gns3.main_window import MainWindow
@@ -60,7 +61,6 @@ from gns3.application import Application
from gns3.utils import parse_version
from gns3.dialogs.profile_select import ProfileSelectDialog
import logging
log = logging.getLogger(__name__)
@@ -111,6 +111,9 @@ def main():
Entry point for GNS3 GUI.
"""
# Get Python tracebacks explicitly, on a fault like segfault
faulthandler.enable()
# Sometimes (for example at first launch) the OSX app service launcher add
# an extra argument starting with -psn_. We filter it
if sys.platform.startswith("darwin"):
@@ -120,11 +123,15 @@ def main():
parser.add_argument("project", help="load a GNS3 project (.gns3)", metavar="path", nargs="?")
parser.add_argument("--version", help="show the version", action="version", version=__version__)
parser.add_argument("--debug", help="print out debug messages", action="store_true", default=False)
parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout")
parser.add_argument("--config", help="Configuration file")
parser.add_argument("--profile", help="Settings profile (blank will use default settings files)")
options = parser.parse_args()
exception_file_path = "exceptions.log"
if options.project:
options.project = os.path.abspath(options.project)
if hasattr(sys, "frozen"):
# We add to the path where the OS search executable our binary location starting by GNS3
# packaged binary
@@ -144,7 +151,6 @@ def main():
os.environ["PATH"] = os.pathsep.join(frozen_dirs) + os.pathsep + os.environ.get("PATH", "")
if options.project:
options.project = os.path.abspath(options.project)
os.chdir(frozen_dir)
def exceptionHook(exception, value, tb):
@@ -178,16 +184,12 @@ def main():
# catch exceptions to write them in a file
sys.excepthook = exceptionHook
current_year = datetime.date.today().year
print("GNS3 GUI version {}".format(__version__))
print("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
# we only support Python 3 version >= 3.4
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__))
@@ -222,31 +224,41 @@ def main():
except win32console.error as e:
print("warning: could not allocate console: {}".format(e))
global app
app = Application(sys.argv)
local_config = LocalConfig.instance()
if local_config.multiProfiles():
global app
app = Application(sys.argv, hdpi=local_config.hdpi())
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:
local_config.setConfigFilePath(options.config)
elif options.profile:
local_config.setProfile(options.profile)
profile = options.profile
# save client logging info to a file
logfile = os.path.join(LocalConfig.instance().configDirectory(), "gns3_gui.log")
# on debug enable logging to stdout
if options.debug:
root_logger = init_logger(logging.DEBUG, logfile)
init_logger(logging.DEBUG, logfile)
elif options.quiet:
init_logger(logging.ERROR, logfile)
else:
root_logger = init_logger(logging.INFO, logfile)
init_logger(logging.INFO, logfile)
current_year = datetime.date.today().year
log.info("GNS3 GUI version {}".format(__version__))
log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
log.info("Application started with {}".format("".join(sys.argv)))
# update the exception file path to have it in the same directory as the settings file.
exception_file_path = os.path.join(LocalConfig.instance().configDirectory(), exception_file_path)

View File

@@ -27,7 +27,7 @@ import logging
from .local_config import LocalConfig
from .local_server import LocalServer
from .modules import MODULES
from .qt import QtGui, QtCore, QtWidgets
from .qt import QtGui, QtCore, QtWidgets, qslot
from .controller import Controller
from .node import Node
from .ui.main_window_ui import Ui_MainWindow
@@ -41,11 +41,9 @@ from .dialogs.doctor_dialog import DoctorDialog
from .dialogs.edit_project_dialog import EditProjectDialog
from .dialogs.setup_wizard import SetupWizard
from .settings import GENERAL_SETTINGS
from .utils.progress_dialog import ProgressDialog
from .items.node_item import NodeItem
from .items.link_item import LinkItem
from .items.shape_item import ShapeItem
from .items.image_item import ImageItem
from .topology import Topology
from .http_client import HTTPClient
from .progress import Progress
@@ -53,7 +51,10 @@ 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
from .appliance_manager import ApplianceManager
log = logging.getLogger(__name__)
@@ -69,14 +70,24 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# signal to tell the view if the user is adding a link or not
adding_link_signal = QtCore.pyqtSignal(bool)
# Signal of settings updates
settings_updated_signal = QtCore.Signal()
def __init__(self, parent=None, open_file=None):
"""
:param open_file: Open this file instead of asking for a new project
"""
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
@@ -84,18 +95,18 @@ 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
self._open_project_path = None
self._loadSettings()
self._connections()
self._max_recent_files = 5
self._maxrecent_files = 5
self._project_dialog = None
self._recent_file_actions = []
self._recent_project_actions = []
self.recent_file_actions = []
self.recent_project_actions = []
self._start_time = time.time()
local_config = LocalConfig.instance()
local_config.config_changed_signal.connect(self._localConfigChangedSlot)
@@ -103,6 +114,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._local_config_timer.timeout.connect(local_config.checkConfigChanged)
self._local_config_timer.start(1000) # milliseconds
self._analytics_client = AnalyticsClient()
self._appliance_manager = ApplianceManager()
# restore the geometry and state of the main window.
self.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["geometry"].encode()))
@@ -112,8 +124,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiDocksMenu.addAction(self.uiTopologySummaryDockWidget.toggleViewAction())
self.uiDocksMenu.addAction(self.uiComputeSummaryDockWidget.toggleViewAction())
self.uiDocksMenu.addAction(self.uiConsoleDockWidget.toggleViewAction())
self.uiDocksMenu.addAction(self.uiNodesDockWidget.toggleViewAction())
action = self.uiNodesDockWidget.toggleViewAction()
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)
@@ -124,28 +140,25 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._pictures_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
# add recent file actions to the File menu
for i in range(0, self._max_recent_files):
for i in range(0, self._maxrecent_files):
action = QtWidgets.QAction(self.uiFileMenu)
action.setVisible(False)
action.triggered.connect(self.openRecentFileSlot)
self._recent_file_actions.append(action)
self.uiFileMenu.insertActions(self.uiQuitAction, self._recent_file_actions)
self._recent_file_actions_separator = self.uiFileMenu.insertSeparator(self.uiQuitAction)
self._recent_file_actions_separator.setVisible(False)
self.recent_file_actions.append(action)
self.uiFileMenu.insertActions(self.uiQuitAction, self.recent_file_actions)
self.recent_file_actions_separator = self.uiFileMenu.insertSeparator(self.uiQuitAction)
self.recent_file_actions_separator.setVisible(False)
self.updateRecentFileActions()
# add recent file actions to the File menu
for i in range(0, self._max_recent_files):
action = QtWidgets.QAction(self.uiProjectMenu)
# add recent projects to the File menu
for i in range(0, self._maxrecent_files):
action = QtWidgets.QAction(self.uiFileMenu)
action.setVisible(False)
action.triggered.connect(self.openRecentProjectSlot)
self._recent_project_actions.append(action)
self._recent_project_actions_separator = self.uiProjectMenu.addSeparator()
self._recent_project_actions_separator.setVisible(False)
self.uiProjectMenu.addActions(self._recent_project_actions)
self.updateRecentProjectActions()
self.recent_project_actions.append(action)
self.recent_project_actions_separator = self.uiFileMenu.addSeparator()
self.recent_project_actions_separator.setVisible(False)
self.uiFileMenu.addActions(self.recent_project_actions)
# set the window icon
self.setWindowIcon(QtGui.QIcon(":/images/gns3.ico"))
@@ -215,6 +228,10 @@ 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)
# control menu connections
self.uiStartAllAction.triggered.connect(self._startAllActionSlot)
@@ -232,6 +249,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
@@ -258,6 +276,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# connect the signal to the view
self.adding_link_signal.connect(self.uiGraphicsView.addingLinkSlot)
# connect to the signal when settings change
self.settings_updated_signal.connect(self.settingsChangedSlot)
def _loadSettings(self):
"""
Loads the settings from the persistent settings file.
@@ -290,13 +311,36 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._settings.update(new_settings)
# save the settings
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
self.settings_updated_signal.emit()
def _openWebInterfaceActionSlot(self):
if Controller.instance().connected():
QtGui.QDesktopServices.openUrl(QtCore.QUrl(Controller.instance().httpClient().fullUrl()))
def _showGridActionSlot(self):
"""
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):
"""
@@ -309,6 +353,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_()
@@ -317,7 +366,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
@@ -329,7 +379,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
dialog.show()
dialog.exec_()
def openApplianceActionSlot(self):
# No projects
if Topology.instance().project() is None:
if self._open_file_at_startup:
self.loadPath(self._open_file_at_startup)
self._open_file_at_startup = None
else:
self._newProjectActionSlot()
@qslot
def openApplianceActionSlot(self, *args):
"""
Slot called to open an appliance.
"""
@@ -377,9 +436,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
action = self.sender()
if action:
project_id = action.data()
Topology.instance().createLoadProject({"project_id": project_id})
if action and action.data():
if len(action.data()) == 2:
project_id, project_path = action.data()
Topology.instance().createLoadProject({
"project_path": project_path,
"project_id": project_id})
else:
(project_id, ) = action.data()
Topology.instance().createLoadProject({"project_id": project_id})
def loadPath(self, path):
"""Open a file and close the previous project"""
@@ -398,6 +463,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if path.endswith(".gns3project") or path.endswith(".gns3p"):
# Portable GNS3 project
Topology.instance().importProject(path)
elif path.endswith(".net"):
QtWidgets.QMessageBox.critical(self, "Open project", "Importing legacy project is not supported in 2.0.\nYou must open it using GNS3 1.x in order to convert it or manually run the gns3 converter.")
return
elif path.endswith(".gns3appliance") or path.endswith(".gns3a"):
# GNS3 appliance
@@ -408,18 +476,41 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
return
self._appliance_wizard.show()
self._appliance_wizard.exec_()
else:
elif path.endswith(".gns3"):
if Controller.instance().isRemote():
QtWidgets.QMessageBox.critical(self, "Project", "You can't remote open a .gns3 please use import / export in order to provide to the remote server the full project")
QtWidgets.QMessageBox.critical(self, "Open project", "Cannot open a .gns3 file on a remote server, please use a portable project (.gns3p) instead")
return
Topology.instance().loadProject(path)
else:
Topology.instance().loadProject(path)
else:
try:
extension = path.split('.')[1]
QtWidgets.QMessageBox.critical(self, "File open", "Unsupported file extension {} for {}".format(extension, path))
except IndexError:
QtWidgets.QMessageBox.critical(self, "File open", "Missing file extension for {}".format(path))
def _projectChangedSlot(self):
@qslot
def _projectChangedSlot(self, *args):
"""
Called when a project finish to load
"""
project = Topology.instance().project()
if project is not None and self._project_dialog:
self._project_dialog.reject()
self._project_dialog = None
self._refreshVisibleWidgets()
@qslot
def settingsChangedSlot(self, *args):
"""
Called when settings are updated
"""
# It covers case when project is not set
# and we need to refresh appliance manager
project = Topology.instance().project()
if project is None:
self._appliance_manager.instance().refresh()
def _refreshVisibleWidgets(self):
"""
Refresh widgets that should be visible or not
@@ -503,6 +594,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.
@@ -515,10 +660,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))
@@ -571,6 +717,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):
"""
@@ -579,6 +726,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):
"""
@@ -586,6 +734,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
self.uiGraphicsView.resetTransform()
self._updateZoomSettings()
def _fitInViewActionSlot(self):
"""
@@ -601,11 +750,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):
"""
@@ -622,16 +773,18 @@ 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):
"""
Slot called when starting all the nodes.
"""
project = Topology.instance().project()
if project is not None:
project.start_all_nodes()
@@ -683,7 +836,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot called when connecting to all the nodes using the console.
"""
self.uiGraphicsView.consoleFromItems(self.uiGraphicsView.scene().items())
self.uiGraphicsView.consoleFromAllItems()
def _addNoteActionSlot(self):
"""
@@ -704,7 +857,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):
@@ -721,6 +874,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.
@@ -746,7 +906,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
with Progress.instance().context(min_duration=0):
setup_wizard = SetupWizard(self)
setup_wizard.show()
setup_wizard.exec_()
res = setup_wizard.exec_()
# start and connect to the local server if needed
LocalServer.instance().localServerAutoStartIfRequire()
if res:
self._newApplianceActionSlot()
def _aboutQtActionSlot(self):
"""
@@ -805,8 +969,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):
"""
@@ -881,6 +1044,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.
@@ -903,6 +1070,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
:param event: QCloseEvent
"""
if Topology.instance().project():
reply = QtWidgets.QMessageBox.question(self, "Confirm Exit", "Are you sure you want to exit GNS3?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
event.ignore()
return
progress = Progress.instance()
progress.setAllowCancelQuery(True)
progress.setCancelButtonText("Force quit")
@@ -910,21 +1084,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
log.debug("Close the Main Window")
self._analytics_client.sendScreenView("Main Window", session_start=False)
project = Topology.instance().project()
if not project:
self._finish_application_closing(close_windows=False)
event.accept()
self.uiConsoleTextEdit.closeIO()
elif project.closed() or not project.autoClose():
log.debug("Project is closed killing server and closing main windows")
self._finish_application_closing(close_windows=False)
event.accept()
self.uiConsoleTextEdit.closeIO()
else:
log.debug("Project is not closed asking for project closing")
project.project_closed_signal.connect(self._finish_application_closing)
project.close(local_server_shutdown=True)
event.ignore()
self._finish_application_closing(close_windows=False)
event.accept()
self.uiConsoleTextEdit.closeIO()
def _finish_application_closing(self, close_windows=True):
"""
@@ -972,12 +1134,31 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
reply = QtWidgets.QMessageBox.warning(self, "GNS3", "Another GNS3 GUI is already running. Continue?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
self.close()
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()
@@ -987,24 +1168,22 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._setStyle(self._settings.get("style"))
Controller.instance().connected_signal.connect(self._controllerConnectedSlot)
Controller.instance().project_list_updated_signal.connect(self.updateRecentProjectActions)
# start and connect to the local server if needed
LocalServer.instance().localServerAutoStartIfRequire()
self._analytics_client.sendScreenView("Main Window")
self.uiGraphicsView.setEnabled(False)
# show the setup wizard
if not self._settings["hide_setup_wizard"]:
with Progress.instance().context(min_duration=0):
setup_wizard = SetupWizard(self)
setup_wizard.show()
setup_wizard.exec_()
self._analytics_client.sendScreenView("Main Window")
self.uiGraphicsView.setEnabled(False)
if self._open_file_at_startup:
self.loadPath(self._open_file_at_startup)
self._setupWizardActionSlot()
else:
self._newProjectActionSlot()
# start and connect to the local server if needed
LocalServer.instance().localServerAutoStartIfRequire()
if self._open_file_at_startup:
self.loadPath(self._open_file_at_startup)
self._open_file_at_startup = None
elif Topology.instance().project() is None:
self._newProjectActionSlot()
if self._settings["check_for_update"]:
# automatic check for update every week (604800 seconds)
@@ -1015,28 +1194,36 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._settings["last_check_for_update"] = current_epoch
self.setSettings(self._settings)
def updateRecentProjectsSettings(self, project_id, project_name):
def updateRecentProjectsSettings(self, project_id, project_name, project_path):
"""
Updates the recent project settings.
:param project_id: The ID of the project
:param project_name: The name of the project
:param project_path: The project path
"""
# Projects are stored as a list of project_id:project_name
key = "{}:{}".format(project_id, project_name)
key = "{}:{}:{}".format(project_id, project_name, project_path)
recent_projects = []
for project in self._settings["recent_projects"]:
recent_projects.append(project)
# Because the name can change we compare only the project id
# Because the name can change we compare only the project id and path
for project_key in list(recent_projects):
if project_key.split(":")[0] == project_id:
recent_projects.remove(project_key)
for project_key in list(recent_projects):
try:
if project_key.split(":")[2] == project_path:
recent_projects.remove(project_key)
# 2.0.0 alpha1 compatible
except IndexError:
pass
recent_projects.insert(0, key)
if len(recent_projects) > self._max_recent_files:
if len(recent_projects) > self._maxrecent_files:
recent_projects.pop()
# write the recent file list
@@ -1052,18 +1239,37 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
size = len(self._settings["recent_projects"])
for project in self._settings["recent_projects"]:
# Projects are stored as a list of project_id:project_name
project_id, project_name = project.split(":", maxsplit=1)
action = self._recent_project_actions[index]
action.setText(" {}. {}".format(index + 1, project_name))
action.setData(project_id)
action.setVisible(True)
try:
project_id, project_name, project_path = project.split(":", maxsplit=2)
except ValueError: # Compatible with 2.0.0a1
project_path = None
project_id, project_name = project.split(":", maxsplit=1)
if project_id not in [p["project_id"] for p in Controller.instance().projects()]:
size -= 1
continue
action = self.recent_project_actions[index]
if project_path and os.path.exists(project_path):
action.setText(" {}. {} [{}]".format(index + 1, project_name, project_path))
action.setData((project_id, project_path, ))
else:
action.setText(" {}. {}".format(index + 1, project_name))
action.setData((project_id, ))
index += 1
for index in range(size + 1, self._max_recent_files):
self._recent_project_actions[index].setVisible(False)
if Controller.instance().isRemote():
for index in range(0, size):
self.recent_project_actions[index].setVisible(True)
for index in range(size + 1, self._maxrecent_files):
self.recent_project_actions[index].setVisible(False)
if size:
self._recent_project_actions_separator.setVisible(True)
if size:
self.recent_project_actions_separator.setVisible(True)
else:
for action in self.recent_project_actions:
action.setVisible(False)
self.recent_project_actions_separator.setVisible(False)
def updateRecentFileSettings(self, path):
"""
@@ -1083,7 +1289,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if path in recent_files:
recent_files.remove(path)
recent_files.insert(0, path)
if len(recent_files) > self._max_recent_files:
if len(recent_files) > self._maxrecent_files:
recent_files.pop()
# write the recent file list
@@ -1100,7 +1306,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
for file_path in self._settings["recent_files"]:
try:
if file_path and os.path.exists(file_path):
action = self._recent_file_actions[index]
action = self.recent_file_actions[index]
action.setText(" {}. {}".format(index + 1, os.path.basename(file_path)))
action.setData(file_path)
action.setVisible(True)
@@ -1111,15 +1317,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
pass
if not Controller.instance().isRemote():
for index in range(size + 1, self._max_recent_files):
self._recent_file_actions[index].setVisible(False)
for index in range(size + 1, self._maxrecent_files):
self.recent_file_actions[index].setVisible(False)
if size:
self._recent_file_actions_separator.setVisible(True)
self.recent_file_actions_separator.setVisible(True)
else:
for index in range(0, self._max_recent_files):
self._recent_file_actions[index].setVisible(False)
self._recent_file_actions_separator.setVisible(False)
for index in range(0, self._maxrecent_files):
self.recent_file_actions[index].setVisible(False)
self.recent_file_actions_separator.setVisible(False)
def _controllerConnectedSlot(self):
self.updateRecentFileActions()
@@ -1156,11 +1362,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Topology.instance().importProject(path)
def _editProjectActionSlot(self):
if Topology.instance().project() is None:
return
dialog = EditProjectDialog(self)
dialog.show()
dialog.exec_()
def _deleteProjectActionSlot(self):
if Topology.instance().project() is None:
return
reply = QtWidgets.QMessageBox.warning(
self,
"GNS3",

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,41 +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"]:
node.create(ports=info["ports_mapping"], default_name_format=info["default_name_format"])
return
elif isinstance(node, Nat):
for key, info in self._nat_nodes.items():
if node_name == info["name"]:
node.create(default_name_format=info["default_name_format"])
return
elif isinstance(node, EthernetHub):
for key, info in self._ethernet_hubs.items():
if node_name == info["name"]:
node.create(ports=info["ports_mapping"], default_name_format=info["default_name_format"])
return
elif isinstance(node, EthernetSwitch):
for key, info in self._ethernet_switches.items():
if node_name == info["name"]:
node.create(ports=info["ports_mapping"], default_name_format=info["default_name_format"])
return
node.create()
@staticmethod
def findAlternativeInterface(node, missing_interface):
@@ -326,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(
@@ -367,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.
@@ -72,18 +58,19 @@ class Cloud(Node):
if "interfaces" in result:
self._interfaces = result["interfaces"].copy()
def update(self, new_settings):
def update(self, new_settings, force=False):
"""
Updates the settings for this cloud.
:param new_settings: settings dictionary
:param force: force this node to update
"""
params = {}
for name, value in new_settings.items():
if name in self._settings and self._settings[name] != value:
params[name] = value
if params:
if params or force:
self._update(params)
def _updateCallback(self, result):
@@ -108,7 +95,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

@@ -37,7 +37,7 @@ class CloudWizard(VMWizard, Ui_CloudNodeWizard):
def __init__(self, cloud_nodes, parent):
super().__init__(cloud_nodes, Builtin.instance().settings()["use_local_server"], parent)
super().__init__(cloud_nodes, parent)
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/cloud.svg"))
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)

View File

@@ -37,7 +37,7 @@ class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
def __init__(self, ethernet_hubs, parent):
super().__init__(ethernet_hubs, Builtin.instance().settings()["use_local_server"], parent)
super().__init__(ethernet_hubs, parent)
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/hub.svg"))
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
@@ -50,7 +50,7 @@ class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
"""
ports = []
for port_number in range(1, self.uiPortsSpinBox.value() + 1):
for port_number in range(0, self.uiPortsSpinBox.value()):
ports.append({"port_number": int(port_number),
"name": "Ethernet{}".format(port_number)})

View File

@@ -37,7 +37,7 @@ class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
def __init__(self, ethernet_switches, parent):
super().__init__(ethernet_switches, Builtin.instance().settings()["use_local_server"], parent)
super().__init__(ethernet_switches, parent)
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/ethernet_switch.svg"))
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
@@ -50,7 +50,7 @@ class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
"""
ports = []
for port_number in range(1, self.uiPortsSpinBox.value() + 1):
for port_number in range(0, self.uiPortsSpinBox.value()):
ports.append({"port_number": int(port_number),
"name": "Ethernet{}".format(port_number),
"type": "access",

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.
@@ -98,7 +84,7 @@ class EthernetHub(Node):
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self.compute().id())
host=self.compute().name())
port_info = ""
for port in self._ports:

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):
"""
@@ -98,7 +88,7 @@ class EthernetSwitch(Node):
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self.compute().id())
host=self.compute().name())
port_info = ""
for port in self._ports:
@@ -126,7 +116,7 @@ class EthernetSwitch(Node):
port_ethertype_info=port_ethertype_info,
port_vlan_info=port_vlan_info)
port_info += " {port_description}\n".format(port_description=port.description())
break
break
return info + port_info

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.
@@ -96,12 +82,11 @@ class FrameRelaySwitch(Node):
Local node ID is {id}
Server's Node ID is {node_id}
Hardware is Dynamips emulated simple Frame relay switch
Switch's server runs on {host}:{port}
Switch's server runs on {host}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self._compute.host(),
port=self._compute.port())
host=self._compute.name())
port_info = ""
for port in self._ports:

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

@@ -47,8 +47,6 @@ class BuiltinPreferencesPage(QtWidgets.QWidget, Ui_BuiltinPreferencesPageWidget)
:param settings: Built-in settings
"""
self.uiUseLocalServercheckBox.setChecked(settings["use_local_server"])
def loadPreferences(self):
"""Loads Built-in preferences."""
@@ -59,5 +57,4 @@ class BuiltinPreferencesPage(QtWidgets.QWidget, Ui_BuiltinPreferencesPageWidget)
"""Saves Built-in preferences."""
new_settings = {}
new_settings["use_local_server"] = self.uiUseLocalServercheckBox.isChecked()
Builtin.instance().setSettings(new_settings)

View File

@@ -19,13 +19,12 @@
Configuration page for clouds.
"""
from gns3.qt import QtGui, 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
from ..ui.cloud_configuration_page_ui import Ui_cloudConfigPageWidget
from ..cloud import Cloud
class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
@@ -48,8 +47,10 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
# connect Ethernet slots
self.uiEthernetListWidget.itemSelectionChanged.connect(self._EthernetChangedSlot)
self.uiEthernetWarningPushButton.clicked.connect(self._EthernetWarningSlot)
self.uiAddEthernetPushButton.clicked.connect(self._EthernetAddSlot)
self.uiAddAllEthernetPushButton.clicked.connect(self._EthernetAddAllSlot)
self.uiRefreshEthernetPushButton.clicked.connect(self._EthernetRefreshSlot)
self.uiDeleteEthernetPushButton.clicked.connect(self._EthernetDeleteSlot)
# connect TAP slots
@@ -57,6 +58,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
self.uiTAPListWidget.itemSelectionChanged.connect(self._TAPChangedSlot)
self.uiAddTAPPushButton.clicked.connect(self._TAPAddSlot)
self.uiAddAllTAPPushButton.clicked.connect(self._TAPAddAllSlot)
self.uiRefreshTAPPushButton.clicked.connect(self._TAPRefreshSlot)
self.uiDeleteTAPPushButton.clicked.connect(self._TAPDeleteSlot)
# connect UDP slots
@@ -68,6 +70,22 @@ 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 _refreshInterfaces(self):
"""
Refresh the network interfaces.
"""
if self._node:
self._interfaces = self._node.interfaces()
self._loadNetworkInterfaces(self._interfaces)
self._node.updated_signal.disconnect(self._refreshInterfaces)
def _EthernetChangedSlot(self):
"""
Enables the use of the delete button.
@@ -79,6 +97,13 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
else:
self.uiDeleteEthernetPushButton.setEnabled(False)
def _EthernetWarningSlot(self):
"""
Shows a warning about Wifi Ethernet interfaces.
"""
QtWidgets.QMessageBox.warning(self, "Ethernet interfaces", "Wifi interfaces may not work properly. It is recommended to use wired Ethernet or Loopback interfaces only.")
def _EthernetAddSlot(self, interface=None):
"""
Adds a new Ethernet interface.
@@ -108,6 +133,15 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
interface = self.uiEthernetComboBox.itemText(index)
self._EthernetAddSlot(interface)
def _EthernetRefreshSlot(self):
"""
Refresh all Ethernet interfaces.
"""
if self._node:
self._node.update({}, force=True)
self._node.updated_signal.connect(self._refreshInterfaces)
def _EthernetDeleteSlot(self):
"""
Deletes the selected Ethernet interface.
@@ -186,6 +220,15 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
interface = self.uiTAPComboBox.itemText(index)
self._TAPAddSlot(interface)
def _TAPRefreshSlot(self):
"""
Refresh all TAP interfaces.
"""
if self._node:
self._node.update({}, force=True)
self._node.updated_signal.connect(self._refreshInterfaces)
def _TAPDeleteSlot(self):
"""
Deletes a TAP interface.
@@ -394,9 +437,9 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
if index != -1:
self.uiCategoryComboBox.setCurrentIndex(index)
Controller.instance().get("/computes/{}/network/interfaces".format(settings["server"]),
self._getInterfacesFromServerCallback,
progressText="Retrieving network interfaces...")
Controller.instance().getCompute("/network/interfaces", settings["server"],
self._getInterfacesFromServerCallback,
progressText="Retrieving network interfaces...")
else:
self.uiDefaultNameFormatLabel.hide()

View File

@@ -69,7 +69,10 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", cloud_node["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", cloud_node["default_name_format"]])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["server"]).name()])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["server"]).name()])
except KeyError:
pass
self.uiCloudNodeInfoTreeWidget.expandAll()
self.uiCloudNodeInfoTreeWidget.resizeColumnToContents(0)

View File

@@ -19,7 +19,7 @@
Configuration page for Ethernet hubs.
"""
from gns3.qt import QtGui, QtWidgets
from gns3.qt import QtWidgets
from gns3.dialogs.node_properties_dialog import ConfigurationError
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.node import Node
@@ -146,7 +146,7 @@ class EthernetHubConfigurationPage(QtWidgets.QWidget, Ui_ethernetHubConfigPageWi
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
settings["ports_mapping"] = []
for port_number in range(1, nb_ports + 1):
for port_number in range(0, nb_ports):
settings["ports_mapping"].append({"port_number": int(port_number),
"name": "Ethernet{}".format(port_number)})
"name": "Ethernet{}".format(port_number)})
return settings

View File

@@ -70,7 +70,10 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_hub["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_hub["default_name_format"]])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["server"]).name()])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["server"]).name()])
except KeyError:
pass
QtWidgets.QTreeWidgetItem(section_item, ["Number of ports:", str(len(ethernet_hub["ports_mapping"]))])
self.uiEthernetHubInfoTreeWidget.expandAll()

View File

@@ -19,7 +19,7 @@
Configuration page for Ethernet switches.
"""
from gns3.qt import QtGui, QtCore, QtWidgets
from gns3.qt import QtCore, QtWidgets
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.node import Node

View File

@@ -70,7 +70,10 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_switch["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_switch["default_name_format"]])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["server"]).name()])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["server"]).name()])
except KeyError:
pass
for port in ethernet_switch["ports_mapping"]:
section_item = self._createSectionItem("Port{}".format(port["port_number"]))
@@ -134,7 +137,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
dialog.show()
if dialog.exec_():
# update the icon
Controller.instance().getSymbolIcon(ethernet_switches["symbol"], qpartial(self._setItemIcon, item))
Controller.instance().getSymbolIcon(ethernet_switch["symbol"], qpartial(self._setItemIcon, item))
if ethernet_switch["name"] != item.text(0):
new_key = "{server}:{name}".format(server=ethernet_switch["server"], name=ethernet_switch["name"])
if new_key in self._ethernet_switches:

View File

@@ -22,18 +22,9 @@ Default Built-in settings.
from gns3.node import Node
BUILTIN_SETTINGS = {
"use_local_server": True,
}
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

@@ -6,13 +6,16 @@
<rect>
<x>0</x>
<y>0</y>
<width>459</width>
<height>419</height>
<width>540</width>
<height>553</height>
</rect>
</property>
<property name="windowTitle">
<string>ATM Switch</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This is a simple ATM switch. Only IOS c7200 routers with at least a configured PA-A1 adapter can connect to it.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="3">
<widget class="QGroupBox" name="uiGeneralGroupBox">

View File

@@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/dynamips/ui/atm_switch_configuration_page.ui'
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/atm_switch_configuration_page.ui'
#
# Created by: PyQt5 UI code generator 5.4.2
# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_atmSwitchConfigPageWidget(object):
def setupUi(self, atmSwitchConfigPageWidget):
atmSwitchConfigPageWidget.setObjectName("atmSwitchConfigPageWidget")
atmSwitchConfigPageWidget.resize(459, 419)
atmSwitchConfigPageWidget.resize(459, 430)
self.gridLayout_2 = QtWidgets.QGridLayout(atmSwitchConfigPageWidget)
self.gridLayout_2.setObjectName("gridLayout_2")
self.uiGeneralGroupBox = QtWidgets.QGroupBox(atmSwitchConfigPageWidget)
@@ -170,6 +168,7 @@ class Ui_atmSwitchConfigPageWidget(object):
def retranslateUi(self, atmSwitchConfigPageWidget):
_translate = QtCore.QCoreApplication.translate
atmSwitchConfigPageWidget.setWindowTitle(_translate("atmSwitchConfigPageWidget", "ATM Switch"))
atmSwitchConfigPageWidget.setWhatsThis(_translate("atmSwitchConfigPageWidget", "<html><head/><body><p>This is a simple ATM switch. Only IOS c7200 routers with at least a configured PA-A1 adapter can connect to it.</p></body></html>"))
self.uiGeneralGroupBox.setTitle(_translate("atmSwitchConfigPageWidget", "General"))
self.uiNameLabel.setText(_translate("atmSwitchConfigPageWidget", "Name:"))
self.uiVPICheckBox.setText(_translate("atmSwitchConfigPageWidget", "Use VPI only (VP tunnel)"))
@@ -186,3 +185,4 @@ class Ui_atmSwitchConfigPageWidget(object):
self.uiDestinationPortLabel.setText(_translate("atmSwitchConfigPageWidget", "Port:"))
self.uiDestinationVPILabel.setText(_translate("atmSwitchConfigPageWidget", "VPI:"))
self.uiDestinationVCILabel.setText(_translate("atmSwitchConfigPageWidget", "VCI:"))

View File

@@ -24,19 +24,9 @@
</property>
<widget class="QWidget" name="uiServerSettingsTabWidget">
<attribute name="title">
<string>General settings</string>
<string>Local settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="uiUseLocalServercheckBox">
<property name="text">
<string>Use the local server</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/builtin_preferences_page.ui'
#
# Created: Thu Jun 9 21:08:46 2016
# Created: Wed Dec 7 21:40:18 2016
# by: PyQt5 UI code generator 5.2.1
#
# WARNING! All changes made in this file will be lost!
@@ -22,10 +22,6 @@ class Ui_BuiltinPreferencesPageWidget(object):
self.uiServerSettingsTabWidget.setObjectName("uiServerSettingsTabWidget")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.uiServerSettingsTabWidget)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.uiUseLocalServercheckBox = QtWidgets.QCheckBox(self.uiServerSettingsTabWidget)
self.uiUseLocalServercheckBox.setChecked(True)
self.uiUseLocalServercheckBox.setObjectName("uiUseLocalServercheckBox")
self.verticalLayout_2.addWidget(self.uiUseLocalServercheckBox)
spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_2.addItem(spacerItem)
self.uiTabWidget.addTab(self.uiServerSettingsTabWidget, "")
@@ -46,7 +42,6 @@ class Ui_BuiltinPreferencesPageWidget(object):
def retranslateUi(self, BuiltinPreferencesPageWidget):
_translate = QtCore.QCoreApplication.translate
BuiltinPreferencesPageWidget.setWindowTitle(_translate("BuiltinPreferencesPageWidget", "Built-in"))
self.uiUseLocalServercheckBox.setText(_translate("BuiltinPreferencesPageWidget", "Use the local server"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("BuiltinPreferencesPageWidget", "General settings"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("BuiltinPreferencesPageWidget", "Local settings"))
self.uiRestoreDefaultsPushButton.setText(_translate("BuiltinPreferencesPageWidget", "Restore defaults"))

View File

@@ -6,13 +6,16 @@
<rect>
<x>0</x>
<y>0</y>
<width>758</width>
<height>299</height>
<width>1000</width>
<height>378</height>
</rect>
</property>
<property name="windowTitle">
<string>Cloud configuration</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;A cloud node allows you to connect your project to the &amp;quot;real world&amp;quot; (a network or host) using either an Ethernet interface, a TAP interface (Linux only) or even an UDP tunnel. &lt;span style=&quot; font-weight:600;&quot;&gt;Please be aware that Wifi interfaces may not work properly.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="uiTabWidget">
@@ -40,21 +43,21 @@
</property>
</widget>
</item>
<item row="0" column="1">
<item row="0" column="2">
<widget class="QPushButton" name="uiAddEthernetPushButton">
<property name="text">
<string>&amp;Add</string>
</property>
</widget>
</item>
<item row="0" column="2">
<item row="0" column="3">
<widget class="QPushButton" name="uiAddAllEthernetPushButton">
<property name="text">
<string>&amp;Add all</string>
</property>
</widget>
</item>
<item row="0" column="3">
<item row="0" column="5">
<widget class="QPushButton" name="uiDeleteEthernetPushButton">
<property name="enabled">
<bool>false</bool>
@@ -64,7 +67,7 @@
</property>
</widget>
</item>
<item row="1" column="0" colspan="4">
<item row="1" column="0" colspan="6">
<widget class="QListWidget" name="uiEthernetListWidget">
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
@@ -74,13 +77,27 @@
</property>
</widget>
</item>
<item row="2" column="0">
<item row="0" column="1">
<widget class="QPushButton" name="uiEthernetWarningPushButton">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="uiShowSpecialInterfacesCheckBox">
<property name="text">
<string>&amp;Show special Ethernet interfaces</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="uiRefreshEthernetPushButton">
<property name="text">
<string>&amp;Refresh</string>
</property>
</widget>
</item>
</layout>
<zorder>uiEthernetListWidget</zorder>
<zorder>uiEthernetComboBox</zorder>
@@ -88,13 +105,15 @@
<zorder>uiDeleteEthernetPushButton</zorder>
<zorder>uiAddAllEthernetPushButton</zorder>
<zorder>uiShowSpecialInterfacesCheckBox</zorder>
<zorder>uiEthernetWarningPushButton</zorder>
<zorder>uiRefreshEthernetPushButton</zorder>
</widget>
<widget class="QWidget" name="TAPTab">
<attribute name="title">
<string>TAP interfaces</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="4">
<item row="1" column="5">
<widget class="QPushButton" name="uiDeleteTAPPushButton">
<property name="enabled">
<bool>false</bool>
@@ -104,7 +123,7 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="5">
<item row="2" column="0" colspan="6">
<widget class="QListWidget" name="uiTAPListWidget">
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
@@ -131,7 +150,7 @@
</property>
</widget>
</item>
<item row="0" column="1" colspan="4">
<item row="0" column="1" colspan="5">
<widget class="QComboBox" name="uiTAPComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@@ -154,6 +173,13 @@
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QPushButton" name="uiRefreshTAPPushButton">
<property name="text">
<string>&amp;Refresh</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="UDPTab">

View File

@@ -2,8 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
#
# Created: Fri Jun 10 16:26:54 2016
# by: PyQt5 UI code generator 5.2.1
# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
@@ -12,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(1000, 378)
self.verticalLayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
@@ -33,21 +32,36 @@ class Ui_cloudConfigPageWidget(object):
self.gridLayout_3.addWidget(self.uiEthernetComboBox, 0, 0, 1, 1)
self.uiAddEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiAddEthernetPushButton.setObjectName("uiAddEthernetPushButton")
self.gridLayout_3.addWidget(self.uiAddEthernetPushButton, 0, 1, 1, 1)
self.gridLayout_3.addWidget(self.uiAddEthernetPushButton, 0, 2, 1, 1)
self.uiAddAllEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiAddAllEthernetPushButton.setObjectName("uiAddAllEthernetPushButton")
self.gridLayout_3.addWidget(self.uiAddAllEthernetPushButton, 0, 2, 1, 1)
self.gridLayout_3.addWidget(self.uiAddAllEthernetPushButton, 0, 3, 1, 1)
self.uiDeleteEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiDeleteEthernetPushButton.setEnabled(False)
self.uiDeleteEthernetPushButton.setObjectName("uiDeleteEthernetPushButton")
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 3, 1, 1)
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 5, 1, 1)
self.uiEthernetListWidget = QtWidgets.QListWidget(self.EthernetTab)
self.uiEthernetListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.uiEthernetListWidget.setObjectName("uiEthernetListWidget")
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 4)
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 6)
self.uiEthernetWarningPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiEthernetWarningPushButton.setText("")
self.uiEthernetWarningPushButton.setObjectName("uiEthernetWarningPushButton")
self.gridLayout_3.addWidget(self.uiEthernetWarningPushButton, 0, 1, 1, 1)
self.uiShowSpecialInterfacesCheckBox = QtWidgets.QCheckBox(self.EthernetTab)
self.uiShowSpecialInterfacesCheckBox.setObjectName("uiShowSpecialInterfacesCheckBox")
self.gridLayout_3.addWidget(self.uiShowSpecialInterfacesCheckBox, 2, 0, 1, 1)
self.gridLayout_3.addWidget(self.uiShowSpecialInterfacesCheckBox, 2, 0, 1, 2)
self.uiRefreshEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiRefreshEthernetPushButton.setObjectName("uiRefreshEthernetPushButton")
self.gridLayout_3.addWidget(self.uiRefreshEthernetPushButton, 0, 4, 1, 1)
self.uiEthernetListWidget.raise_()
self.uiEthernetComboBox.raise_()
self.uiAddEthernetPushButton.raise_()
self.uiDeleteEthernetPushButton.raise_()
self.uiAddAllEthernetPushButton.raise_()
self.uiShowSpecialInterfacesCheckBox.raise_()
self.uiEthernetWarningPushButton.raise_()
self.uiRefreshEthernetPushButton.raise_()
self.uiTabWidget.addTab(self.EthernetTab, "")
self.TAPTab = QtWidgets.QWidget()
self.TAPTab.setObjectName("TAPTab")
@@ -56,11 +70,11 @@ class Ui_cloudConfigPageWidget(object):
self.uiDeleteTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
self.uiDeleteTAPPushButton.setEnabled(False)
self.uiDeleteTAPPushButton.setObjectName("uiDeleteTAPPushButton")
self.gridLayout_2.addWidget(self.uiDeleteTAPPushButton, 1, 4, 1, 1)
self.gridLayout_2.addWidget(self.uiDeleteTAPPushButton, 1, 5, 1, 1)
self.uiTAPListWidget = QtWidgets.QListWidget(self.TAPTab)
self.uiTAPListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.uiTAPListWidget.setObjectName("uiTAPListWidget")
self.gridLayout_2.addWidget(self.uiTAPListWidget, 2, 0, 1, 5)
self.gridLayout_2.addWidget(self.uiTAPListWidget, 2, 0, 1, 6)
self.uiTAPLineEdit = QtWidgets.QLineEdit(self.TAPTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -81,10 +95,13 @@ class Ui_cloudConfigPageWidget(object):
self.uiTAPComboBox.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
self.uiTAPComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
self.uiTAPComboBox.setObjectName("uiTAPComboBox")
self.gridLayout_2.addWidget(self.uiTAPComboBox, 0, 1, 1, 4)
self.gridLayout_2.addWidget(self.uiTAPComboBox, 0, 1, 1, 5)
self.uiAddAllTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
self.uiAddAllTAPPushButton.setObjectName("uiAddAllTAPPushButton")
self.gridLayout_2.addWidget(self.uiAddAllTAPPushButton, 1, 3, 1, 1)
self.uiRefreshTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
self.uiRefreshTAPPushButton.setObjectName("uiRefreshTAPPushButton")
self.gridLayout_2.addWidget(self.uiRefreshTAPPushButton, 1, 4, 1, 1)
self.uiTabWidget.addTab(self.TAPTab, "")
self.UDPTab = QtWidgets.QWidget()
self.UDPTab.setObjectName("UDPTab")
@@ -225,16 +242,19 @@ class Ui_cloudConfigPageWidget(object):
def retranslateUi(self, cloudConfigPageWidget):
_translate = QtCore.QCoreApplication.translate
cloudConfigPageWidget.setWindowTitle(_translate("cloudConfigPageWidget", "Cloud configuration"))
cloudConfigPageWidget.setWhatsThis(_translate("cloudConfigPageWidget", "<html><head/><body><p>A cloud node allows you to connect your project to the &quot;real world&quot; (a network or host) using either an Ethernet interface, a TAP interface (Linux only) or even an UDP tunnel. <span style=\" font-weight:600;\">Please be aware that Wifi interfaces may not work properly.</span></p></body></html>"))
self.uiAddEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiAddAllEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
self.uiDeleteEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiEthernetListWidget.setSortingEnabled(True)
self.uiShowSpecialInterfacesCheckBox.setText(_translate("cloudConfigPageWidget", "&Show special Ethernet interfaces"))
self.uiRefreshEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Refresh"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.EthernetTab), _translate("cloudConfigPageWidget", "Ethernet interfaces"))
self.uiDeleteTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiTAPListWidget.setSortingEnabled(True)
self.uiAddTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiAddAllTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
self.uiRefreshTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Refresh"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.TAPTab), _translate("cloudConfigPageWidget", "TAP interfaces"))
self.uiUDPTunnelSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "UDP tunnel settings"))
self.uiRemoteHostLineEdit.setText(_translate("cloudConfigPageWidget", "127.0.0.1"))

View File

@@ -105,13 +105,13 @@
</sizepolicy>
</property>
<property name="minimum">
<number>1</number>
<number>0</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>1</number>
<number>0</number>
</property>
</widget>
</item>

View File

@@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/ethernet_switch_configuration_page.ui'
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/builtin/ui/ethernet_switch_configuration_page.ui'
#
# Created: Fri Jun 10 20:45:43 2016
# by: PyQt5 UI code generator 5.2.1
# Created by: PyQt5 UI code generator 5.6
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_ethernetSwitchConfigPageWidget(object):
def setupUi(self, ethernetSwitchConfigPageWidget):
ethernetSwitchConfigPageWidget.setObjectName("ethernetSwitchConfigPageWidget")
ethernetSwitchConfigPageWidget.resize(545, 435)
@@ -69,9 +70,9 @@ class Ui_ethernetSwitchConfigPageWidget(object):
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiPortSpinBox.sizePolicy().hasHeightForWidth())
self.uiPortSpinBox.setSizePolicy(sizePolicy)
self.uiPortSpinBox.setMinimum(1)
self.uiPortSpinBox.setMinimum(0)
self.uiPortSpinBox.setMaximum(65535)
self.uiPortSpinBox.setProperty("value", 1)
self.uiPortSpinBox.setProperty("value", 0)
self.uiPortSpinBox.setObjectName("uiPortSpinBox")
self.gridlayout.addWidget(self.uiPortSpinBox, 0, 1, 1, 1)
self.label_3 = QtWidgets.QLabel(self.uiEthernetSwitchSettingsGroupBox)
@@ -174,4 +175,3 @@ class Ui_ethernetSwitchConfigPageWidget(object):
self.uiPortsTreeWidget.headerItem().setText(3, _translate("ethernetSwitchConfigPageWidget", "EtherType"))
self.uiAddPushButton.setText(_translate("ethernetSwitchConfigPageWidget", "&Add"))
self.uiDeletePushButton.setText(_translate("ethernetSwitchConfigPageWidget", "&Delete"))

View File

@@ -7,12 +7,15 @@
<x>0</x>
<y>0</y>
<width>499</width>
<height>405</height>
<height>414</height>
</rect>
</property>
<property name="windowTitle">
<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 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">
<widget class="QGroupBox" name="uiGeneralGroupBox">

View File

@@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/dynamips/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.4.2
# 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_frameRelaySwitchConfigPageWidget(object):
def setupUi(self, frameRelaySwitchConfigPageWidget):
frameRelaySwitchConfigPageWidget.setObjectName("frameRelaySwitchConfigPageWidget")
frameRelaySwitchConfigPageWidget.resize(499, 405)
frameRelaySwitchConfigPageWidget.resize(499, 414)
self.gridLayout_2 = QtWidgets.QGridLayout(frameRelaySwitchConfigPageWidget)
self.gridLayout_2.setObjectName("gridLayout_2")
self.uiGeneralGroupBox = QtWidgets.QGroupBox(frameRelaySwitchConfigPageWidget)
@@ -136,6 +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 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"))
@@ -149,3 +148,4 @@ class Ui_frameRelaySwitchConfigPageWidget(object):
self.uiDestinationDLCILabel.setText(_translate("frameRelaySwitchConfigPageWidget", "DLCI:"))
self.uiAddPushButton.setText(_translate("frameRelaySwitchConfigPageWidget", "&Add"))
self.uiDeletePushButton.setText(_translate("frameRelaySwitchConfigPageWidget", "&Delete"))

View File

@@ -137,61 +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.server().host() or (
node.server().isLocal() and info["server"] == "local"):
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()
@@ -202,7 +150,7 @@ class Docker(Module):
:param server: server to send the request to
:param callback: callback for the reply from the server
"""
Controller.instance().get("/computes/{}/docker/images".format(compute_id), callback)
Controller.instance().getCompute("/docker/images", compute_id, callback)
@staticmethod
def getNodeClass(name):

View File

@@ -21,6 +21,7 @@ import sys
from gns3.qt import QtGui, QtWidgets
from gns3.dialogs.vm_wizard import VMWizard
from gns3.compute_manager import ComputeManager
from ..ui.docker_vm_wizard_ui import Ui_DockerVMWizard
from .. import Docker
@@ -35,7 +36,7 @@ class DockerVMWizard(VMWizard, Ui_DockerVMWizard):
def __init__(self, docker_containers, parent):
super().__init__(docker_containers, Docker.instance().settings()["use_local_server"], parent)
super().__init__(docker_containers, parent)
self._docker_containers = docker_containers
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/docker.png"))
@@ -43,7 +44,7 @@ class DockerVMWizard(VMWizard, Ui_DockerVMWizard):
self._existingImageRadioButtonToggledSlot(False)
self.uiExistingImageRadioButton.toggled.connect(self._existingImageRadioButtonToggledSlot)
if sys.platform.startswith("win") or sys.platform.startswith("darwin"):
if ComputeManager.instance().localPlatform().startswith("win") or ComputeManager.instance().localPlatform().startswith("darwin"):
# Cannot use Docker locally on Windows and Mac
self._disableLocalServer()

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

@@ -40,9 +40,8 @@ class DockerPreferencesPage(QtWidgets.QWidget, Ui_DockerPreferencesPageWidget):
# connect signals
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
if not sys.platform.startswith("linux"):
# if not sys.platform.startswith("linux"):
# Docker is only supported on Linux
self.uiUseLocalServercheckBox.setEnabled(False)
def _restoreDefaultsSlot(self):
"""Slot to populate the page widgets with the default settings."""
@@ -53,7 +52,6 @@ class DockerPreferencesPage(QtWidgets.QWidget, Ui_DockerPreferencesPageWidget):
:param settings: Docker settings
"""
self.uiUseLocalServercheckBox.setChecked(settings["use_local_server"])
def loadPreferences(self):
"""Loads Docker preferences."""
@@ -63,5 +61,4 @@ class DockerPreferencesPage(QtWidgets.QWidget, Ui_DockerPreferencesPageWidget):
def savePreferences(self):
"""Saves Docker preferences."""
new_settings = {}
new_settings["use_local_server"] = self.uiUseLocalServercheckBox.isChecked()
Docker.instance().setSettings(new_settings)

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