Compare commits

...

196 Commits

Author SHA1 Message Date
grossmj
450fbc9af3 Release v2.1.20 2019-05-29 15:44:25 +07:00
grossmj
469ee8fab8 Fix KeyError: 'endpoint' issue. Fixes #2802 2019-05-28 23:17:55 +07:00
grossmj
6ccfcaf76e Development on 2.1.20dev1 2019-05-28 16:33:43 +07:00
grossmj
520e857874 Release v2.1.19 2019-05-28 15:23:35 +07:00
grossmj
012c7b4241 Fix wrong aligment of symbols in saved/exported projects. Fixes #2800 2019-05-27 16:33:51 +07:00
grossmj
1d71cd5bf0 Replace urllib.request by Qt implementation for local server synchronous check. Fixes #2793 2019-05-27 16:03:55 +07:00
grossmj
d96277882a Set grid's minimum to 5. Fixes #2795 2019-05-23 14:41:53 +07:00
grossmj
ecec917752 Development on 2.1.19dev1 2019-05-23 14:37:37 +07:00
grossmj
ea9c1a8ee1 Release v2.1.18 2019-05-22 16:13:28 +07:00
grossmj
cfbb09fb57 Fix error in HTTPConnection.request for Python3.6. Fixes #2793 2019-05-22 16:05:34 +07:00
grossmj
dc8aa1fb92 Catch more OSError/PermissionError when checking md5 on remote images. Fixes #2582 2019-05-22 15:23:56 +07:00
grossmj
786cc8aa65 Fix exception when grid size is 0. Fixes #2790 2019-05-22 15:13:16 +07:00
grossmj
4a353e08e3 Catch PermissionError when scanning local image directories. Fixes #2791 2019-05-22 14:55:07 +07:00
grossmj
2a59013604 Revert "Make sure the latest PyQt5 version 5.12.x is used on Windows." Ref #2778 2019-05-20 12:01:16 +07:00
grossmj
7732aaf9a5 Release v2.1.17 2019-05-17 15:10:28 +07:00
grossmj
1f566a31cf Development on 2.1.17dev1 2019-04-15 12:41:41 +07:00
grossmj
10d75e15da Release v2.1.16 2019-04-15 12:00:18 +07:00
grossmj
17def7e00a Do not make NPF or NPCAP service mandatory to start the local server on Windows. 2019-04-15 10:33:27 +07:00
grossmj
f68a8ea829 Fix OverflowError error with progress dialog. Fixes #2767 2019-04-13 17:38:43 +07:00
grossmj
50066b2f12 More fixes for stuck progress window. Fixes #2765 2019-04-13 17:10:24 +07:00
grossmj
21a99d4376 Fix adding multiple devices - stuck progress window. Fixes #2765 2019-04-13 17:04:23 +07:00
grossmj
f97d3041b8 Make sure the latest PyQt5 version 5.12.x is used on Windows. 2019-04-13 15:43:23 +07:00
grossmj
31d6a065b0 Show a warning when a config export is not supported. Ref #2762 2019-04-11 15:32:22 +07:00
grossmj
8f077456b1 Development on 2.1.16dev1 2019-03-21 13:56:11 +08:00
grossmj
a29f3e35c0 Release v2.1.15 2019-03-21 11:41:44 +08:00
grossmj
fc3781550a Development on 2.1.15dev1 2019-02-27 15:59:16 +07:00
grossmj
d285e62c04 Release v2.1.14 2019-02-27 14:58:52 +07:00
grossmj
44d70de687 Better description to why an appliance cannot be installed. 2019-02-27 14:51:12 +07:00
grossmj
752c516f82 Development on 2.1.14dev1 2019-02-26 18:09:56 +07:00
grossmj
e1ec6c5771 Merge remote-tracking branch 'origin/2.1' into 2.1 2019-02-26 16:43:27 +07:00
grossmj
e8308869d9 Release v2.1.13 2019-02-26 16:43:14 +07:00
Jeremy Grossmann
484c5abe9d Force jsonschema dependency to 2.6.0
This is to fix this issue when starting the (frozen) application on Windows:

```
  File "C:\Python36-x64\lib\site-packages\jsonschema\validators.py", line 505, in <module>
  File "C:\Python36-x64\lib\site-packages\jsonschema\_utils.py", line 57, in load_schema
  File "C:\Python36-x64\lib\pkgutil.py", line 634, in get_data
OSError: [Errno 34] Result too large: 'jsonschema\\schemas\\draft6.json'
```
2019-02-26 15:28:16 +07:00
grossmj
fe222b873f Disable computer hibernation detection mechanism. Ref #2678 2019-02-22 17:04:12 +07:00
grossmj
f8bb6661dd Add some advice for request timeout message. Fixes #2652 2019-02-20 00:14:15 +07:00
grossmj
0f9aab9230 Merge remote-tracking branch 'origin/2.1' into 2.1 2019-02-19 16:07:51 +07:00
grossmj
a5cf5e16b7 Show/Hide interface labels when status points are not shown. Fixes #2690 2019-02-19 16:07:39 +07:00
Jeremy Grossmann
097458d108 Merge pull request #2715 from GNS3/critical-messages-before-running
Show critical messages before the main window runs. Fixes #2710
2019-02-19 15:19:53 +07:00
grossmj
f4cafac9c7 Do not print critical message twice on stderr.
Replace QMessageBox calls with no parent by log.error()/log.warning().
2019-02-18 22:09:23 +08:00
grossmj
7f132fdc36 Show critical messages before the main window runs. 2019-02-18 11:22:13 +08:00
grossmj
6b7d629755 Avoid using PyQt5.Qt, which imports unneeded stuff. Fixes #2592 2019-02-16 15:05:42 +08:00
grossmj
b7ccc37ea5 Fix SIP import error with recent PyQt versions. Fixes #2709 2019-02-16 14:38:44 +08:00
Jeremy Grossmann
bbe2826c77 Upgrade to Qt 5.12. Fixes #2636 2019-02-12 11:55:09 +08:00
grossmj
68e2a0ee39 Adjust the setup wizard (VMware image size, layouts). 2019-01-27 22:44:56 +08:00
grossmj
52418ed94a Development on 2.1.13dev1 2019-01-23 15:25:48 +08:00
grossmj
a1496bffd4 Release 2.1.12 2019-01-23 15:22:26 +08:00
Jeremy Grossmann
911f6305fa Merge pull request #2677 from GNS3/svg-symbols-fixes
Resize SVG node symbols that are too big. Fixes #2674
2019-01-22 22:25:13 +07:00
grossmj
c6594d4845 Checkbox to activate/deactivate the size limit of node symbols. 2019-01-22 22:16:45 +07:00
grossmj
538adc4817 Option to limit the size of node symbols (activated by default). Ref #2674.
(cherry picked from commit 6b38b58633)
2019-01-22 00:28:06 +07:00
grossmj
961c5652ea Resize SVG node symbol only when height is above 80px. Ref #2674
Work on str instead of binary when resizing SVG symbol.

(cherry picked from commit 938e9129cd)
2019-01-22 00:24:30 +07:00
grossmj
c0ecf3ccc4 Automatically resize SVG symbols that are too big. Ref #2674.
(cherry picked from commit 8f381a4720)
2019-01-22 00:24:11 +07:00
grossmj
cf73db25b4 Update VMware banners and links. 2018-12-21 13:18:29 +08:00
grossmj
dd79939140 Allow users to refresh the template list in the nodes view panel. 2018-12-18 17:52:50 -06:00
grossmj
45b3c17c97 Remove typo. Ref #2648. 2018-12-13 22:37:00 -06:00
grossmj
1ddf3e6388 Fix Dynamips decompress doesn't work with relative images. Fixes #2648. 2018-12-13 22:32:06 -06:00
Jeremy Grossmann
1c8e166393 Update download URL for "Check For Update". 2018-11-23 16:28:59 +07:00
grossmj
ec324f9b01 Development on 2.1.12dev1 2018-09-28 20:44:52 +02:00
grossmj
cbfd59498e Release v2.1.11 2018-09-28 20:40:58 +02:00
grossmj
0216bc8b4d Handle deleted SIP objects. 2018-09-28 15:01:22 +02:00
grossmj
a8477597ab Update paths for UltraVNC and VirtViewer. 2018-09-27 22:22:36 +02:00
grossmj
e240dbad6b Indicate if Solar-PuTTY is included or not. Fixes #2595 2018-09-27 21:08:51 +02:00
grossmj
8d183a3283 Fix bad link to installation instructions in README.rst. Fixes #2590 2018-09-20 14:58:41 +02:00
grossmj
3af5046d0f Downgrade to Qt 5.9. Fixes #2592. 2018-09-20 14:30:57 +02:00
grossmj
c8397a1ef7 Development on 2.1.11dev1 2018-09-15 11:13:39 +02:00
grossmj
b419891950 Release v2.1.10 2018-09-15 11:11:24 +02:00
grossmj
2c1ba697bd Fix small errors like unhandled exceptions etc. 2018-09-11 15:06:01 +02:00
grossmj
3000a9aa7f Fix when appliance version is not available for Dynamips/IOU/Qemu. Fixes #2585. 2018-09-05 15:32:38 +08:00
grossmj
2f8541c543 Fix issue when installing appliance with no version selected. Fixes #2585. 2018-09-05 14:53:47 +08:00
grossmj
5c3d4b2ab6 Check for existing appliance name across all emulator types. Fixes #2584. 2018-09-05 14:08:05 +08:00
grossmj
f44ac8cba5 Improve the invalid port format detection. Fixes https://github.com/GNS3/gns3-gui/issues/2580 2018-09-05 13:35:42 +08:00
grossmj
7ef49fbca7 Catch OSError/PermissionError when checking md5 on remote image. Fixes #2582. 2018-09-05 13:24:01 +08:00
grossmj
5ccf5778a2 Fix UnicodeDecodeError in file editor. Fixes #2581. 2018-09-04 21:52:35 +08:00
grossmj
6030d5e019 Catch import error for win32serviceutil. Fixes #2583. 2018-09-04 21:18:03 +08:00
grossmj
6de8880937 Fix bug with empty project ID when creating a new node. Fixes #2366
..
2018-09-04 20:51:30 +08:00
grossmj
08c89c4fac Fix various small errors, mostly about non-existing C/C++ objects. 2018-09-03 16:48:23 +07:00
grossmj
e411d497c4 Send extra controller and compute information in crash reports. 2018-09-02 21:47:33 +07:00
grossmj
e037835769 Update setup.py and fix minor issues. 2018-09-02 15:32:34 +07:00
grossmj
a5f4ec0135 Set the default delay console all value to 1500ms if using Solar-PuTTY. 2018-08-29 21:15:40 +07:00
grossmj
154f10a686 Make Solar-Putty the default if installed. Ref #2519. 2018-08-27 16:24:38 +07:00
grossmj
e5320c318f Fix tests. 2018-08-22 20:37:55 +07:00
grossmj
07ea6207c1 Fix issue with custom appliance. Fixes https://github.com/GNS3/gns3-registry/issues/361 2018-08-22 20:18:35 +07:00
grossmj
12398881f8 Forbid controller and compute servers to be different versions.
Report last compute server error to clients and display in the server summary.
2018-08-22 16:54:43 +07:00
grossmj
27a8e3c7f8 Fix issue with appliance categories. Fixes https://github.com/GNS3/gns3-registry/issues/361 2018-08-22 15:52:32 +07:00
grossmj
d92ff1abe3 Add compute information to crash reports. 2018-08-21 20:40:01 +07:00
grossmj
e97b3b6a42 Add controller version in Sentry bug reports. 2018-08-21 19:16:49 +07:00
grossmj
5ee3f73213 Backport: Fix "Network session error" issues. Fixes #2560. 2018-08-21 18:29:57 +07:00
grossmj
a30aa2f5f1 Add SolarPutty command line. Fixes #2519. 2018-08-21 18:16:51 +07:00
grossmj
98bb6590aa Add missing Qemu boot priority values. Fixes https://github.com/GNS3/gns3-server/issues/1385 2018-08-21 17:49:58 +07:00
grossmj
4250e961a3 Merge remote-tracking branch 'origin/2.1' into 2.1 2018-08-21 17:32:24 +07:00
grossmj
3c46a3a72d Update PyQt5 from version 5.8 to version 5.10. Fixes #2564. 2018-08-21 17:32:09 +07:00
ziajka
c55442a517 Development on 2.1.10dev1 2018-08-13 13:50:31 +02:00
ziajka
45e0080726 Release v2.1.9 2018-08-13 13:14:20 +02:00
grossmj
95558ec2e6 Fix incorrect short port names in topology summary. Fixes https://github.com/GNS3/gns3-gui/issues/2562 2018-08-13 15:10:21 +07:00
grossmj
5b56d54030 Add compute version in server summary tooltip. 2018-08-07 15:32:16 +07:00
Jeremy Grossmann
d5ee1ea5d2 Merge pull request #2538 from ehlers/osx_telnet_utf8_path
Support PATH with UTF-8 characters in OSX telnet console, fixes #2537
2018-07-30 10:13:52 -05:00
grossmj
69b8c07c0a Fix test for Qemu boot priority. Fixes #2548. 2018-07-30 09:48:38 -05:00
grossmj
dbe73eb8d7 Fix boot priority missing when installing an appliance. Fixes #2548. 2018-07-30 09:29:51 -05:00
Bernhard Ehlers
706f89debb Support PATH with UTF-8 characters in OSX telnet console, fixes #2537 2018-07-14 12:38:53 +02:00
grossmj
ec0be9e22b Allow users to accept different MD5 hashes for preconfigured appliances. Fixes #2526. 2018-07-10 16:02:44 +08:00
grossmj
0e6fa597ec Do not try to update drawing if it is being deleted. Ref #2483. 2018-07-10 15:39:35 +08:00
grossmj
f81450c65a Merge remote-tracking branch 'origin/2.1' into 2.1 2018-07-10 11:54:26 +08:00
grossmj
38cbe70aaa Catch exception when loading invalid appliance file. 2018-07-10 11:54:11 +08:00
ziajka
47d335f4c9 Release v2.1.8 2018-06-14 15:16:54 +02:00
grossmj
20d4f73f56 Add error information when cannot access/read IOS/IOU config file. Ref #2501 2018-06-13 16:27:43 +08:00
grossmj
5204184029 Fallback when using process name to bring console to front. 2018-06-12 17:55:09 +08:00
grossmj
9915beeb8e Use process name to bring console to front. Fixes #2514. 2018-06-12 17:45:54 +08:00
ziajka
1ea383fce2 Development on v2.1.8dev1 2018-06-12 11:15:26 +02:00
ziajka
2744e669b4 Release v2.1.7 2018-06-12 11:12:49 +02:00
grossmj
6ab2d63bdc Do not try to update link if it is being deleted. Fixes #2483. 2018-06-06 21:00:08 +07:00
grossmj
0de6bfe7e1 Fix can't add SVG image to project. Fixes #2502 2018-06-06 18:26:37 +07:00
grossmj
f144103bca Remove unwanted trailing characters and other white spaces when reading .md5sum files. Fixes #2498. 2018-06-04 23:59:53 +07:00
grossmj
c0b26aff48 Update interface sequence number check. Fixes #2491. 2018-06-04 22:31:19 +07:00
ziajka
9601e4e6f2 Logo should not have context menu, Fixes: #2507 2018-05-24 13:21:13 +02:00
ziajka
88708c2a8d Update logo position only when changes, Fixes: #2506 2018-05-24 13:15:32 +02:00
ziajka
8eff12194d Development on v2.1.7dev1 2018-05-22 14:14:46 +02:00
ziajka
b0520b2bd4 Release v2.1.6 2018-05-22 14:11:59 +02:00
ziajka
17d2c023bf Fix redraw logo on Windows 2018-05-22 13:16:48 +02:00
ziajka
ce9fdea0a0 Merge pull request #2492 from GNS3/extra-hosts
Extra hosts for Docker, global variables for project and supplier logo support, Fixes: #2482
2018-05-15 09:23:42 +02:00
ziajka
24d7dacb4e Variables fix on ProjectWelcomeDialog 2018-05-10 10:46:57 +02:00
ziajka
bb36765407 Remove project_created_signal 2018-05-09 15:24:41 +02:00
ziajka
250db92ce0 Ask for global variables when project is loaded 2018-05-09 11:54:13 +02:00
ziajka
d59ec39505 Add/Edit global variables of project 2018-05-08 18:31:26 +02:00
ziajka
5e9ae04dc1 Rename tabs at Edit Project 2018-05-08 17:05:25 +02:00
ziajka
ddb0fccda3 Global variables tab on Edit project 2018-05-08 17:03:04 +02:00
ziajka
9b22a52f14 Support of supplier logo and url 2018-05-08 16:22:01 +02:00
grossmj
948878bfdd Add missing crowdfunder name in About dialog. 2018-05-08 21:52:37 +08:00
ziajka
7340abbaa9 Project variables and supplier 2018-05-08 13:00:32 +02:00
grossmj
4ea0528bf2 No timeout when duplicating a project. 2018-04-28 17:09:08 +07:00
grossmj
49005e6add No timeout when restoring snapshot. 2018-04-28 16:41:54 +07:00
ziajka
5484c039b5 Fix tests 2018-04-27 14:47:09 +02:00
ziajka
daaf71b6d2 Add advanced settings for docker and param, Ref. #2482 2018-04-27 14:28:14 +02:00
grossmj
450f0e006b Merge remote-tracking branch 'origin/2.1' into 2.1 2018-04-23 15:40:18 +07:00
grossmj
a6a967fbde Replace "not supported" by "none" in topology summary view. 2018-04-23 15:39:58 +07:00
ziajka
1a6293709e Development on v2.1.6 2018-04-18 11:41:43 +02:00
ziajka
2ed53225e0 Release v2.1.5 2018-04-18 11:28:52 +02:00
grossmj
b8798fbda5 Disable TraceNG for version 2.1.5 2018-04-18 17:19:44 +08:00
grossmj
368de32faa Fix Qemu binary list locks when a version is deleted. Fixes #2474. 2018-04-18 15:44:33 +08:00
grossmj
98d01cbfa0 Fix invalid answer from the PyPi server. Fixes #2473. 2018-04-18 15:10:31 +08:00
grossmj
ad62bb7832 Fix wrong wizard page name. 2018-04-16 17:16:20 +08:00
grossmj
637061663a Add default destination setting for traceng + some checks. Ref #2450. 2018-04-16 15:03:02 +08:00
grossmj
c137198985 Grid size support for projects. Fixes #2469. 2018-04-13 16:56:37 +08:00
grossmj
946efb61de Remove 'include INSTALL' from MANIFEST. Fixes #2470. 2018-04-13 14:17:03 +08:00
grossmj
4c610acfa4 Fix traceng tests. 2018-03-30 12:10:57 +07:00
grossmj
37f74824f1 Merge branch 'traceng' into 2.1 2018-03-29 15:19:29 +07:00
grossmj
5ccf8c414d Sync 2018-03-29 15:19:18 +07:00
grossmj
913f0d5e4a Check for valid IP address and prevent to run on non-Windows platforms. 2018-03-29 13:26:43 +07:00
grossmj
061bac0cc6 Support for source and destination for traceNG. 2018-03-27 16:58:49 +07:00
ziajka
ec59cd87bd Back to development on v2.1.5dev1 2018-03-15 08:46:06 +01:00
ziajka
05d9ee8499 Re-release v2.1.4 due to travis issue 2018-03-14 15:28:15 +01:00
grossmj
a72ece5c18 Custom icons and small fixes for TraceNG integration. 2018-03-14 16:56:39 +07:00
grossmj
63baa2eff0 Base support for TraceNG. 2018-03-12 17:57:13 +07: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
139 changed files with 103950 additions and 99435 deletions

View File

@@ -11,6 +11,8 @@ before_deploy:
- 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=

173
CHANGELOG
View File

@@ -1,5 +1,178 @@
# Change Log
## 2.1.20 29/05/2019
* Fix KeyError: 'endpoint' issue. Fixes #2802
## 2.1.19 28/05/2019
* Fix wrong aligment of symbols in saved/exported projects. Fixes #2800
* Replace urllib.request by Qt implementation for local server synchronous check. Fixes #2793
* Set grid's minimum to 5. Fixes #2795
## 2.1.18 22/05/2019
* Fix error in HTTPConnection.request for Python3.6. Fixes #2793
* Catch more OSError/PermissionError when checking md5 on remote images. Fixes #2582
* Fix exception when grid size is 0. Fixes #2790
* Catch PermissionError when scanning local image directories. Fixes #2791
* Revert "Make sure the latest PyQt5 version 5.12.x is used on Windows." Ref #2778
## 2.1.17 17/05/2019
* No changes.
## 2.1.16 15/04/2019
* Do not make NPF or NPCAP service mandatory to start the local server on Windows.
* Fix OverflowError error with progress dialog. Fixes #2767
* More fixes for stuck progress window. Fixes #2765
* Fix adding multiple devices - stuck progress window. Fixes #2765
* Make sure the latest PyQt5 version 5.12.x is used on Windows.
* Show a warning when a config export is not supported. Ref #2762
## 2.1.15 21/03/2019
* No changes on the GUI.
## 2.1.14 27/02/2019
* Better description to why an appliance cannot be installed.
## 2.1.13 26/02/2019
* Disable computer hibernation detection mechanism. Ref #2678
* Add some advice for request timeout message. Fixes #2652
* Show/Hide interface labels when status points are not shown. Fixes #2690
* Do not print critical message twice on stderr. Replace QMessageBox calls with no parent by log.error()/log.warning().
* Show critical messages before the main window runs.
* Avoid using PyQt5.Qt, which imports unneeded stuff. Fixes #2592
* Fix SIP import error with recent PyQt versions. Fixes #2709
* Upgrade to Qt 5.12. Fixes #2636
* Adjust the setup wizard (VMware image size, layouts).
## 2.1.12 23/01/2019
* Option to resize SVG symbols that are too big (height above 80px, activated by default). Ref #2674.
* Update VMware banners and links.
* Allow users to refresh the template list in the nodes view panel.
* Fix Dynamips decompress doesn't work with relative images. Fixes #2648.
* Update download URL for "Check For Update".
## 2.1.11 28/09/2018
* Handle deleted SIP objects.
* Update paths for UltraVNC and VirtViewer.
* Indicate if Solar-PuTTY is included or not. Fixes #2595
* Fix bad link to installation instructions in README.rst. Fixes #2590
* Downgrade to Qt 5.9. Fixes #2592.
## 2.1.10 15/09/2018
* Fix small errors like unhandled exceptions etc.
* Fix when appliance version is not available for Dynamips/IOU/Qemu. Fixes #2585.
* Fix issue when installing appliance with no version selected. Fixes #2585.
* Check for existing appliance name across all emulator types. Fixes #2584.
* Improve the invalid port format detection. Fixes https://github.com/GNS3/gns3-gui/issues/2580
* Catch OSError/PermissionError when checking md5 on remote image. Fixes #2582.
* Fix UnicodeDecodeError in file editor. Fixes #2581.
* Catch import error for win32serviceutil. Fixes #2583.
* Fix bug with empty project ID when creating a new node. Fixes #2366
* Fix various small errors, mostly about non-existing C/C++ objects.
* Send extra controller and compute information in crash reports.
* Update setup.py and fix minor issues.
* Set the default delay console all value to 1500ms if using Solar-PuTTY.
* Make Solar-Putty the default if installed. Ref #2519.
* Fix issue with custom appliance. Fixes https://github.com/GNS3/gns3-registry/issues/361
* Forbid controller and compute servers to be different versions. Report last compute server error to clients and display in the server summary.
* Fix issue with appliance categories. Fixes https://github.com/GNS3/gns3-registry/issues/361
* Add compute information to crash reports.
* Add controller version in Sentry bug reports.
* Backport: Fix "Network session error" issues. Fixes #2560.
* Add SolarPutty command line. Fixes #2519.
* Add missing Qemu boot priority values. Fixes https://github.com/GNS3/gns3-server/issues/1385
* Update PyQt5 from version 5.8 to version 5.10. Fixes #2564.
## 2.1.9 13/08/2018
* Fix incorrect short port names in topology summary. Fixes https://github.com/GNS3/gns3-gui/issues/2562
* Add compute version in server summary tooltip.
* Fix test for Qemu boot priority. Fixes #2548.
* Fix boot priority missing when installing an appliance. Fixes #2548.
* Support PATH with UTF-8 characters in OSX telnet console, fixes #2537
* Allow users to accept different MD5 hashes for preconfigured appliances. Fixes #2526.
* Do not try to update drawing if it is being deleted. Ref #2483.
* Catch exception when loading invalid appliance file.
## 2.1.8 14/06/2018
* Add error information when cannot access/read IOS/IOU config file. Ref #2501
* Fallback when using process name to bring console to front.
* Use process name to bring console to front. Fixes #2514.
## 2.1.7 12/06/2018
* Do not try to update link if it is being deleted. Fixes #2483.
* Fix can't add SVG image to project. Fixes #2502
* Remove unwanted trailing characters and other white spaces when reading .md5sum files. Fixes #2498.
* Update interface sequence number check. Fixes #2491.
* Logo should not have context menu, Fixes: #2507
* Update logo position only when changes, Fixes: #2506
## 2.1.6 22/05/2018
* Ask for global variables when project is loaded
* Add/Edit global variables of project
* Rename tabs at Edit Project
* Global variables tab on Edit project
* Support of supplier logo and url
* Add missing crowdfunder name in About dialog.
* Project variables and supplier
* No timeout when duplicating a project.
* No timeout when restoring snapshot.
* Add advanced settings for docker and ExtraHosts param, Ref. #2482
* Replace "not supported" by "none" in topology summary view.
## 2.1.5 18/04/2018
* Fix Qemu binary list locks when a version is deleted. Fixes #2474.
* Fix invalid answer from the PyPi server. Fixes #2473.
* Fix wrong wizard page name.
* Grid size support for projects. Fixes #2469.
* Remove 'include INSTALL' from MANIFEST. Fixes #2470.
* Check for valid IP address and prevent to run on non-Windows platforms.
## 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

View File

@@ -1,12 +1,12 @@
# Run tests inside a container
FROM ubuntu:yakkety
FROM ubuntu:17.10
MAINTAINER GNS3 Team
#ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y --force-yes python3.5 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.5-dev xvfb
RUN apt-get 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.5 -m pytest -vv
CMD xvfb-run python3.6 -m pytest -vv

View File

@@ -1,6 +1,5 @@
include README.rst
include AUTHORS
include INSTALL
include LICENSE
include MANIFEST.in
include requirements.txt

View File

@@ -13,7 +13,7 @@ GNS3 GUI repository.
Installation
------------
https://gns3.com/support/docs
Please see https://docs.gns3.com/
Development
-------------

View File

@@ -63,14 +63,14 @@ class ApplianceManager(QtCore.QObject):
def _listAppliancesCallback(self, result, error=False, **kwargs):
if error is True:
log.error("Error while getting appliances list: {}".format(result["message"]))
log.error("Error while getting appliances list: {}".format(result.get("message", "unknown")))
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"]))
log.error("Error while getting appliance templates list: {}".format(result.get("message", "unknown")))
return
self._appliance_templates = result
self.appliances_changed_signal.emit()
@@ -80,8 +80,15 @@ class ApplianceManager(QtCore.QObject):
if appliance["appliance_id"] == appliance_id:
break
project_id = project.id()
if not self._controller.connected():
log.error("Cannot create node: not connected to any controller server")
return
if not project or not project.id():
log.error("Cannot create node: please create a project first!")
return
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"])
@@ -92,12 +99,14 @@ class ApplianceManager(QtCore.QObject):
"x": int(x),
"y": int(y)
},
showProgress=False,
timeout=None)
else:
self._controller.post("/projects/" + project_id + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
"x": int(x),
"y": int(y)
},
showProgress=False,
timeout=None)
return True

View File

@@ -336,13 +336,16 @@ class BaseNode(QtCore.QObject):
"""
if not hasattr(self, "configFiles"):
return
return False
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)
return True
def _exportConfigToDirectoryCallback(self, result, error=False, raw_body=None, context={}, **kwargs):
"""
Callback for exportConfigToDirectory.
@@ -352,8 +355,7 @@ class BaseNode(QtCore.QObject):
"""
if error:
# The file could be missing if you have not private config for
# exemple
# The file could be missing if you have not private config for example
return
export_directory = context["directory"]

View File

@@ -36,6 +36,7 @@ class Compute:
self._password = None
self._cpu_usage_percent = None
self._memory_usage_percent = None
self._last_error = None
self._capabilities = {
"node_types": []
}
@@ -97,6 +98,12 @@ class Compute:
def capabilities(self):
return self._capabilities
def setLastError(self, last_error):
self._last_error = last_error
def lastError(self):
return self._last_error
def setCapabilities(self, val):
self._capabilities = val

View File

@@ -55,7 +55,7 @@ class ComputeManager(QtCore.QObject):
def _refreshComputesSlot(self):
if self._refreshingComputes:
return
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 5:
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 1:
self._last_computes_refresh = datetime.datetime.now().timestamp()
self._refreshingComputes = True
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
@@ -84,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
@@ -101,6 +102,7 @@ class ComputeManager(QtCore.QObject):
self._computes[compute_id].setCpuUsagePercent(compute["cpu_usage_percent"])
self._computes[compute_id].setMemoryUsagePercent(compute["memory_usage_percent"])
self._computes[compute_id].setCapabilities(compute["capabilities"])
self._computes[compute_id].setLastError(compute.get("last_error"))
if new_node:
self.created_signal.emit(compute_id)

View File

@@ -62,19 +62,26 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
text = "{} CPU {}%, RAM {}%".format(text, self._compute.cpuUsagePercent(), self._compute.memoryUsagePercent())
self.setText(0, text)
self.setToolTip(0, text + " on " + self._compute.capabilities().get("platform", ""))
if self._compute.connected():
self._status = "connected"
self.setToolTip(0, "Server {} version {} running on {}".format(self._compute.name(),
self._compute.capabilities().get("version", "n/a"),
self._compute.capabilities().get("platform", "")))
if usage is None or (self._compute.cpuUsagePercent() < 90 and self._compute.memoryUsagePercent() < 90):
self.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
else:
self.setIcon(0, QtGui.QIcon(':/icons/led_yellow.svg'))
else:
if self._status == "unknown":
last_error = self._compute.lastError()
if last_error:
self.setToolTip(0, "Failed to connect to {}: {}".format(self._compute.name(), last_error))
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
elif self._status == "unknown":
self.setToolTip(0, "Discovering or connecting to {}...".format(self._compute.name()))
self.setIcon(0, QtGui.QIcon(':/icons/led_gray.svg'))
else:
self._status = "stopped"
self.setToolTip(0, "{} is stopped or cannot be reached".format(self._compute.name()))
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
self._parent.sortItems(0, QtCore.Qt.AscendingOrder)

View File

@@ -22,7 +22,7 @@ Handles commands typed in the GNS3 console.
import sys
import cmd
import struct
import sip
from .qt import sip
import json
from .node import Node

View File

@@ -16,13 +16,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import sip
from .qt import sip
import struct
import inspect
import datetime
import platform
from .qt import QtCore, Qt
from .qt import QtCore
from .topology import Topology
from .version import __version__
from .console_cmd import ConsoleCmd
@@ -75,7 +75,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {} and PyQt {}.\n" \
"Copyright (c) 2006-{} GNS3 Technologies.\n" \
"Use Help -> GNS3 Doctor to detect common issues." \
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, Qt.PYQT_VERSION_STR, current_year)
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, QtCore.PYQT_VERSION_STR, current_year)
# Parent class initialization
try:

View File

@@ -41,6 +41,7 @@ class Controller(QtCore.QObject):
super().__init__()
self._connected = False
self._connecting = False
self._version = None
self._cache_directory = tempfile.mkdtemp()
self._http_client = None
# If it's the first error we display an alert box to the user
@@ -55,6 +56,9 @@ class Controller(QtCore.QObject):
def host(self):
return self._http_client.host()
def version(self):
return self._version
def isRemote(self):
"""
:returns Boolean: True if the controller is remote
@@ -121,7 +125,7 @@ class Controller(QtCore.QObject):
def _versionGetSlot(self, result, error=False, **kwargs):
"""
Called after the inital version get
Called after the initial version get
"""
if error:
if self._first_error:
@@ -142,6 +146,7 @@ class Controller(QtCore.QObject):
if self._error_dialog:
self._error_dialog.reject()
self._error_dialog = None
self._version = result.get("version")
def _httpClientConnectedSlot(self):
if not self._connected:
@@ -208,9 +213,6 @@ class Controller(QtCore.QObject):
if self._http_client:
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
def getSynchronous(self, endpoint, timeout=2):
return self._http_client.getSynchronous(endpoint, timeout)
def connectWebSocket(self, path, *args):
return self._http_client.connectWebSocket(path)
@@ -237,13 +239,9 @@ class Controller(QtCore.QObject):
if not self._http_client:
return
m = hashlib.md5()
m.update(url.encode())
if ".svg" in url:
extension = ".svg"
else:
extension = ".png"
path = os.path.join(self._cache_directory, m.hexdigest() + extension)
path = self.getStaticCachedPath(url)
if os.path.exists(path):
callback(path)
elif path in self._static_asset_download_queue:
@@ -263,8 +261,7 @@ class Controller(QtCore.QObject):
self.getStatic(fallback, callback)
fallback_used = True
if fallback_used:
log.error("Error while downloading file: {}".format(url))
log.error("Error while downloading file: {}".format(url))
log.debug("Error while downloading file: {}".format(url))
del self._static_asset_download_queue[path]
return
try:
@@ -278,6 +275,21 @@ class Controller(QtCore.QObject):
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, m.hexdigest() + extension)
return path
def getSymbolIcon(self, symbol_id, callback, fallback=None):
"""
Get a QIcon for a symbol from the controller
@@ -298,6 +310,9 @@ class Controller(QtCore.QObject):
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))

View File

@@ -51,7 +51,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "sync+https://c66a939550054db9a330a82c26a2e47a:ffd37ed644064b638526f48d81b2201e@sentry.io/38506"
DSN = "https://84ef3f39811242728d4b151f4d5ca5a4:eb9df95eb15f4672a4abb17ddb8322fe@sentry.io/38506"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):
@@ -71,6 +71,8 @@ class CrashReport:
def captureException(self, exception, value, tb):
from .local_server import LocalServer
from .local_config import LocalConfig
from .controller import Controller
from .compute_manager import ComputeManager
local_server = LocalServer.instance().localServerSettings()
if local_server["report_errors"]:
@@ -102,10 +104,24 @@ class CrashReport:
sys.version_info[2]),
"python:bit": struct.calcsize("P") * 8,
"python:encoding": sys.getdefaultencoding(),
"python:frozen": "{}".format(hasattr(sys, "frozen"))
"python:frozen": "{}".format(hasattr(sys, "frozen")),
}
# extra controller and compute information
extra_context = {"controller:version": Controller.instance().version(),
"controller:host": Controller.instance().host(),
"controller:connected": Controller.instance().connected()}
for index, compute in enumerate(ComputeManager.instance().computes()):
extra_context["compute{}:id".format(index)] = compute.id()
extra_context["compute{}:name".format(index)] = compute.name(),
extra_context["compute{}:host".format(index)] = compute.host(),
extra_context["compute{}:connected".format(index)] = compute.connected()
extra_context["compute{}:platform".format(index)] = compute.capabilities().get("platform")
extra_context["compute{}:version".format(index)] = compute.capabilities().get("version")
context = self._add_qt_information(context)
client.tags_context(context)
client.extra_context(extra_context)
try:
report = client.captureException((exception, value, tb))
except Exception as e:
@@ -116,7 +132,7 @@ class CrashReport:
def _add_qt_information(self, context):
try:
from .qt import QtCore
import sip
from .qt import sip
except ImportError:
return context
context["psutil:version"] = psutil.__version__

View File

@@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sip
from ..qt import sip
import shutil
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
@@ -34,6 +34,9 @@ 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()
@@ -41,6 +44,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
def __init__(self, parent, path):
super().__init__(parent)
self.setupUi(self)
self.images_changed_signal.connect(self._refreshVersions)
self.versions_changed_signal.connect(self._versionRefreshedSlot)
@@ -76,16 +80,27 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiLocalRadioButton.setText("Install the appliance on the main server")
else:
if not path.endswith('.builtin.gns3a'):
destination = Config().appliances_dir
destination = None
try:
os.makedirs(destination, exist_ok=True)
destination = os.path.join(destination, os.path.basename(path))
shutil.copy(path, destination)
destination = Config().appliances_dir
except OSError as e:
QtWidgets.QMessageBox.warning(self.parent(), "Can't copy {} to {}".format(path, destination), str(e))
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", "Could not find configuration file: {}".format(e))
except ValueError as e:
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", "Invalid configuration file: {}".format(e))
if destination:
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(), "Cannot 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.
@@ -110,6 +125,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"])
@@ -316,9 +333,15 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiApplianceVersionTreeWidget.resizeColumnToContents(1)
self._refreshing = False
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
Scan local directory in order to find the images on disk
"""
# Docker do not have versions
@@ -415,15 +438,16 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
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
reply = QtWidgets.QMessageBox.question(self, "Add appliance",
"This is not the correct file. The MD5 sum is {} and should be {}.\nDo you want to accept it at your own risks?".format(image.md5sum, disk["md5sum"]),
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return
except OSError as e:
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
return
image_upload_manger = ImageUploadManager(
image, Controller.instance(), self._compute_id,
self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
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):
@@ -459,12 +483,15 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
try:
config = Config()
except OSError as e:
except (OSError, ValueError) as e:
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
return False
if version is None:
appliance_configuration = self._appliance.copy()
if not "docker" in appliance_configuration:
# only Docker do not have version
return False
else:
try:
appliance_configuration = self._appliance.search_images_for_version(version)
@@ -482,7 +509,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if "qemu" in appliance_configuration:
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, self._compute_id), 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_():
@@ -541,8 +571,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
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"]))
try:
self._appliance.search_images_for_version(version["name"])
except ApplianceError as e:
QtWidgets.QMessageBox.critical(self, "Appliance", "Cannot install {} version {}: {}".format(appliance["name"], version["name"], e))
return False
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {} version {}?".format(appliance["name"], version["name"]),
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
@@ -552,7 +584,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
elif self.currentPage() == self.uiUsageWizardPage:
if self._image_uploading_count > 0:
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for image uploading")
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for all images to be uploaded...")
return False
current = self.uiApplianceVersionTreeWidget.currentItem()

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

View File

@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from ..qt import QtWidgets
from ..qt import QtWidgets, QtCore, qslot, qpartial
from ..topology import Topology
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog
@@ -36,6 +36,68 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
self.uiProjectAutoStartCheckBox.setChecked(self._project.autoStart())
self.uiSceneWidthSpinBox.setValue(self._project.sceneWidth())
self.uiSceneHeightSpinBox.setValue(self._project.sceneHeight())
self.uiGridSizeSpinBox.setValue(self._project.gridSize())
self.uiGlobalVariablesGrid.setAlignment(QtCore.Qt.AlignTop)
self.uiNewVarButton = QtWidgets.QPushButton('Add new variable', self)
self.uiNewVarButton.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.uiNewVarButton.clicked.connect(self.onAddNewVariable)
self.uiGlobalVariablesGrid.addWidget(self.uiNewVarButton, 0, 3, QtCore.Qt.AlignRight)
self._variables = self.setUpVariables()
self.updateGlobalVariables()
def setUpVariables(self):
new_variable = {"name": "", "value": ""}
variables = self._project.variables()
if variables is not None:
variables.append(new_variable)
else:
variables = [new_variable]
return variables
def updateGlobalVariables(self):
while True:
item = self.uiGlobalVariablesGrid.takeAt(1)
if item is None:
break
elif item.widget():
item.widget().deleteLater()
for i, variable in enumerate(self._variables, start=1):
nameLabel = QtWidgets.QLabel()
nameLabel.setText("Name:")
self.uiGlobalVariablesGrid.addWidget(nameLabel, i, 0)
nameEdit = QtWidgets.QLineEdit()
nameEdit.setText(variable.get("name", ""))
nameEdit.textChanged.connect(qpartial(self.onNameChange, variable))
self.uiGlobalVariablesGrid.addWidget(nameEdit, i, 1)
valueLabel = QtWidgets.QLabel()
valueLabel.setText("Value:")
self.uiGlobalVariablesGrid.addWidget(valueLabel, i, 2)
valueEdit = QtWidgets.QLineEdit()
valueEdit.setText(variable.get("value", ""))
valueEdit.textChanged.connect(qpartial(self.onValueChange, variable))
self.uiGlobalVariablesGrid.addWidget(valueEdit, i, 3)
@qslot
def onAddNewVariable(self, event):
self._variables += [{"name": "", "value": ""}]
self.updateGlobalVariables()
def onNameChange(self, variable, text):
variable["name"] = text
def onValueChange(self, variable, text):
variable["value"] = text
def _cleanVariables(self):
return [v for v in self._variables if v.get("name", "").strip() != ""]
def done(self, result):
"""
@@ -51,5 +113,7 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
self._project.setGridSize(self.uiGridSizeSpinBox.value())
self._project.setVariables(self._cleanVariables())
self._project.update()
super().done(result)

View File

@@ -66,7 +66,7 @@ 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"))
self.uiFileTextEdit.setText(raw_body.decode("utf-8", errors="ignore"))
elif result.get("status") == 404:
if self._default:
self.uiFileTextEdit.setText(self._default)

View File

@@ -18,6 +18,9 @@
from ..qt import QtGui, QtWidgets, qslot
from ..ui.filter_dialog_ui import Ui_FilterDialog
import logging
log = logging.getLogger(__name__)
class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
@@ -30,6 +33,7 @@ class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
super().__init__(parent)
self.setupUi(self)
self._link = link
self._filters = {}
self._link.updated_link_signal.connect(self._updateUiSlot)
self._link.listAvailableFilters(self._listAvailableFiltersCallback)
self._initialized = False
@@ -40,7 +44,7 @@ class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
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"]))
log.warning("Error while listing information about the link: {}".format(result["message"]))
return
self._filters = result
self._initialized = True

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, qslot
from ..qt import QtCore, QtGui, QtWidgets, qslot, sip_is_deleted
from ..ui.project_dialog_ui import Ui_ProjectDialog
from ..controller import Controller
from ..topology import Topology
@@ -91,6 +91,8 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
projects_to_delete = set()
for project in self.uiProjectsTreeWidget.selectedItems():
if sip_is_deleted(project):
continue
project_id = project.data(0, QtCore.Qt.UserRole)
project_name = project.data(1, QtCore.Qt.UserRole)
@@ -106,6 +108,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
Controller.instance().deleteProject(project_id)
def _duplicateProjectSlot(self):
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
QtWidgets.QMessageBox.critical(self, "Duplicate project", "No project selected")
return
@@ -135,12 +138,16 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
if Controller.instance().isRemote():
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
self._duplicateCallback,
body={"name": name})
body={"name": name},
progressText="Duplicating project '{}'...".format(name),
timeout=None)
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})
body={"name": name, "path": project_location},
progressText="Duplicating project '{}'...".format(name),
timeout=None)
def _duplicateCallback(self, result, error=False, **kwargs):
if error:

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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 copy
from gns3.qt import QtWidgets, QtCore, qpartial
from gns3.ui.project_welcome_dialog_ui import Ui_ProjectWelcomeDialog
import logging
log = logging.getLogger(__name__)
class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
"""
This dialog shows when project is imported and global variables assigned to the project are missing.
"""
def __init__(self, parent, project):
super().__init__(parent)
self._project = project
self.setupUi(self)
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
self.gridLayout.setAlignment(QtCore.Qt.AlignTop)
self.label.setOpenExternalLinks(True)
self._variables = self._getVariables(project)
self._loadReadme()
self._addMisingVariablesEdits()
def _getVariables(self, project):
variables = copy.copy(self._project.variables())
if variables is None:
variables = []
return variables
def _addMisingVariablesEdits(self):
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
for i, variable in enumerate(missing, start=0):
nameLabel = QtWidgets.QLabel()
nameLabel.setText(variable.get("name", ""))
self.gridLayout.addWidget(nameLabel, i, 0)
valueEdit = QtWidgets.QLineEdit()
valueEdit.setText(variable.get("value", ""))
valueEdit.textChanged.connect(qpartial(self.onValueChange, variable))
self.gridLayout.addWidget(valueEdit, i, 1)
def _loadReadme(self):
self._project.get("/files/README.txt", self._loadedReadme)
def _loadedReadme(self, result, error=False, raw_body=None, context={}, **kwargs):
if not error:
self.label.setText(raw_body.decode("utf-8"))
def onValueChange(self, variable, text):
variable["value"] = text
def _okButtonClickedSlot(self):
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
if len(missing) > 0:
reply = QtWidgets.QMessageBox.warning(
self, 'Missing values',
'Are you sure you want to continue without providing missing values?',
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return
self._project.setVariables(self._variables)
self._project.update()
self.accept()

View File

@@ -86,9 +86,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
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")
@@ -116,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/621395/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):
@@ -263,7 +263,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
def _saveSettingsCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while save settings: {}".format(result["message"]))
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while saving settings: {}".format(result["message"]))
return
def _addSummaryEntry(self, name, value):

View File

@@ -22,8 +22,6 @@ Dialog to manage the snapshots.
from ..qt import QtCore, QtWidgets
from ..ui.snapshots_dialog_ui import Ui_SnapshotsDialog
from ..controller import Controller
from ..utils.progress_dialog import ProgressDialog
from ..utils.create_snapshot_worker import CreateSnapshotWorker
from datetime import datetime
@@ -87,21 +85,21 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
if ok and snapshot_name and self._project:
snapshot_worker = CreateSnapshotWorker(self._project, snapshot_name)
snapshot_worker.finished.connect(self._createSnapshotsCallback)
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()),
self._createSnapshotsCallback,
{"name": snapshot_name},
progressText="Creation of snapshot '{}' in progress...".format(snapshot_name),
timeout=None)
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):
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])
else:
log.error("Cannot create snapshot of project")
return
self._listSnapshots()
def _createSnapshotsErrorCallback(self, message, error):
log.error(message)
def _deleteSnapshotSlot(self):
"""
Slot to delete a snapshot.
@@ -113,6 +111,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
Controller.instance().delete("/projects/{}/snapshots/{}".format(self._project.id(), snapshot_id), self._deleteSnapshotsCallback)
def _deleteSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])
@@ -135,13 +134,16 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
:param snapshot_id: id of the snapshot
"""
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken, would you like to proceed?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
if reply == QtWidgets.QMessageBox.Cancel:
return
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback, timeout=300)
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id),
self._restoreSnapshotsCallback, progressText="Restoring snapshot...", timeout=None)
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])

View File

@@ -22,7 +22,7 @@ Dialog to change node symbols.
import os
import pathlib
from ..qt import QtCore, QtGui, QtWidgets, qpartial
from ..qt import QtCore, QtGui, QtWidgets, qpartial, sip_is_deleted
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from ..ui.symbol_selection_dialog_ui import Ui_SymbolSelectionDialog
from ..local_server import LocalServer
@@ -91,6 +91,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
item.setIcon(icon)
def render(item, path):
if sip_is_deleted(item):
return
svg_renderer = QImageSvgRenderer(path)
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
# Set the ARGB to 0 to prevent rendering artifacts
@@ -184,7 +186,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
if error:
log.error("Error while uploading symbol: {}".format(path))
log.error("Error while uploading symbol: {}: {}".format(path, result.get("message", "unknown")))
return
self.uiSymbolLineEdit.clear()
self.uiSymbolLineEdit.setText(path)

View File

@@ -19,7 +19,7 @@
Text editor to edit Note items.
"""
from ..qt import QtCore, QtWidgets, qslot
from ..qt import QtCore, QtWidgets, qslot, sip_is_deleted
from ..ui.text_editor_dialog_ui import Ui_TextEditorDialog
@@ -98,6 +98,8 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
"""
for item in self._items:
if sip_is_deleted(item):
continue
item.setFont(self.uiPlainTextEdit.font())
if self.uiApplyColorToAllItemsCheckBox.isChecked():
item.setDefaultTextColor(self._color)

View File

@@ -21,7 +21,7 @@ Graphical view on the scene where items are drawn.
import logging
import os
import sip
from .qt import sip
import sys
from .qt import QtCore, QtGui, QtNetwork, QtWidgets, qpartial, qslot
@@ -60,6 +60,7 @@ from .items.rectangle_item import RectangleItem
from .items.line_item import LineItem
from .items.ellipse_item import EllipseItem
from .items.image_item import ImageItem
from .items.logo_item import LogoItem
log = logging.getLogger(__name__)
@@ -88,6 +89,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._adding_line = False
self._newlink = None
self._dragging = False
self._grid_size = 75
self._last_mouse_position = None
self._topology = Topology.instance()
self._background_warning_msgbox = QtWidgets.QErrorMessage(self)
@@ -127,6 +129,24 @@ class GraphicsView(QtWidgets.QGraphicsView):
factor = zoom / 100.
self.scale(factor, factor)
def setGridSize(self, grid_size):
"""
Sets the grid size.
:param grid_size: integer
"""
self._grid_size = grid_size
def gridSize(self):
"""
Returns the grid size
:returns: integer
"""
return self._grid_size
def setEnabled(self, enabled):
if enabled is False:
@@ -275,6 +295,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
self.scene().addItem(image_item)
self._topology.addDrawing(image_item)
def addLogo(self, logo_path, logo_url):
logo_item = LogoItem(logo_path, logo_url, self._topology.project())
self.scene().addItem(logo_item)
def addLink(self, source_node, source_port, destination_node, destination_port, **link_data):
"""
Creates a Link instance representing a connection between 2 devices.
@@ -395,12 +419,16 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
is_not_link = True
is_not_logo = True
item = self.itemAt(event.pos())
if item and sip.isdeleted(item):
return
if item and (isinstance(item, LinkItem) or isinstance(item.parentItem(), LinkItem)):
is_not_link = False
if item and (isinstance(item, LogoItem) or isinstance(item.parentItem(), LogoItem)):
is_not_logo = False
else:
for it in self.scene().items():
if isinstance(it, LinkItem):
@@ -420,7 +448,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
item.setSelected(False)
else:
item.setSelected(True)
elif is_not_link and event.button() == QtCore.Qt.RightButton and not self._adding_link:
elif is_not_link and is_not_logo and event.button() == QtCore.Qt.RightButton and not self._adding_link:
if item and not sip.isdeleted(item):
# Prevent right clicking on a selected item from de-selecting all other items
if not item.isSelected():
@@ -1546,22 +1574,22 @@ class GraphicsView(QtWidgets.QGraphicsView):
def drawBackground(self, painter, rect):
super().drawBackground(painter, rect)
if self._main_window.uiShowGridAction.isChecked():
gridSize = 75
if self._main_window.uiShowGridAction.isChecked() and self.gridSize():
grid_size = self.gridSize()
painter.save()
painter.setPen(QtGui.QPen(QtGui.QColor(190, 190, 190)))
left = int(rect.left()) - (int(rect.left()) % gridSize)
top = int(rect.top()) - (int(rect.top()) % gridSize)
left = int(rect.left()) - (int(rect.left()) % grid_size)
top = int(rect.top()) - (int(rect.top()) % grid_size)
x = left
while x < rect.right():
painter.drawLine(x, rect.top(), x, rect.bottom())
x += gridSize
x += grid_size
y = top
while y < rect.bottom():
painter.drawLine(rect.left(), y, rect.right(), y)
y += gridSize
y += grid_size
painter.restore()
def toggleUiDeviceMenu(self):

View File

@@ -15,7 +15,7 @@
# 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
from .qt import sip
import json
import copy
import http
@@ -47,7 +47,7 @@ class HTTPClient(QtCore.QObject):
"""
HTTP client.
:param settings: Dictionnary with connection information to the server
:param settings: Dictionary with connection information to the server
:param network_manager: A QT network manager
"""
@@ -209,16 +209,16 @@ class HTTPClient(QtCore.QObject):
Called when a query upload progress
"""
if not sip_is_deleted(HTTPClient._progress_callback):
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(total))
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
def _notify_progress_download(self, query_id, sent, total):
"""
Called when a query download progress
"""
if not sip_is_deleted(HTTPClient._progress_callback):
# abs() for maxium because sometimes the system send negative
# abs() for maximum because sometimes the system send negative
# values
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(abs(total)))
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
@classmethod
def setProgressCallback(cls, progress_callback):
@@ -303,16 +303,17 @@ class HTTPClient(QtCore.QObject):
if self._shutdown:
return
# TODO: clean this
# 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
# 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,
@@ -391,21 +392,22 @@ class HTTPClient(QtCore.QObject):
return
if params["version"].split("-")[0] != __version__.split("-")[0]:
msg = "Client version {} differs with server version {}".format(__version__, params["version"])
log.error(msg)
msg = "Client version {} is not the same as server (controller) 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 upgrade or use at your own risk.".format(msg))
self._connected = True
self._retry = 0
@@ -490,7 +492,7 @@ class HTTPClient(QtCore.QObject):
def _paramsToQueryString(self, params):
"""
:param params: Dictionnary of query string parameters
:param params: Dictionary of query string parameters
:returns: String of the query string
"""
if params == {}:
@@ -618,7 +620,7 @@ class HTTPClient(QtCore.QObject):
# We check if we received HTTP headers
if not sip.isdeleted(response) and response.isRunning() and not len(response.rawHeaderList()) > 0:
if not response.error() != QtNetwork.QNetworkReply.NoError:
log.warning("Timeout after {} seconds for request {}".format(timeout, response.url().toString()))
log.warning("Timeout after {} seconds for request {}. Please check the connection is not blocked by a firewall or an anti-virus.".format(timeout, response.url().toString()))
response.abort()
def disconnect(self):
@@ -631,14 +633,14 @@ class HTTPClient(QtCore.QObject):
def _requestCanceled(self, response, context):
if response.isRunning() and not response.error() != QtNetwork.QNetworkReply.NoError:
log.warn("Aborting request for {}".format(response.url().toString()))
log.warning("Aborting request for {}".format(response.url().toString()))
response.abort()
if "query_id" in context:
self._notify_progress_end_query(context["query_id"])
def _processError(self, response, server, callback, context, request_body, ignore_errors, error_code):
if error_code != QtNetwork.QNetworkReply.NoError:
error_message = response.errorString()
error_message = "{} ({}:{})".format(response.errorString(), self._host, self._port)
if not ignore_errors:
log.debug("Response error: %s for %s (error: %d)", error_message, response.url().toString(), error_code)
@@ -648,7 +650,10 @@ class HTTPClient(QtCore.QObject):
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
error_message = "Operation timeout" # It's clearer than cancel because cancel is triggered by us when we timeout
elif error_code == QtNetwork.QNetworkReply.NetworkSessionFailedError:
# ignore the network session failed error to let the network manager recover from it
return
elif not ignore_errors:
self.disconnect()
if callback is not None:
@@ -722,46 +727,54 @@ class HTTPClient(QtCore.QObject):
e = HttpBadRequest(body)
raise e
def getSynchronous(self, endpoint, timeout=2):
def getSynchronous(self, method, endpoint, prefix="/v2", timeout=2):
"""
Synchronous check if a server is running
:returns: Tuple (Status code, json of anwser). Status 0 is a non HTTP error
:returns: Tuple (Status code, json of answer). Status 0 is a non HTTP error
"""
try:
url = "{protocol}://{host}:{port}/v2/{endpoint}".format(protocol=self._protocol, host=self._host, port=self._port, endpoint=endpoint)
if self._user is not None and len(self._user) > 0:
log.debug("Synchronous get {} with user '{}'".format(url, self._user))
auth_handler = urllib.request.HTTPBasicAuthHandler()
auth_handler.add_password(realm="GNS3 server",
uri=url,
user=self._user,
passwd=self._password)
opener = urllib.request.build_opener(auth_handler)
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:
host = self._getHostForQuery()
log.debug("{method} {protocol}://{host}:{port}{prefix}{endpoint}".format(method=method, protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
if self._user:
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, user=self._user, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
else:
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
request = self._request(url)
request = self._addAuth(request)
request.setRawHeader(b"User-Agent", "GNS3 QT Client v{version}".format(version=__version__).encode())
try:
response = self._network_manager.sendCustomRequest(request, method.encode())
except SystemError as e:
log.error("Can't send query: {}".format(str(e)))
return
loop = QtCore.QEventLoop()
response.finished.connect(loop.quit)
if timeout is not None:
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response, timeout))
if not loop.isRunning():
loop.exec_()
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
if response.error() != QtNetwork.QNetworkReply.NoError:
log.debug("Error while connecting to local server {}".format(response.errorString()))
return status, None
else:
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
if status == 200:
if content_type == "application/json":
content = response.read()
content = bytes(response.readAll())
json_data = json.loads(content.decode("utf-8"))
return response.status, json_data
return status, json_data
else:
return response.status, None
except http.client.InvalidURL as e:
log.warn("Invalid local server url: {}".format(e))
return 0, None
except urllib.error.URLError:
# Connection refused. It's a normal behavior if server is not started
return 0, None
except urllib.error.HTTPError as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return e.code, None
except (OSError, http.client.BadStatusLine, ValueError) as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return status, None
return 0, None
@classmethod

View File

@@ -43,6 +43,7 @@ class DrawingItem:
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, rotation=0, **kws):
self._id = drawing_id
self._deleting = False
if self._id is None:
self._id = str(uuid.uuid4())
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
@@ -81,13 +82,13 @@ class DrawingItem:
"""
if error:
log.error("Error while setting up drawing: {}".format(result["message"]))
log.error("Error while creating drawing: {}".format(result["message"]))
return False
self._id = result["drawing_id"]
self.updateDrawingCallback(result)
def updateDrawing(self):
if self._id:
if self._id and not self.deleting() and self._project:
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__(), showProgress=False)
@qslot
@@ -101,7 +102,7 @@ class DrawingItem:
"""
if error:
log.error("Error while setting up drawing: {}".format(result["message"]))
log.error("Error while updating drawing: {}".format(result["message"]))
return False
self.setPos(QtCore.QPoint(result["x"], result["y"]))
self.setZValue(result["z"])
@@ -173,6 +174,20 @@ class DrawingItem:
self.setFlag(self.ItemIsSelectable, True)
self.setFlag(self.ItemIsMovable, True)
def deleting(self):
"""
Is the link being deleted
"""
return self._deleting
def setDeleting(self):
"""
Mark this drawing as being deleted
"""
self._deleting = True
def delete(self, skip_controller=False):
"""
Deletes this drawing.
@@ -180,6 +195,7 @@ class DrawingItem:
:param skip_controller: Do not replicate change on the controller (usefull when it's already deleted on controller)
"""
self.setDeleting()
self.scene().removeItem(self)
from ..topology import Topology
Topology.instance().removeDrawing(self)
@@ -188,11 +204,11 @@ class DrawingItem:
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
GRID_SIZE = 75
grid_size = self._graphics_view.gridSize()
mid_x = self.boundingRect().width() / 2
tmp_x = (GRID_SIZE * round((self.x() + mid_x) / GRID_SIZE)) - mid_x
tmp_x = (grid_size * round((self.x() + mid_x) / grid_size)) - mid_x
mid_y = self.boundingRect().height() / 2
tmp_y = (GRID_SIZE * round((self.y() + mid_y) / GRID_SIZE)) - mid_y
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)

View File

@@ -106,7 +106,7 @@ class EthernetLinkItem(LinkItem):
"""
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
if not self._adding_flag and self._settings["draw_link_status_points"]:
if not self._adding_flag:
# points disappears if nodes are too close to each others.
if self.length < 100:
@@ -151,7 +151,8 @@ class EthernetLinkItem(LinkItem):
else:
source_port_label.hide()
painter.drawPoint(point1)
if self._settings["draw_link_status_points"]:
painter.drawPoint(point1)
if self._link.suspended() or self._destination_port.status() == Port.suspended:
# link or port is suspended
@@ -192,6 +193,7 @@ class EthernetLinkItem(LinkItem):
else:
destination_port_label.hide()
painter.drawPoint(point2)
if self._settings["draw_link_status_points"]:
painter.drawPoint(point2)
self._drawSymbol()

View File

@@ -21,7 +21,7 @@ Link items are graphical representation of a link on the QGraphicsScene
"""
import math
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot, sip_is_deleted
from ..packet_capture import PacketCapture
from ..dialogs.filter_dialog import FilterDialog
@@ -286,14 +286,15 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
QtWidgets.QApplication.sendEvent(MainWindow.instance(), key)
return
# create the contextual menu
self.setAcceptHoverEvents(False)
menu = QtWidgets.QMenu()
self.populateLinkContextualMenu(menu)
menu.exec_(QtGui.QCursor.pos())
self.setAcceptHoverEvents(True)
self._hovered = False
self.adjust()
if not sip_is_deleted(self):
# create the contextual menu
self.setAcceptHoverEvents(False)
menu = QtWidgets.QMenu()
self.populateLinkContextualMenu(menu)
menu.exec_(QtGui.QCursor.pos())
self.setAcceptHoverEvents(True)
self._hovered = False
self.adjust()
def keyPressEvent(self, event):
"""

136
gns3/items/logo_item.py Normal file
View File

@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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 urllib.parse
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from ..controller import Controller
import logging
log = logging.getLogger(__name__)
class LogoItem(QtSvg.QGraphicsSvgItem):
"""
Margin for the logo
"""
MARGIN = 20
"""
Logo for the scene.
:param logo_path: Path to the logo (remote)
:param logo_url: URL which needs to be open user clicks on the logo
:param project: Current project
"""
def __init__(self, logo_path, logo_url, project):
super().__init__()
self._logo_path = logo_path
self._logo_url = logo_url
self._project = project
# Temporary symbol during loading
renderer = QImageSvgRenderer(":/icons/reload.svg")
renderer.setObjectName("symbol_loading")
self.setSharedRenderer(renderer)
effect = QtWidgets.QGraphicsColorizeEffect()
effect.setColor(QtGui.QColor("black"))
effect.setStrength(0.8)
self.setGraphicsEffect(effect)
self.graphicsEffect().setEnabled(False)
# set graphical settings for this item
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)
from ..main_window import MainWindow
self._main_window = MainWindow.instance()
self._settings = self._main_window.uiGraphicsView.settings()
self.updatePosition()
self._main_window.uiGraphicsView.viewport().installEventFilter(self)
remote_file = urllib.parse.quote('project-files/images/{}'.format(logo_path))
Controller.instance().getStatic(
'/projects/{}/files/{}'.format(project.id(), remote_file),
self.updateImage
)
# make it the last one
self.setZValue(-2)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.Paint:
self.updatePosition()
return QtWidgets.QWidget.eventFilter(self, source, event)
def updateImage(self, local_path):
renderer = QImageSvgRenderer(local_path)
renderer.setObjectName("project_logo")
self.setSharedRenderer(renderer)
def updatePosition(self):
"""
Updates position to be located in the right bottom corner
"""
logo_rect = self.boundingRect()
width = self._main_window.uiGraphicsView.viewport().width()
height = self._main_window.uiGraphicsView.viewport().height()
rect = self._main_window.uiGraphicsView.mapToScene(QtCore.QRect(0, 0, width, height)).boundingRect()
x = rect.x() + rect.width() - self.MARGIN - logo_rect.width()
y = rect.y() + rect.height() - self.MARGIN - logo_rect.height()
# update only when changes
if [int(self.x()), int(self.y())] != [int(x), int(y)]:
self.setX(x)
self.setY(y)
self.update()
def hoverEnterEvent(self, event):
"""
Handles all hover enter events for this item.
:param event: QGraphicsSceneHoverEvent instance
"""
if self._logo_url is not None:
self.graphicsEffect().setEnabled(True)
def hoverLeaveEvent(self, event):
"""
Handles all hover leave events for this item.
:param event: QGraphicsSceneHoverEvent instance
"""
self.graphicsEffect().setEnabled(False)
def mousePressEvent(self, event):
url = QtCore.QUrl(self._logo_url)
if not QtGui.QDesktopServices.openUrl(url):
QtWidgets.QMessageBox.warning(self, 'Open Url', 'Could not open url')

View File

@@ -19,7 +19,7 @@
Graphical representation of a node on the QGraphicsScene.
"""
import sip
from ..qt import sip
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
from ..qt.qimage_svg_renderer import QImageSvgRenderer
@@ -41,7 +41,6 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
"""
show_layer = False
GRID_SIZE = 75
def __init__(self, node):
super().__init__()
@@ -100,26 +99,16 @@ 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 _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):
@@ -144,9 +133,14 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
@qslot
def _symbolLoadedCallback(self, path, *args):
renderer = QImageSvgRenderer(path, fallback=":/icons/cancel.svg")
renderer.setObjectName(path)
self.setSharedRenderer(renderer)
if self._settings["limit_size_node_symbols"] is True and renderer.defaultSize().height() > 80:
# resize the SVG
renderer.resize(80)
self.setSharedRenderer(renderer)
if self._node.settings().get("symbol") != self._symbol:
self.updateNode()
if not self._initialized:
@@ -458,10 +452,11 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
"""
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
grid_size = self._main_window.uiGraphicsView.gridSize()
mid_x = self.boundingRect().width() / 2
value.setX((self.GRID_SIZE * round((value.x() + mid_x) / self.GRID_SIZE)) - mid_x)
value.setX((grid_size * round((value.x() + mid_x) / grid_size)) - mid_x)
mid_y = self.boundingRect().height() / 2
value.setY((self.GRID_SIZE * round((value.y() + mid_y) / self.GRID_SIZE)) - mid_y)
value.setY((grid_size * round((value.y() + mid_y) / grid_size)) - mid_y)
# dynamically change the renderer when this node item is selected/unselected.
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:

View File

@@ -107,7 +107,7 @@ class SerialLinkItem(LinkItem):
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
if not self._adding_flag and self._settings["draw_link_status_points"]:
if not self._adding_flag:
# points disappears if nodes are too close to each others.
if self.length < 80:
@@ -140,7 +140,8 @@ class SerialLinkItem(LinkItem):
else:
source_port_label.hide()
painter.drawPoint(self.source_point)
if self._settings["draw_link_status_points"]:
painter.drawPoint(self.source_point)
# destination point color
if self._link.suspended() or self._destination_port.status() == Port.suspended:
@@ -170,6 +171,7 @@ class SerialLinkItem(LinkItem):
else:
destination_port_label.hide()
painter.drawPoint(self.destination_point)
if self._settings["draw_link_status_points"]:
painter.drawPoint(self.destination_point)
self._drawSymbol()

View File

@@ -21,10 +21,10 @@ Manages and stores everything needed for a connection between 2 devices.
import os
import re
import sip
from .qt import sip
import uuid
from .qt import QtCore, QtWidgets
from .qt import QtCore
from .controller import Controller
@@ -76,6 +76,7 @@ class Link(QtCore.QObject):
self._destination_label = None
self._link_id = link_id
self._capturing = False
self._deleting = False
self._capture_file_path = None
self._capture_file = None
self._initialized = False
@@ -158,7 +159,7 @@ class Link(QtCore.QObject):
self._updateLabels()
def update(self):
if not self._link_id:
if not self._link_id or self.deleting():
return
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)
@@ -171,7 +172,7 @@ class Link(QtCore.QObject):
def updateLinkCallback(self, result, error=False, *args, **kwargs):
if error:
QtWidgets.QMessageBox.warning(None, "Update link", "Error while updating link: {}".format(result["message"]))
log.warning("Error while updating link: {}".format(result["message"]))
return
self._parseResponse(result)
@@ -221,7 +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"]))
log.warning("Error while creating link: {}".format(result["message"]))
self.deleteLink(skip_controller=True)
return
@@ -244,6 +245,19 @@ class Link(QtCore.QObject):
def link_id(self):
return self._link_id
def deleting(self):
"""
Is the link being deleted
"""
return self._deleting
def setDeleting(self):
"""
Mark this link as being deleted
"""
self._deleting = True
def capturing(self):
"""
Is a capture running on the link?
@@ -306,8 +320,10 @@ class Link(QtCore.QObject):
if skip_controller:
self._linkDeletedCallback({})
else:
self.setDeleting()
Controller.instance().delete("/projects/{project_id}/links/{link_id}".format(project_id=self.project().id(),
link_id=self._link_id), self._linkDeletedCallback)
link_id=self._link_id),
self._linkDeletedCallback)
def _linkDeletedCallback(self, result, error=False, **kwargs):
"""

View File

@@ -216,9 +216,12 @@ 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]:
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()))
app = QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
error_message = "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())
QtWidgets.QMessageBox.critical(False, "Version error", error_message)
# Exit immediately not clean but we want to avoid any side effect that could corrupt the file
QtCore.QTimer.singleShot(0, app.quit)
app.exec_()
sys.exit(1)
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.0alpha1"):
@@ -265,11 +268,13 @@ class LocalConfig(QtCore.QObject):
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") as f:
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.warn("Can't import IOU licence {}: {}".format(self._settings["IOU"]["iourc_path"], str(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):
"""
@@ -317,7 +322,7 @@ class LocalConfig(QtCore.QObject):
"""
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", "Dynamips"]
section_to_save_on_controller = ["Builtin", "Docker", "IOU", "Qemu", "VMware", "VPCS", "TraceNG", "VirtualBox", "GraphicsView", "Dynamips"]
controller_settings = {}
for key, val in self._settings.items():
if key in section_to_save_on_controller:
@@ -475,6 +480,21 @@ class LocalConfig(QtCore.QObject):
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

@@ -36,7 +36,6 @@ from gns3.local_config import LocalConfig
from gns3.local_server_config import LocalServerConfig
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.http import getSynchronous
from gns3.utils.sudo import sudo
from gns3.http_client import HTTPClient
from gns3.controller import Controller
@@ -124,9 +123,13 @@ class LocalServer(QtCore.QObject):
return self._parent
def _checkWindowsService(self, service_name):
import pywintypes
import win32service
import win32serviceutil
try:
import pywintypes
import win32service
import win32serviceutil
except ImportError as e:
log.error("Could not check if the {} service is running: {}".format(service_name, e))
try:
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
@@ -136,6 +139,7 @@ class LocalServer(QtCore.QObject):
return False
else:
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
return True
def _checkUbridgePermissions(self):
@@ -367,15 +371,13 @@ 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 False
log.warning("The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
self._port = self._settings["port"]
# check the local server path
local_server_path = self.localServerPath()
if not local_server_path:
log.warn("No local server is configured")
log.warning("No local server is configured")
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))
@@ -512,9 +514,7 @@ class LocalServer(QtCore.QObject):
:returns: boolean
"""
status, json_data = getSynchronous(self._settings["protocol"], self._settings["host"], self._port, "version",
timeout=2, user=self._settings["user"], password=self._settings["password"])
status, json_data = HTTPClient(self._settings).getSynchronous("GET", "/version", timeout=2)
if status == 401: # Auth issue that need to be solved later
return True
elif json_data is None:

View File

@@ -145,7 +145,8 @@ def main():
frozen_dirs = [
frozen_dir,
os.path.normpath(os.path.join(frozen_dir, 'dynamips')),
os.path.normpath(os.path.join(frozen_dir, 'vpcs'))
os.path.normpath(os.path.join(frozen_dir, 'vpcs')),
os.path.normpath(os.path.join(frozen_dir, 'traceng'))
]
os.environ["PATH"] = os.pathsep.join(frozen_dirs) + os.pathsep + os.environ.get("PATH", "")
@@ -267,7 +268,10 @@ def main():
# issue when people run GNS3 from the .dmg
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
if not os.path.realpath(sys.executable).startswith("/Applications"):
QtWidgets.QMessageBox.critical(None, "Error", "You need to copy GNS3 in your /Applications folder before using it.")
error_message = "GNS3.app must be moved to the '/Applications' folder before it can be used"
QtWidgets.QMessageBox.critical(False, "Loading error", error_message)
QtCore.QTimer.singleShot(0, app.quit)
app.exec_()
sys.exit(1)
global mainwindow
@@ -291,7 +295,6 @@ def main():
mainwindow.show()
exit_code = app.exec_()
signal.signal(signal.SIGINT, orig_sigint)
signal.signal(signal.SIGTERM, orig_sigterm)
@@ -300,7 +303,7 @@ def main():
# We force deleting the app object otherwise it's segfault on Fedora
del app
# We force a full garbage collect before exit
# for unknow reason otherwise Qt Segfault on OSX in some
# for unknown reason otherwise Qt Segfault on OSX in some
# conditions
import gc
gc.collect()

View File

@@ -54,6 +54,7 @@ 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,6 +70,9 @@ 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
@@ -110,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()))
@@ -271,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.
@@ -303,6 +311,7 @@ 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():
@@ -491,6 +500,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
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
@@ -611,7 +631,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
for item in self.uiGraphicsView.scene().items():
if isinstance(item, LinkItem):
item.adjust()
def _updateZoomSettings(self, zoom=None):
"""
Updates zoom settings
@@ -1050,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")

View File

@@ -19,9 +19,11 @@ from gns3.modules.builtin import Builtin
from gns3.modules.dynamips import Dynamips
from gns3.modules.iou import IOU
from gns3.modules.vpcs import VPCS
from gns3.modules.traceng import TraceNG
from gns3.modules.virtualbox import VirtualBox
from gns3.modules.qemu import Qemu
from gns3.modules.vmware import VMware
from gns3.modules.docker import Docker
#FIXME: removed TraceNG
MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker]

View File

@@ -58,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):

View File

@@ -50,6 +50,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
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
@@ -74,6 +76,19 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
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)
try:
self._node.updated_signal.disconnect(self._refreshInterfaces)
except (TypeError, RuntimeError):
pass # was not connected
def _EthernetChangedSlot(self):
"""
Enables the use of the delete button.
@@ -121,6 +136,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.
@@ -199,6 +223,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.

View File

@@ -223,8 +223,8 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
for port_info in settings["ports_mapping"]:
item = TreeWidgetItem(self.uiPortsTreeWidget)
item.setText(0, str(port_info["port_number"]))
item.setText(1, str(port_info["vlan"]))
item.setText(2, port_info["type"])
item.setText(1, str(port_info.get("vlan", 1)))
item.setText(2, port_info.get("type", "access"))
item.setText(3, port_info.get("ethertype", ""))
self.uiPortsTreeWidget.addTopLevelItem(item)
self._ports[port_info["port_number"]] = port_info

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>821</width>
<height>363</height>
<width>1000</width>
<height>378</height>
</rect>
</property>
<property name="windowTitle">
@@ -57,7 +57,7 @@
</property>
</widget>
</item>
<item row="0" column="4">
<item row="0" column="5">
<widget class="QPushButton" name="uiDeleteEthernetPushButton">
<property name="enabled">
<bool>false</bool>
@@ -67,7 +67,7 @@
</property>
</widget>
</item>
<item row="1" column="0" colspan="5">
<item row="1" column="0" colspan="6">
<widget class="QListWidget" name="uiEthernetListWidget">
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
@@ -91,6 +91,13 @@
</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>
@@ -99,13 +106,14 @@
<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>
@@ -115,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>
@@ -142,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">
@@ -165,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,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
#
# Created by: PyQt5 UI code generator 5.9
# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
@@ -11,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_cloudConfigPageWidget(object):
def setupUi(self, cloudConfigPageWidget):
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
cloudConfigPageWidget.resize(821, 363)
cloudConfigPageWidget.resize(1000, 378)
self.verticalLayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
@@ -39,11 +39,11 @@ class Ui_cloudConfigPageWidget(object):
self.uiDeleteEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiDeleteEthernetPushButton.setEnabled(False)
self.uiDeleteEthernetPushButton.setObjectName("uiDeleteEthernetPushButton")
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 4, 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, 5)
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 6)
self.uiEthernetWarningPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiEthernetWarningPushButton.setText("")
self.uiEthernetWarningPushButton.setObjectName("uiEthernetWarningPushButton")
@@ -51,6 +51,9 @@ class Ui_cloudConfigPageWidget(object):
self.uiShowSpecialInterfacesCheckBox = QtWidgets.QCheckBox(self.EthernetTab)
self.uiShowSpecialInterfacesCheckBox.setObjectName("uiShowSpecialInterfacesCheckBox")
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_()
@@ -58,6 +61,7 @@ class Ui_cloudConfigPageWidget(object):
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")
@@ -66,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)
@@ -91,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")
@@ -241,11 +248,13 @@ class Ui_cloudConfigPageWidget(object):
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

@@ -135,6 +135,6 @@ class DockerVMWizard(VMWizard, Ui_DockerVMWizard):
"name": name,
"environment": self.uiEnvironmentTextEdit.toPlainText(),
"start_command": start_command,
"console_type": self.uiConsoleTypeComboBox.currentText()
"console_type": self.uiConsoleTypeComboBox.currentText(),
}
return settings

View File

@@ -49,7 +49,8 @@ class DockerVM(Node):
"console_type": DOCKER_CONTAINER_SETTINGS["console_type"],
"console_resolution": DOCKER_CONTAINER_SETTINGS["console_resolution"],
"console_http_port": DOCKER_CONTAINER_SETTINGS["console_http_port"],
"console_http_path": DOCKER_CONTAINER_SETTINGS["console_http_path"]}
"console_http_path": DOCKER_CONTAINER_SETTINGS["console_http_path"],
"extra_hosts": DOCKER_CONTAINER_SETTINGS["extra_hosts"]}
self.settings().update(docker_vm_settings)

View File

@@ -67,6 +67,7 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
self.uiConsoleResolutionComboBox.setCurrentIndex(self.uiConsoleResolutionComboBox.findText(settings["console_resolution"]))
self.uiConsoleHttpPortSpinBox.setValue(settings["console_http_port"])
self.uiHttpConsolePathLineEdit.setText(settings["console_http_path"])
self.uiExtraHostsTextEdit.setText(settings["extra_hosts"])
if not group:
self.uiNameLineEdit.setText(settings["name"])
@@ -128,6 +129,7 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
settings["console_resolution"] = self.uiConsoleResolutionComboBox.currentText()
settings["console_http_port"] = self.uiConsoleHttpPortSpinBox.value()
settings["console_http_path"] = self.uiHttpConsolePathLineEdit.text()
settings["extra_hosts"] = self.uiExtraHostsTextEdit.toPlainText()
if not group:
adapters = self.uiAdapterSpinBox.value()

View File

@@ -61,7 +61,6 @@ class DockerVMPreferencesPage(QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidge
return section_item
def _refreshInfo(self, docker_image):
self.uiDockerVMInfoTreeWidget.clear()
# fill out the General section
@@ -79,6 +78,9 @@ class DockerVMPreferencesPage(QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidge
if docker_image["environment"]:
QtWidgets.QTreeWidgetItem(section_item, ["Environment:", str(docker_image["environment"])])
if docker_image["extra_hosts"]:
QtWidgets.QTreeWidgetItem(section_item, ["Extra hosts:", str(docker_image["extra_hosts"])])
self.uiDockerVMInfoTreeWidget.expandAll()
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(0)
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(1)

View File

@@ -38,5 +38,6 @@ DOCKER_CONTAINER_SETTINGS = {
"console_type": "telnet",
"console_resolution": "1024x768",
"console_http_port": 80,
"console_http_path": "/"
"console_http_path": "/",
"extra_hosts": ""
}

View File

@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>613</width>
<height>519</height>
<height>524</height>
</rect>
</property>
<property name="windowTitle">
@@ -24,7 +24,16 @@
<string>General settings</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item row="0" column="1">
@@ -263,6 +272,45 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="advancedTab">
<attribute name="title">
<string>Advanced</string>
</attribute>
<widget class="QLabel" name="uiExtraHostsLabel">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>152</width>
<height>82</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Extra hosts added to
/etc/hosts file.
(hostname:IP, one per line)</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
<widget class="QTextEdit" name="uiExtraHostsTextEdit">
<property name="geometry">
<rect>
<x>168</x>
<y>10</y>
<width>413</width>
<height>82</height>
</rect>
</property>
</widget>
</widget>
</widget>
</item>
<item>

View File

@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/docker/ui/docker_vm_configuration_page.ui'
# Form implementation generated from reading ui file '/home/dominik/projects/gns3-gui/gns3/modules/docker/ui/docker_vm_configuration_page.ui'
#
# Created: Thu Jan 5 14:49:45 2017
# by: PyQt5 UI code generator 5.2.1
# Created by: PyQt5 UI code generator 5.8.2
#
# WARNING! All changes made in this file will be lost!
@@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_dockerVMConfigPageWidget(object):
def setupUi(self, dockerVMConfigPageWidget):
dockerVMConfigPageWidget.setObjectName("dockerVMConfigPageWidget")
dockerVMConfigPageWidget.resize(613, 519)
dockerVMConfigPageWidget.resize(613, 524)
self.verticalLayout = QtWidgets.QVBoxLayout(dockerVMConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(dockerVMConfigPageWidget)
@@ -122,6 +121,21 @@ class Ui_dockerVMConfigPageWidget(object):
self.uiHttpConsolePathLineEdit.setObjectName("uiHttpConsolePathLineEdit")
self.gridLayout.addWidget(self.uiHttpConsolePathLineEdit, 9, 1, 1, 1)
self.uiTabWidget.addTab(self.tab, "")
self.advancedTab = QtWidgets.QWidget()
self.advancedTab.setObjectName("advancedTab")
self.uiExtraHostsLabel = QtWidgets.QLabel(self.advancedTab)
self.uiExtraHostsLabel.setGeometry(QtCore.QRect(10, 10, 152, 82))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiExtraHostsLabel.sizePolicy().hasHeightForWidth())
self.uiExtraHostsLabel.setSizePolicy(sizePolicy)
self.uiExtraHostsLabel.setWordWrap(True)
self.uiExtraHostsLabel.setObjectName("uiExtraHostsLabel")
self.uiExtraHostsTextEdit = QtWidgets.QTextEdit(self.advancedTab)
self.uiExtraHostsTextEdit.setGeometry(QtCore.QRect(168, 10, 413, 82))
self.uiExtraHostsTextEdit.setObjectName("uiExtraHostsTextEdit")
self.uiTabWidget.addTab(self.advancedTab, "")
self.verticalLayout.addWidget(self.uiTabWidget)
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
@@ -164,4 +178,8 @@ class Ui_dockerVMConfigPageWidget(object):
self.uiConsoleResolutionLabel.setText(_translate("dockerVMConfigPageWidget", "VNC console resolution:"))
self.label_2.setText(_translate("dockerVMConfigPageWidget", "HTTP path:"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab), _translate("dockerVMConfigPageWidget", "General settings"))
self.uiExtraHostsLabel.setText(_translate("dockerVMConfigPageWidget", "Extra hosts added to \n"
"/etc/hosts file.\n"
"(hostname:IP, one per line)"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.advancedTab), _translate("dockerVMConfigPageWidget", "Advanced"))

View File

@@ -31,6 +31,9 @@ from gns3.node import Node
from ..ui.ios_router_configuration_page_ui import Ui_iosRouterConfigPageWidget
from ..settings import CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
import logging
log = logging.getLogger(__name__)
class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget):
@@ -323,14 +326,12 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
self.uiNPEComboBox.clear()
self.uiNPEComboBox.addItems(["npe-100", "npe-150", "npe-175", "npe-200", "npe-225", "npe-300", "npe-400", "npe-g2"])
if settings["midplane"]:
index = self.uiMidplaneComboBox.findText(settings["midplane"])
if index != -1:
self.uiMidplaneComboBox.setCurrentIndex(index)
if settings["npe"]:
index = self.uiNPEComboBox.findText(settings["npe"])
if index != -1:
self.uiNPEComboBox.setCurrentIndex(index)
index = self.uiMidplaneComboBox.findText(settings.get("midplane", "vxr"))
if index != -1:
self.uiMidplaneComboBox.setCurrentIndex(index)
index = self.uiNPEComboBox.findText(settings.get("npe", "npe-400"))
if index != -1:
self.uiNPEComboBox.setCurrentIndex(index)
if node:
# load the sensor settings
@@ -513,7 +514,7 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
if self._configFileValid(startup_config):
settings["startup_config"] = startup_config
else:
QtWidgets.QMessageBox.critical(self, "Startup-config", "Cannot read the startup-config file")
QtWidgets.QMessageBox.critical(self, "Startup-config", "Cannot access or read the startup-config file")
private_config = self.uiPrivateConfigLineEdit.text().strip()
if not private_config:
@@ -522,7 +523,7 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
if self._configFileValid(private_config):
settings["private_config"] = private_config
else:
QtWidgets.QMessageBox.critical(self, "Private-config", "Cannot read the private-config file")
QtWidgets.QMessageBox.critical(self, "Private-config", "Cannot access or read the private-config file")
symbol_path = self.uiSymbolLineEdit.text()
settings["symbol"] = symbol_path
@@ -620,7 +621,7 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
if node:
settings["wic" + str(wic_number)] = node.settings().get("wic" + str(wic_number))
if settings["wic" + str(wic_number)] and settings["wic" + str(wic_number)] != wic_name:
if settings.get("wic" + str(wic_number)) and settings["wic" + str(wic_number)] != wic_name:
if node:
self._checkForLinkConnectedToWIC(wic_number, settings, node)
settings["wic" + str(wic_number)] = wic_name
@@ -634,6 +635,13 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
"""
Return true if it's a valid configuration file
"""
if not os.path.isabs(path):
path = os.path.join(LocalServer.instance().localServerSettings()["configs_path"], path)
return os.access(path, os.R_OK)
result = os.access(path, os.R_OK)
if not result:
if not os.path.exists(path):
log.error("Cannot access config file '{}'".format(path))
else:
log.error("Cannot read config file '{}'".format(path))
return result

View File

@@ -294,8 +294,10 @@ class IOSRouterPreferencesPage(QtWidgets.QWidget, Ui_IOSRouterPreferencesPageWid
key = item.data(0, QtCore.Qt.UserRole)
ios_router = self._ios_routers[key]
path = ios_router["image"]
if not os.path.isabs(path):
path = os.path.join(self.getImageDirectory(), path)
if not os.path.isfile(path):
QtWidgets.QMessageBox.critical(self, "IOS image", "IOS image file {} is does not exist".format(path))
QtWidgets.QMessageBox.critical(self, "IOS image", "IOS image file {} does not exist".format(path))
return
try:
if not isIOSCompressed(path):

View File

@@ -77,7 +77,7 @@ class IOUDevice(Node):
return
log.debug("{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, progressText="{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, showProgress=False)
def update(self, new_settings):
"""

View File

@@ -30,6 +30,9 @@ from gns3.controller import Controller
from gns3.utils.get_resource import get_resource
from ..ui.iou_device_configuration_page_ui import Ui_iouDeviceConfigPageWidget
import logging
log = logging.getLogger(__name__)
class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget):
@@ -262,7 +265,7 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
if self._configFileValid(startup_config):
settings["startup_config"] = startup_config
else:
QtWidgets.QMessageBox.critical(self, "Startup-config", "Cannot read the startup-config file")
QtWidgets.QMessageBox.critical(self, "Startup-config", "Cannot access or read the startup-config file")
# save the private-config
private_config = self.uiPrivateConfigLineEdit.text().strip()
@@ -272,7 +275,7 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
if self._configFileValid(private_config):
settings["private_config"] = private_config
else:
QtWidgets.QMessageBox.critical(self, "Private-config", "Cannot read the private-config file")
QtWidgets.QMessageBox.critical(self, "Private-config", "Cannot access or read the private-config file")
settings["symbol"] = self.uiSymbolLineEdit.text()
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
@@ -306,6 +309,13 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
"""
Return true if it's a valid configuration file
"""
if not os.path.isabs(path):
path = os.path.join(LocalServer.instance().localServerSettings()["configs_path"], path)
return os.access(path, os.R_OK)
result = os.access(path, os.R_OK)
if not result:
if not os.path.exists(path):
log.error("Cannot access config file '{}'".format(path))
else:
log.error("Cannot read config file '{}'".format(path))
return result

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>569</width>
<height>503</height>
<width>767</width>
<height>685</height>
</rect>
</property>
<property name="windowTitle">
@@ -181,7 +181,7 @@
<bool>true</bool>
</property>
<property name="text">
<string>Enable layer 1 keepalive messages (testing only)</string>
<string>Enable layer 1 keepalive messages (non-functional)</string>
</property>
<property name="checked">
<bool>false</bool>

View File

@@ -2,8 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/iou/ui/iou_device_configuration_page.ui'
#
# Created: Thu Jan 5 14:49:45 2017
# by: PyQt5 UI code generator 5.2.1
# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
@@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_iouDeviceConfigPageWidget(object):
def setupUi(self, iouDeviceConfigPageWidget):
iouDeviceConfigPageWidget.setObjectName("iouDeviceConfigPageWidget")
iouDeviceConfigPageWidget.resize(569, 503)
iouDeviceConfigPageWidget.resize(767, 685)
self.verticalLayout = QtWidgets.QVBoxLayout(iouDeviceConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(iouDeviceConfigPageWidget)
@@ -209,7 +208,7 @@ class Ui_iouDeviceConfigPageWidget(object):
self.uiPrivateConfigToolButton.setText(_translate("iouDeviceConfigPageWidget", "&Browse..."))
self.uiDefaultNameFormatLabel.setText(_translate("iouDeviceConfigPageWidget", "Default name format:"))
self.uiOtherSettingsGroupBox.setTitle(_translate("iouDeviceConfigPageWidget", "Other settings"))
self.uiL1KeepalivesCheckBox.setText(_translate("iouDeviceConfigPageWidget", "Enable layer 1 keepalive messages (testing only)"))
self.uiL1KeepalivesCheckBox.setText(_translate("iouDeviceConfigPageWidget", "Enable layer 1 keepalive messages (non-functional)"))
self.uiDefaultValuesCheckBox.setText(_translate("iouDeviceConfigPageWidget", "Use default IOU values for memories"))
self.uiRamLabel.setText(_translate("iouDeviceConfigPageWidget", "RAM size:"))
self.uiRamSpinBox.setSuffix(_translate("iouDeviceConfigPageWidget", " MB"))

View File

@@ -78,9 +78,14 @@ class Module(QtCore.QObject):
:param directory: destination directory path
"""
node_names_cannot_export = []
for node in self._nodes:
if hasattr(node, "initialized") and node.initialized():
node.exportConfigToDirectory(directory)
if not node.exportConfigToDirectory(directory):
node_names_cannot_export.append(node.name())
if node_names_cannot_export:
log.warning("Config export is not supported by the following nodes: {}".format(" ".join(node_names_cannot_export)))
def importConfigs(self, directory):
"""

View File

@@ -25,9 +25,9 @@ import re
from collections import OrderedDict
from gns3.modules.qemu.dialogs.qemu_image_wizard import QemuImageWizard
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.ports.port_name_factory import StandardPortNameFactory
from gns3.node import Node
from gns3.qt import QtCore, QtWidgets, qpartial
from gns3.modules.module_error import ModuleError
from gns3.qt import QtCore, QtWidgets, qpartial, sip_is_deleted
from gns3.dialogs.node_properties_dialog import ConfigurationError
from gns3.image_manager import ImageManager
@@ -54,6 +54,10 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
self.uiBootPriorityComboBox.addItem("Network", "n")
self.uiBootPriorityComboBox.addItem("HDD or Network", "cn")
self.uiBootPriorityComboBox.addItem("HDD or CD/DVD-ROM", "cd")
self.uiBootPriorityComboBox.addItem("CD/DVD-ROM or Network", "dn")
self.uiBootPriorityComboBox.addItem("CD/DVD-ROM or HDD", "dc")
self.uiBootPriorityComboBox.addItem("Network or HDD", "nc")
self.uiBootPriorityComboBox.addItem("Network or CD/DVD-ROM", "nd")
self.uiHdaDiskImageToolButton.clicked.connect(self._hdaDiskImageBrowserSlot)
self.uiHdbDiskImageToolButton.clicked.connect(self._hdbDiskImageBrowserSlot)
@@ -264,6 +268,9 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
:param error: indicates an error (boolean)
"""
if sip_is_deleted(self.uiQemuListComboBox) or sip_is_deleted(self):
return
if error:
QtWidgets.QMessageBox.critical(self, "Qemu binaries", "{}".format(result["message"]))
else:
@@ -281,8 +288,12 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
if index != -1:
self.uiQemuListComboBox.setCurrentIndex(index)
else:
QtWidgets.QMessageBox.critical(self, "Qemu", "Could not find {} in the Qemu binaries list".format(qemu_path))
self.uiQemuListComboBox.clear()
index = self.uiQemuListComboBox.findData("{path}".format(path=os.path.basename(qemu_path)), flags=QtCore.Qt.MatchEndsWith)
self.uiQemuListComboBox.setCurrentIndex(index)
if index == -1:
QtWidgets.QMessageBox.warning(self, "Qemu","Could not find '{}' in the Qemu binaries list, please select a new binary".format(qemu_path))
else:
QtWidgets.QMessageBox.warning(self, "Qemu","Could not find '{}' in the Qemu binaries list, an alternative path has been selected".format(qemu_path))
def _cpuThrottlingChangedSlot(self, state):
"""
@@ -322,11 +333,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
QtWidgets.QMessageBox.warning(self, "Qemu", "Server {} is not running, cannot retrieve the QEMU binaries list".format(settings["server"]))
else:
callback = qpartial(self._getQemuBinariesFromServerCallback, qemu_path=settings["qemu_path"])
try:
Qemu.instance().getQemuBinariesFromServer(self._compute_id, callback)
except ModuleError as e:
QtWidgets.QMessageBox.critical(self, "Qemu", "Error while getting the QEMU binaries list: {}".format(e))
self.uiQemuListComboBox.clear()
Qemu.instance().getQemuBinariesFromServer(self._compute_id, callback)
if not group:
# set the device name
@@ -497,25 +504,29 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
symbol_path = self.uiSymbolLineEdit.text()
settings["symbol"] = symbol_path
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
port_name_format = self.uiPortNameFormatLineEdit.text()
if '{0}' not in port_name_format and '{port0}' not in port_name_format and '{port1}' not in port_name_format:
QtWidgets.QMessageBox.critical(self, "Port name format", "The format must contain at least {0}, {port0} or {port1}")
else:
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
port_segment_size = self.uiPortSegmentSizeSpinBox.value()
if port_segment_size and '{1}' not in port_name_format and '{segment0}' not in port_name_format and '{segment1}' not in port_name_format:
QtWidgets.QMessageBox.critical(self, "Port name format", "If the segment size is not 0, the format must contain {1}, {segment0} or {segment1}")
else:
settings["port_segment_size"] = port_segment_size
first_port_name = self.uiFirstPortNameLineEdit.text().strip()
settings["first_port_name"] = self.uiFirstPortNameLineEdit.text().strip()
try:
StandardPortNameFactory(self.uiAdaptersSpinBox.value(), first_port_name, port_name_format, port_segment_size)
except (IndexError, ValueError, KeyError):
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
raise ConfigurationError()
if self.uiQemuListComboBox.count():
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
settings["port_segment_size"] = port_segment_size
settings["first_port_name"] = first_port_name
if self.uiQemuListComboBox.currentIndex() != -1:
qemu_path = self.uiQemuListComboBox.itemData(self.uiQemuListComboBox.currentIndex())
settings["qemu_path"] = qemu_path
else:
QtWidgets.QMessageBox.critical(self, "Qemu binary", "Please select a Qemu binary")
if node:
raise ConfigurationError()
settings["boot_priority"] = self.uiBootPriorityComboBox.itemData(self.uiBootPriorityComboBox.currentIndex())
settings["console_type"] = self.uiConsoleTypeComboBox.currentText().lower()

View File

@@ -0,0 +1,249 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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/>.
"""
TraceNG module implementation.
"""
import os
import copy
import shutil
from gns3.local_config import LocalConfig
from gns3.local_server_config import LocalServerConfig
from ..module import Module
from .traceng_node import TraceNGNode
from .settings import TRACENG_SETTINGS
from .settings import TRACENG_NODES_SETTINGS
import logging
log = logging.getLogger(__name__)
class TraceNG(Module):
"""
TraceNG module.
"""
def __init__(self):
super().__init__()
self._settings = {}
self._nodes = []
self._traceng_nodes = {}
self._working_dir = ""
self._loadSettings()
def configChangedSlot(self):
# load the settings
self._loadSettings()
def _loadSettings(self):
"""
Loads the settings from the persistent settings file.
"""
self._settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, TRACENG_SETTINGS)
if not os.path.exists(self._settings["traceng_path"]):
traceng_path = shutil.which("traceng")
if traceng_path:
self._settings["traceng_path"] = os.path.abspath(traceng_path)
else:
self._settings["traceng_path"] = ""
self._loadTraceNGNodes()
def _saveSettings(self):
"""
Saves the settings to the persistent settings file.
"""
# save the settings
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
server_settings = {}
if self._settings["traceng_path"]:
# save some settings to the server config file
server_settings["traceng_path"] = os.path.normpath(self._settings["traceng_path"])
config = LocalServerConfig.instance()
config.saveSettings(self.__class__.__name__, server_settings)
def _loadTraceNGNodes(self):
"""
Load the TraceNG nodes from the persistent settings file.
"""
self._traceng_nodes = {}
settings = LocalConfig.instance().settings()
if "nodes" in settings.get(self.__class__.__name__, {}):
for node in settings[self.__class__.__name__]["nodes"]:
name = node.get("name")
server = node.get("server")
key = "{server}:{name}".format(server=server, name=name)
if key in self._traceng_nodes or not name or not server:
continue
node_settings = TRACENG_NODES_SETTINGS.copy()
node_settings.update(node)
self._traceng_nodes[key] = node_settings
def _saveTraceNGNodes(self):
"""
Saves the TraceNG nodes to the persistent settings file.
"""
self._settings["nodes"] = list(self._traceng_nodes.values())
self._saveSettings()
def addNode(self, node):
"""
Adds a node to this module.
:param node: Node instance
"""
self._nodes.append(node)
def removeNode(self, node):
"""
Removes a node from this module.
:param node: Node instance
"""
if node in self._nodes:
self._nodes.remove(node)
def settings(self):
"""
Returns the module settings
:returns: module settings (dictionary)
"""
return self._settings
def setSettings(self, settings):
"""
Sets the module settings
:param settings: module settings (dictionary)
"""
self._settings.update(settings)
self._saveSettings()
def instantiateNode(self, node_class, server, project):
"""
Instantiate a new node.
:param node_class: Node object
:param server: HTTPClient instance
:param project: Project instance
"""
# create an instance of the node class
return node_class(self, server, project)
def reset(self):
"""
Resets the module.
"""
self._nodes.clear()
@staticmethod
def getNodeType(name, platform=None):
if name == "traceng":
return TraceNGNode
return None
@staticmethod
def vmConfigurationPage():
from .pages.traceng_node_configuration_page import TraceNGNodeConfigurationPage
return TraceNGNodeConfigurationPage
def VMs(self):
"""
Returns list of TraceNG nodes
"""
return self._traceng_nodes
def setVMs(self, new_traceng_nodes):
"""
Sets TraceNG list
:param new_traceng_vms: TraceNG node list
"""
self._traceng_nodes = new_traceng_nodes.copy()
self._saveTraceNGNodes()
@staticmethod
def classes():
"""
Returns all the node classes supported by this module.
:returns: list of classes
"""
return [TraceNGNode]
def nodes(self):
"""
Returns all the node data necessary to represent a node
in the nodes view and create a node on the scene.
"""
nodes = []
# Add a default TraceNG not linked to a specific server
nodes.append(
{
"class": TraceNGNode.__name__,
"name": "TraceNG",
"categories": [TraceNGNode.end_devices],
"symbol": TraceNGNode.defaultSymbol(),
"builtin": True
}
)
return nodes
@staticmethod
def preferencePages():
"""
:returns: QWidget object list
"""
from .pages.traceng_preferences_page import TraceNGPreferencesPage
from .pages.traceng_node_preferences_page import TraceNGNodePreferencesPage
return [TraceNGPreferencesPage, TraceNGNodePreferencesPage]
@staticmethod
def instance():
"""
Singleton to return only on instance of TraceNG module.
:returns: instance of TraceNG
"""
if not hasattr(TraceNG, "_instance"):
TraceNG._instance = TraceNG()
return TraceNG._instance

View File

View File

@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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/>.
"""
Wizard for TraceNG nodes.
"""
import sys
import ipaddress
from gns3.qt import QtGui, QtWidgets
from gns3.node import Node
from gns3.dialogs.vm_wizard import VMWizard
from ..ui.traceng_node_wizard_ui import Ui_TraceNGNodeWizard
class TraceNGNodeWizard(VMWizard, Ui_TraceNGNodeWizard):
"""
Wizard to create a TraceNG node template.
:param parent: parent widget
"""
def __init__(self, traceng_nodes, parent):
super().__init__(traceng_nodes, parent)
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/traceng.png"))
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
# TraceNG is only supported on a local server
self.uiRemoteRadioButton.setEnabled(False)
self.uiVMRadioButton.setEnabled(False)
def validateCurrentPage(self):
"""
Validates the server.
"""
if super().validateCurrentPage() is False:
return False
if self.currentPage() == self.uiNameWizardPage:
if not sys.platform.startswith("win"):
QtWidgets.QMessageBox.critical(self, "TraceNG", "TraceNG can only run on Windows with a local server")
return False
ip_address = self.uiIPAddressLineEdit.text()
if ip_address:
try:
ipaddress.IPv4Address(ip_address)
except ipaddress.AddressValueError:
QtWidgets.QMessageBox.critical(self, "IP address", "Invalid IP address format")
return False
return True
def getSettings(self):
"""
Returns the settings set in this Wizard.
:return: settings dict
"""
settings = {"name": self.uiNameLineEdit.text(),
"ip_address": self.uiIPAddressLineEdit.text(),
"symbol": ":/symbols/traceng.svg",
"category": Node.end_devices,
"server": self._compute_id}
return settings

View File

View File

@@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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/>.
"""
Configuration page for TraceNG nodes
"""
import ipaddress
from gns3.qt import QtWidgets
from gns3.local_server import LocalServer
from gns3.node import Node
from gns3.controller import Controller
from ..ui.traceng_node_configuration_page_ui import Ui_TraceNGNodeConfigPageWidget
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.dialogs.node_properties_dialog import ConfigurationError
class TraceNGNodeConfigurationPage(QtWidgets.QWidget, Ui_TraceNGNodeConfigPageWidget):
"""
QWidget configuration page for TraceNG nodes.
"""
def __init__(self):
super().__init__()
self.setupUi(self)
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
self._default_configs_dir = LocalServer.instance().localServerSettings()["configs_path"]
if Controller.instance().isRemote():
self.uiScriptFileToolButton.hide()
# add the categories
for name, category in Node.defaultCategories().items():
self.uiCategoryComboBox.addItem(name, category)
def _symbolBrowserSlot(self):
"""
Slot to open the symbol browser and select a new symbol.
"""
symbol_path = self.uiSymbolLineEdit.text()
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
dialog.show()
if dialog.exec_():
new_symbol_path = dialog.getSymbol()
self.uiSymbolLineEdit.setText(new_symbol_path)
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
def loadSettings(self, settings, node=None, group=False):
"""
Loads the TraceNG node settings.
:param settings: the settings (dictionary)
:param node: Node instance
:param group: indicates the settings apply to a group of routers
"""
if not group:
self.uiNameLineEdit.setText(settings["name"])
self.uiIPAddressLineEdit.setText(settings["ip_address"])
self.uiDefaultDestinationLineEdit.setText(settings["default_destination"])
else:
self.uiIPAddressLabel.hide()
self.uiIPAddressLineEdit.hide()
self.uiDefaultDestinationLabel.hide()
self.uiDefaultDestinationLineEdit.hide()
self.uiNameLabel.hide()
self.uiNameLineEdit.hide()
if not node:
# these are template settings
# rename the label from "Name" to "Template name"
self.uiNameLabel.setText("Template name:")
# load the default name format
self.uiDefaultNameFormatLineEdit.setText(settings["default_name_format"])
# load the symbol
self.uiSymbolLineEdit.setText(settings["symbol"])
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(settings["symbol"]))
# load the category
index = self.uiCategoryComboBox.findData(settings["category"])
if index != -1:
self.uiCategoryComboBox.setCurrentIndex(index)
else:
self.uiDefaultNameFormatLabel.hide()
self.uiDefaultNameFormatLineEdit.hide()
self.uiSymbolLabel.hide()
self.uiSymbolLineEdit.hide()
self.uiSymbolToolButton.hide()
self.uiCategoryComboBox.hide()
self.uiCategoryLabel.hide()
self.uiCategoryComboBox.hide()
def saveSettings(self, settings, node=None, group=False):
"""
Saves the TraceNG node settings.
:param settings: the settings (dictionary)
:param node: Node instance
:param group: indicates the settings apply to a group of routers
"""
# these settings cannot be shared by nodes and updated
# in the node properties dialog.
if not group:
# set the node name
name = self.uiNameLineEdit.text()
if not name:
QtWidgets.QMessageBox.critical(self, "Name", "TraceNG node name cannot be empty!")
else:
settings["name"] = name
ip_address = self.uiIPAddressLineEdit.text().strip()
if ip_address:
try:
ipaddress.IPv4Address(ip_address)
settings["ip_address"] = ip_address
except ipaddress.AddressValueError:
QtWidgets.QMessageBox.critical(self, "IP address", "Invalid IP address format")
if node:
raise ConfigurationError()
settings["default_destination"] = self.uiDefaultDestinationLineEdit.text().strip()
if not node:
default_name_format = self.uiDefaultNameFormatLineEdit.text().strip()
if '{0}' not in default_name_format and '{id}' not in default_name_format:
QtWidgets.QMessageBox.critical(self, "Default name format", "The default name format must contain at least {0} or {id}")
else:
settings["default_name_format"] = default_name_format
symbol_path = self.uiSymbolLineEdit.text()
settings["symbol"] = symbol_path
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
return settings

View File

@@ -0,0 +1,189 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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/>.
"""
Configuration page for TraceNG node preferences.
"""
import copy
from gns3.qt import QtCore, QtWidgets, qpartial
from gns3.main_window import MainWindow
from gns3.dialogs.configuration_dialog import ConfigurationDialog
from gns3.compute_manager import ComputeManager
from gns3.controller import Controller
from .. import TraceNG
from ..settings import TRACENG_NODES_SETTINGS
from ..ui.traceng_node_preferences_page_ui import Ui_TraceNGNodePageWidget
from ..pages.traceng_node_configuration_page import TraceNGNodeConfigurationPage
from ..dialogs.traceng_node_wizard import TraceNGNodeWizard
class TraceNGNodePreferencesPage(QtWidgets.QWidget, Ui_TraceNGNodePageWidget):
"""
QWidget preference page for TraceNG node preferences.
"""
def __init__(self):
super().__init__()
self.setupUi(self)
self._main_window = MainWindow.instance()
self._traceng_nodes = {}
self._items = []
self.uiNewTraceNGPushButton.clicked.connect(self._newTraceNGSlot)
self.uiEditTraceNGPushButton.clicked.connect(self._editTraceNGSlot)
self.uiDeleteTraceNGPushButton.clicked.connect(self._deleteTraceNGSlot)
self.uiTraceNGTreeWidget.itemSelectionChanged.connect(self._tracengChangedSlot)
def _createSectionItem(self, name):
section_item = QtWidgets.QTreeWidgetItem(self.uiTraceNGInfoTreeWidget)
section_item.setText(0, name)
font = section_item.font(0)
font.setBold(True)
section_item.setFont(0, font)
return section_item
def _refreshInfo(self, traceng_node):
self.uiTraceNGInfoTreeWidget.clear()
# fill out the General section
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", traceng_node["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["IP address:", traceng_node["ip_address"]])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", traceng_node["default_name_format"]])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(traceng_node["server"]).name()])
except KeyError:
pass
self.uiTraceNGInfoTreeWidget.expandAll()
self.uiTraceNGInfoTreeWidget.resizeColumnToContents(0)
self.uiTraceNGInfoTreeWidget.resizeColumnToContents(1)
self.uiTraceNGTreeWidget.setMaximumWidth(self.uiTraceNGTreeWidget.sizeHintForColumn(0) + 20)
def _tracengChangedSlot(self):
"""
Loads a selected TraceNG node template from the tree widget.
"""
selection = self.uiTraceNGTreeWidget.selectedItems()
self.uiDeleteTraceNGPushButton.setEnabled(len(selection) != 0)
single_selected = len(selection) == 1
self.uiEditTraceNGPushButton.setEnabled(single_selected)
if single_selected:
key = selection[0].data(0, QtCore.Qt.UserRole)
traceng_node = self._traceng_nodes[key]
self._refreshInfo(traceng_node)
else:
self.uiTraceNGInfoTreeWidget.clear()
def _newTraceNGSlot(self):
"""
Creates a new TraceNG node template.
"""
wizard = TraceNGNodeWizard(self._traceng_nodes, parent=self)
wizard.show()
if wizard.exec_():
new_traceng_node_settings = wizard.getSettings()
key = "{server}:{name}".format(server=new_traceng_node_settings["server"], name=new_traceng_node_settings["name"])
self._traceng_nodes[key] = TRACENG_NODES_SETTINGS.copy()
self._traceng_nodes[key].update(new_traceng_node_settings)
item = QtWidgets.QTreeWidgetItem(self.uiTraceNGTreeWidget)
item.setText(0, self._traceng_nodes[key]["name"])
Controller.instance().getSymbolIcon(self._traceng_nodes[key]["symbol"], qpartial(self._setItemIcon, item))
item.setData(0, QtCore.Qt.UserRole, key)
self._items.append(item)
self.uiTraceNGTreeWidget.setCurrentItem(item)
def _editTraceNGSlot(self):
"""
Edits a TraceNG node template.
"""
item = self.uiTraceNGTreeWidget.currentItem()
if item:
key = item.data(0, QtCore.Qt.UserRole)
traceng_node = self._traceng_nodes[key]
dialog = ConfigurationDialog(traceng_node["name"], traceng_node, TraceNGNodeConfigurationPage(), parent=self)
dialog.show()
if dialog.exec_():
# update the icon
Controller.instance().getSymbolIcon(traceng_node["symbol"], qpartial(self._setItemIcon, item))
if traceng_node["name"] != item.text(0):
new_key = "{server}:{name}".format(server=traceng_node["server"], name=traceng_node["name"])
if new_key in self._traceng_nodes:
QtWidgets.QMessageBox.critical(self, "TraceNG node", "TraceNG node name {} already exists for server {}".format(traceng_node["name"],
traceng_node["server"]))
traceng_node["name"] = item.text(0)
return
self._traceng_nodes[new_key] = self._traceng_nodes[key]
del self._traceng_nodes[key]
item.setText(0, traceng_node["name"])
item.setData(0, QtCore.Qt.UserRole, new_key)
self._refreshInfo(traceng_node)
def _deleteTraceNGSlot(self):
"""
Deletes a TraceNG node template.
"""
for item in self.uiTraceNGTreeWidget.selectedItems():
if item:
key = item.data(0, QtCore.Qt.UserRole)
del self._traceng_nodes[key]
self.uiTraceNGTreeWidget.takeTopLevelItem(self.uiTraceNGTreeWidget.indexOfTopLevelItem(item))
def loadPreferences(self):
"""
Loads the TraceNG node preferences.
"""
traceng_module = TraceNG.instance()
self._traceng_nodes = copy.deepcopy(traceng_module.VMs())
self._items.clear()
for key, node in self._traceng_nodes.items():
item = QtWidgets.QTreeWidgetItem(self.uiTraceNGTreeWidget)
item.setText(0, node["name"])
Controller.instance().getSymbolIcon(node["symbol"], qpartial(self._setItemIcon, item))
item.setData(0, QtCore.Qt.UserRole, key)
self._items.append(item)
if self._items:
self.uiTraceNGTreeWidget.setCurrentItem(self._items[0])
self.uiTraceNGTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiTraceNGTreeWidget.setMaximumWidth(self.uiTraceNGTreeWidget.sizeHintForColumn(0) + 20)
def _setItemIcon(self, item, icon):
item.setIcon(0, icon)
self.uiTraceNGTreeWidget.setMaximumWidth(self.uiTraceNGTreeWidget.sizeHintForColumn(0) + 20)
def savePreferences(self):
"""
Saves the TraceNG node preferences.
"""
TraceNG.instance().setVMs(self._traceng_nodes)

View File

@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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/>.
"""
Configuration page for TraceNG preferences.
"""
import os
import sys
import shutil
from gns3.qt import QtWidgets
from .. import TraceNG
from ..ui.traceng_preferences_page_ui import Ui_TraceNGPreferencesPageWidget
from ..settings import TRACENG_SETTINGS
class TraceNGPreferencesPage(QtWidgets.QWidget, Ui_TraceNGPreferencesPageWidget):
"""
QWidget preference page for TraceNG
"""
def __init__(self):
super().__init__()
self.setupUi(self)
# connect signals
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
self.uiTraceNGPathToolButton.clicked.connect(self._tracengPathBrowserSlot)
def _tracengPathBrowserSlot(self):
"""
Slot to open a file browser and select traceng
"""
filter = ""
if sys.platform.startswith("win"):
filter = "Executable (*.exe);;All files (*.*)"
traceng_path = shutil.which("traceng")
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select TraceNG", traceng_path, filter)
if not path:
return
if self._checkTraceNGPath(path):
self.uiTraceNGPathLineEdit.setText(os.path.normpath(path))
def _checkTraceNGPath(self, path):
"""
Checks that the TraceNG path is valid.
:param path: TraceNG path
:returns: boolean
"""
if not os.path.exists(path):
QtWidgets.QMessageBox.critical(self, "TraceNG", '"{}" does not exist'.format(path))
return False
if not os.access(path, os.X_OK):
QtWidgets.QMessageBox.critical(self, "TraceNG", "{} is not an executable".format(os.path.basename(path)))
return False
return True
def _restoreDefaultsSlot(self):
"""
Slot to populate the page widgets with the default settings.
"""
self._populateWidgets(TRACENG_SETTINGS)
def _useLocalServerSlot(self, state):
"""
Slot to enable or not local server settings.
"""
if state:
self.uiTraceNGPathLineEdit.setEnabled(True)
self.uiTraceNGPathToolButton.setEnabled(True)
else:
self.uiTraceNGPathLineEdit.setEnabled(False)
self.uiTraceNGPathToolButton.setEnabled(False)
def _populateWidgets(self, settings):
"""
Populates the widgets with the settings.
:param settings: TraceNG settings
"""
self.uiTraceNGPathLineEdit.setText(settings["traceng_path"])
def loadPreferences(self):
"""
Loads TraceNG preferences.
"""
traceng_settings = TraceNG.instance().settings()
self._populateWidgets(traceng_settings)
def savePreferences(self):
"""
Saves TraceNG preferences.
"""
traceng_path = self.uiTraceNGPathLineEdit.text().strip()
if traceng_path and not self._checkTraceNGPath(traceng_path):
return
new_settings = {"traceng_path": traceng_path}
TraceNG.instance().setSettings(new_settings)

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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/>.
"""
Default TraceNG settings.
"""
from gns3.node import Node
TRACENG_SETTINGS = {
"traceng_path": "",
}
TRACENG_NODES_SETTINGS = {
"name": "",
"ip_address": "",
"default_destination": "",
"default_name_format": "TraceNG{0}",
"console_type": "none",
"symbol": ":/symbols/traceng.svg",
"category": Node.end_devices,
}

View File

@@ -0,0 +1,172 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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/>.
"""
TraceNG node implementation.
"""
from gns3.node import Node
from gns3.qt import QtWidgets
import logging
log = logging.getLogger(__name__)
class TraceNGNode(Node):
"""
TraceNG node.
:param module: parent module for this node
:param server: GNS3 server instance
:param project: Project instance
"""
URL_PREFIX = "traceng"
def __init__(self, module, server, project):
super().__init__(module, server, project)
traceng_settings = {"console_host": None,
"console": None,
"console_type": "none",
"ip_address": "",
"default_destination": ""}
self._last_destination = ""
self.settings().update(traceng_settings)
def update(self, new_settings):
"""
Updates the settings for this TraceNG node.
:param new_settings: settings dictionary
"""
params = {}
for name, value in new_settings.items():
if name in self._settings and self._settings[name] != value:
params[name] = value
if params:
self._update(params)
def start(self):
"""
Starts this node instance.
"""
if self.isStarted():
log.debug("{} is already running".format(self.name()))
return
if self._last_destination:
destination = self._last_destination
else:
destination = self.settings()["default_destination"]
destination, ok = QtWidgets.QInputDialog.getText(self.parent(), "TraceNG", "Destination host or IP address:", text=destination)
if ok:
if not destination:
QtWidgets.QMessageBox.critical(self, "TraceNG", "Please provide a host or IP address to trace")
return
ip_address = self.settings()["ip_address"]
if destination == ip_address:
QtWidgets.QMessageBox.critical(self, "TraceNG", "Destination cannot be the same as this node IP address ({})".format(ip_address))
return
self._last_destination = destination
params = {"destination": destination}
log.debug("{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, body=params, timeout=None, showProgress=False)
def info(self):
"""
Returns information about this TraceNG node.
:returns: formatted string
"""
if self.status() == Node.started:
state = "started"
else:
state = "stopped"
info = """Node {name} is {state}
Local node ID is {id}
Server's VPCS node ID is {node_id}
TraceNG's server runs on {host}, console is on port {console}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
state=state,
host=self.compute().name(),
console=self._settings["console"])
port_info = ""
for port in self._ports:
if port.isFree():
port_info += " {port_name} is empty\n".format(port_name=port.name())
else:
port_info += " {port_name} {port_description}\n".format(port_name=port.name(),
port_description=port.description())
return info + port_info
def console(self):
"""
Returns the console port for this TraceNG node.
:returns: port (integer)
"""
return self._settings["console"]
def configPage(self):
"""
Returns the configuration page widget to be used by the node properties dialog.
:returns: QWidget object
"""
from .pages.traceng_node_configuration_page import TraceNGNodeConfigurationPage
return TraceNGNodeConfigurationPage
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this node.
:returns: symbol path (or resource).
"""
return ":/symbols/traceng.svg"
@staticmethod
def symbolName():
return "TraceNG"
@staticmethod
def categories():
"""
Returns the node categories the node is part of (used by the node panel).
:returns: list of node categories
"""
return [Node.end_devices]
def __str__(self):
return "TraceNG node"

View File

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TraceNGNodeConfigPageWidget</class>
<widget class="QWidget" name="TraceNGNodeConfigPageWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>846</width>
<height>340</height>
</rect>
</property>
<property name="windowTitle">
<string>TraceNG node configuration</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="uiNameLabel">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="uiIPAddressLabel">
<property name="text">
<string>IP address:</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="uiDefaultDestinationLabel">
<property name="text">
<string>Default destination:</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QLabel" name="uiDefaultNameFormatLabel">
<property name="text">
<string>Default name format:</string>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QLineEdit" name="uiDefaultNameFormatLineEdit"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="uiSymbolLabel">
<property name="text">
<string>Symbol:</string>
</property>
</widget>
</item>
<item row="4" column="3">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLineEdit" name="uiSymbolLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="uiSymbolToolButton">
<property name="text">
<string>&amp;Browse...</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0" colspan="2">
<widget class="QLabel" name="uiCategoryLabel">
<property name="text">
<string>Category:</string>
</property>
</widget>
</item>
<item row="5" column="3">
<widget class="QComboBox" name="uiCategoryComboBox"/>
</item>
<item row="6" column="1" colspan="3">
<spacer name="spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>263</width>
<height>212</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="3">
<widget class="QLineEdit" name="uiDefaultDestinationLineEdit"/>
</item>
<item row="1" column="3">
<widget class="QLineEdit" name="uiIPAddressLineEdit"/>
</item>
<item row="0" column="3">
<widget class="QLineEdit" name="uiNameLineEdit"/>
</item>
</layout>
<zorder>uiNameLabel</zorder>
<zorder>uiNameLineEdit</zorder>
<zorder>uiDefaultNameFormatLabel</zorder>
<zorder>uiDefaultNameFormatLineEdit</zorder>
<zorder>uiSymbolLabel</zorder>
<zorder>uiCategoryLabel</zorder>
<zorder>uiCategoryComboBox</zorder>
<zorder>uiIPAddressLabel</zorder>
<zorder>uiIPAddressLineEdit</zorder>
<zorder>uiDefaultDestinationLabel</zorder>
<zorder>uiDefaultDestinationLineEdit</zorder>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/traceng/ui/traceng_node_configuration_page.ui'
#
# 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_TraceNGNodeConfigPageWidget(object):
def setupUi(self, TraceNGNodeConfigPageWidget):
TraceNGNodeConfigPageWidget.setObjectName("TraceNGNodeConfigPageWidget")
TraceNGNodeConfigPageWidget.resize(846, 340)
self.gridLayout = QtWidgets.QGridLayout(TraceNGNodeConfigPageWidget)
self.gridLayout.setObjectName("gridLayout")
self.uiNameLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
self.uiNameLabel.setObjectName("uiNameLabel")
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
self.uiIPAddressLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
self.uiIPAddressLabel.setObjectName("uiIPAddressLabel")
self.gridLayout.addWidget(self.uiIPAddressLabel, 1, 0, 1, 1)
self.uiDefaultDestinationLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
self.uiDefaultDestinationLabel.setObjectName("uiDefaultDestinationLabel")
self.gridLayout.addWidget(self.uiDefaultDestinationLabel, 2, 0, 1, 2)
self.uiDefaultNameFormatLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
self.uiDefaultNameFormatLabel.setObjectName("uiDefaultNameFormatLabel")
self.gridLayout.addWidget(self.uiDefaultNameFormatLabel, 3, 0, 1, 3)
self.uiDefaultNameFormatLineEdit = QtWidgets.QLineEdit(TraceNGNodeConfigPageWidget)
self.uiDefaultNameFormatLineEdit.setObjectName("uiDefaultNameFormatLineEdit")
self.gridLayout.addWidget(self.uiDefaultNameFormatLineEdit, 3, 3, 1, 1)
self.uiSymbolLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
self.uiSymbolLabel.setObjectName("uiSymbolLabel")
self.gridLayout.addWidget(self.uiSymbolLabel, 4, 0, 1, 1)
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
self.uiSymbolLineEdit = QtWidgets.QLineEdit(TraceNGNodeConfigPageWidget)
self.uiSymbolLineEdit.setObjectName("uiSymbolLineEdit")
self.horizontalLayout_7.addWidget(self.uiSymbolLineEdit)
self.uiSymbolToolButton = QtWidgets.QToolButton(TraceNGNodeConfigPageWidget)
self.uiSymbolToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
self.uiSymbolToolButton.setObjectName("uiSymbolToolButton")
self.horizontalLayout_7.addWidget(self.uiSymbolToolButton)
self.gridLayout.addLayout(self.horizontalLayout_7, 4, 3, 1, 1)
self.uiCategoryLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
self.uiCategoryLabel.setObjectName("uiCategoryLabel")
self.gridLayout.addWidget(self.uiCategoryLabel, 5, 0, 1, 2)
self.uiCategoryComboBox = QtWidgets.QComboBox(TraceNGNodeConfigPageWidget)
self.uiCategoryComboBox.setObjectName("uiCategoryComboBox")
self.gridLayout.addWidget(self.uiCategoryComboBox, 5, 3, 1, 1)
spacerItem = QtWidgets.QSpacerItem(263, 212, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem, 6, 1, 1, 3)
self.uiDefaultDestinationLineEdit = QtWidgets.QLineEdit(TraceNGNodeConfigPageWidget)
self.uiDefaultDestinationLineEdit.setObjectName("uiDefaultDestinationLineEdit")
self.gridLayout.addWidget(self.uiDefaultDestinationLineEdit, 2, 3, 1, 1)
self.uiIPAddressLineEdit = QtWidgets.QLineEdit(TraceNGNodeConfigPageWidget)
self.uiIPAddressLineEdit.setObjectName("uiIPAddressLineEdit")
self.gridLayout.addWidget(self.uiIPAddressLineEdit, 1, 3, 1, 1)
self.uiNameLineEdit = QtWidgets.QLineEdit(TraceNGNodeConfigPageWidget)
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 3, 1, 1)
self.uiNameLabel.raise_()
self.uiNameLineEdit.raise_()
self.uiDefaultNameFormatLabel.raise_()
self.uiDefaultNameFormatLineEdit.raise_()
self.uiSymbolLabel.raise_()
self.uiCategoryLabel.raise_()
self.uiCategoryComboBox.raise_()
self.uiIPAddressLabel.raise_()
self.uiIPAddressLineEdit.raise_()
self.uiDefaultDestinationLabel.raise_()
self.uiDefaultDestinationLineEdit.raise_()
self.retranslateUi(TraceNGNodeConfigPageWidget)
QtCore.QMetaObject.connectSlotsByName(TraceNGNodeConfigPageWidget)
def retranslateUi(self, TraceNGNodeConfigPageWidget):
_translate = QtCore.QCoreApplication.translate
TraceNGNodeConfigPageWidget.setWindowTitle(_translate("TraceNGNodeConfigPageWidget", "TraceNG node configuration"))
self.uiNameLabel.setText(_translate("TraceNGNodeConfigPageWidget", "Name:"))
self.uiIPAddressLabel.setText(_translate("TraceNGNodeConfigPageWidget", "IP address:"))
self.uiDefaultDestinationLabel.setText(_translate("TraceNGNodeConfigPageWidget", "Default destination:"))
self.uiDefaultNameFormatLabel.setText(_translate("TraceNGNodeConfigPageWidget", "Default name format:"))
self.uiSymbolLabel.setText(_translate("TraceNGNodeConfigPageWidget", "Symbol:"))
self.uiSymbolToolButton.setText(_translate("TraceNGNodeConfigPageWidget", "&Browse..."))
self.uiCategoryLabel.setText(_translate("TraceNGNodeConfigPageWidget", "Category:"))

View File

@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TraceNGNodePageWidget</class>
<widget class="QWidget" name="TraceNGNodePageWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>546</width>
<height>455</height>
</rect>
</property>
<property name="windowTitle">
<string>TraceNG nodes</string>
</property>
<property name="accessibleName">
<string>TraceNG node templates</string>
</property>
<property name="accessibleDescription">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QTreeWidget" name="uiTraceNGTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>160</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeWidget" name="uiTraceNGInfoTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="indentation">
<number>10</number>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>1</string>
</property>
</column>
<column>
<property name="text">
<string>2</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QPushButton" name="uiNewTraceNGPushButton">
<property name="text">
<string>&amp;New</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiEditTraceNGPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Edit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDeleteTraceNGPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>uiNewTraceNGPushButton</tabstop>
<tabstop>uiDeleteTraceNGPushButton</tabstop>
</tabstops>
<resources/>
<connections/>
<designerdata>
<property name="gridDeltaX">
<number>10</number>
</property>
<property name="gridDeltaY">
<number>10</number>
</property>
<property name="gridSnapX">
<bool>true</bool>
</property>
<property name="gridSnapY">
<bool>true</bool>
</property>
<property name="gridVisible">
<bool>true</bool>
</property>
</designerdata>
</ui>

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/traceng/ui/traceng_node_preferences_page.ui'
#
# 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_TraceNGNodePageWidget(object):
def setupUi(self, TraceNGNodePageWidget):
TraceNGNodePageWidget.setObjectName("TraceNGNodePageWidget")
TraceNGNodePageWidget.resize(546, 455)
TraceNGNodePageWidget.setAccessibleDescription("")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(TraceNGNodePageWidget)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.splitter = QtWidgets.QSplitter(TraceNGNodePageWidget)
self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName("splitter")
self.uiTraceNGTreeWidget = QtWidgets.QTreeWidget(self.splitter)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiTraceNGTreeWidget.sizePolicy().hasHeightForWidth())
self.uiTraceNGTreeWidget.setSizePolicy(sizePolicy)
self.uiTraceNGTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
font = QtGui.QFont()
font.setPointSize(11)
font.setBold(True)
font.setWeight(75)
self.uiTraceNGTreeWidget.setFont(font)
self.uiTraceNGTreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.uiTraceNGTreeWidget.setIconSize(QtCore.QSize(32, 32))
self.uiTraceNGTreeWidget.setRootIsDecorated(False)
self.uiTraceNGTreeWidget.setObjectName("uiTraceNGTreeWidget")
self.uiTraceNGTreeWidget.headerItem().setText(0, "1")
self.uiTraceNGTreeWidget.header().setVisible(False)
self.layoutWidget = QtWidgets.QWidget(self.splitter)
self.layoutWidget.setObjectName("layoutWidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTraceNGInfoTreeWidget = QtWidgets.QTreeWidget(self.layoutWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiTraceNGInfoTreeWidget.sizePolicy().hasHeightForWidth())
self.uiTraceNGInfoTreeWidget.setSizePolicy(sizePolicy)
self.uiTraceNGInfoTreeWidget.setIndentation(10)
self.uiTraceNGInfoTreeWidget.setAllColumnsShowFocus(True)
self.uiTraceNGInfoTreeWidget.setObjectName("uiTraceNGInfoTreeWidget")
self.uiTraceNGInfoTreeWidget.header().setVisible(False)
self.verticalLayout.addWidget(self.uiTraceNGInfoTreeWidget)
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.uiNewTraceNGPushButton = QtWidgets.QPushButton(self.layoutWidget)
self.uiNewTraceNGPushButton.setObjectName("uiNewTraceNGPushButton")
self.horizontalLayout_5.addWidget(self.uiNewTraceNGPushButton)
self.uiEditTraceNGPushButton = QtWidgets.QPushButton(self.layoutWidget)
self.uiEditTraceNGPushButton.setEnabled(False)
self.uiEditTraceNGPushButton.setObjectName("uiEditTraceNGPushButton")
self.horizontalLayout_5.addWidget(self.uiEditTraceNGPushButton)
self.uiDeleteTraceNGPushButton = QtWidgets.QPushButton(self.layoutWidget)
self.uiDeleteTraceNGPushButton.setEnabled(False)
self.uiDeleteTraceNGPushButton.setObjectName("uiDeleteTraceNGPushButton")
self.horizontalLayout_5.addWidget(self.uiDeleteTraceNGPushButton)
self.verticalLayout.addLayout(self.horizontalLayout_5)
self.verticalLayout_2.addWidget(self.splitter)
self.retranslateUi(TraceNGNodePageWidget)
QtCore.QMetaObject.connectSlotsByName(TraceNGNodePageWidget)
TraceNGNodePageWidget.setTabOrder(self.uiNewTraceNGPushButton, self.uiDeleteTraceNGPushButton)
def retranslateUi(self, TraceNGNodePageWidget):
_translate = QtCore.QCoreApplication.translate
TraceNGNodePageWidget.setWindowTitle(_translate("TraceNGNodePageWidget", "TraceNG nodes"))
TraceNGNodePageWidget.setAccessibleName(_translate("TraceNGNodePageWidget", "TraceNG node templates"))
self.uiTraceNGInfoTreeWidget.headerItem().setText(0, _translate("TraceNGNodePageWidget", "1"))
self.uiTraceNGInfoTreeWidget.headerItem().setText(1, _translate("TraceNGNodePageWidget", "2"))
self.uiNewTraceNGPushButton.setText(_translate("TraceNGNodePageWidget", "&New"))
self.uiEditTraceNGPushButton.setText(_translate("TraceNGNodePageWidget", "&Edit"))
self.uiDeleteTraceNGPushButton.setText(_translate("TraceNGNodePageWidget", "&Delete"))

View File

@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TraceNGNodeWizard</class>
<widget class="QWizard" name="TraceNGNodeWizard">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>706</width>
<height>452</height>
</rect>
</property>
<property name="windowTitle">
<string>New TraceNG node template</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<widget class="QWizardPage" name="uiServerWizardPage">
<property name="title">
<string>Server</string>
</property>
<property name="subTitle">
<string>Please choose a server type to run your new TraceNG node.</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QGroupBox" name="uiServerTypeGroupBox">
<property name="title">
<string>Server type</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QRadioButton" name="uiRemoteRadioButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Run the TraceNG node on a remote computer</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="uiVMRadioButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Run the TraceNG node on the GNS3 VM</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="uiLocalRadioButton">
<property name="text">
<string>Run the TraceNG node on your local computer</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="uiRemoteServersGroupBox">
<property name="title">
<string>Remote server</string>
</property>
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="0">
<widget class="QLabel" name="uiRemoteServersLabel">
<property name="text">
<string>Run on:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="uiRemoteServersComboBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWizardPage" name="uiNameWizardPage">
<property name="title">
<string>Name and IP address</string>
</property>
<property name="subTitle">
<string>Please choose a descriptive name and IP address for the new TraceNG node.</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="uiNameLabel">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="uiNameLineEdit"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="uiIPAddressLabel">
<property name="text">
<string>IP address:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="uiIPAddressLineEdit">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<tabstops>
<tabstop>uiNameLineEdit</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/traceng/ui/traceng_node_wizard.ui'
#
# 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_TraceNGNodeWizard(object):
def setupUi(self, TraceNGNodeWizard):
TraceNGNodeWizard.setObjectName("TraceNGNodeWizard")
TraceNGNodeWizard.resize(706, 452)
TraceNGNodeWizard.setModal(True)
self.uiServerWizardPage = QtWidgets.QWizardPage()
self.uiServerWizardPage.setObjectName("uiServerWizardPage")
self.gridLayout_2 = QtWidgets.QGridLayout(self.uiServerWizardPage)
self.gridLayout_2.setObjectName("gridLayout_2")
self.uiServerTypeGroupBox = QtWidgets.QGroupBox(self.uiServerWizardPage)
self.uiServerTypeGroupBox.setObjectName("uiServerTypeGroupBox")
self.verticalLayout = QtWidgets.QVBoxLayout(self.uiServerTypeGroupBox)
self.verticalLayout.setObjectName("verticalLayout")
self.uiRemoteRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
self.uiRemoteRadioButton.setEnabled(False)
self.uiRemoteRadioButton.setChecked(False)
self.uiRemoteRadioButton.setObjectName("uiRemoteRadioButton")
self.verticalLayout.addWidget(self.uiRemoteRadioButton)
self.uiVMRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
self.uiVMRadioButton.setEnabled(False)
self.uiVMRadioButton.setObjectName("uiVMRadioButton")
self.verticalLayout.addWidget(self.uiVMRadioButton)
self.uiLocalRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
self.uiLocalRadioButton.setChecked(True)
self.uiLocalRadioButton.setObjectName("uiLocalRadioButton")
self.verticalLayout.addWidget(self.uiLocalRadioButton)
self.gridLayout_2.addWidget(self.uiServerTypeGroupBox, 0, 0, 1, 1)
self.uiRemoteServersGroupBox = QtWidgets.QGroupBox(self.uiServerWizardPage)
self.uiRemoteServersGroupBox.setObjectName("uiRemoteServersGroupBox")
self.gridLayout_7 = QtWidgets.QGridLayout(self.uiRemoteServersGroupBox)
self.gridLayout_7.setObjectName("gridLayout_7")
self.uiRemoteServersLabel = QtWidgets.QLabel(self.uiRemoteServersGroupBox)
self.uiRemoteServersLabel.setObjectName("uiRemoteServersLabel")
self.gridLayout_7.addWidget(self.uiRemoteServersLabel, 0, 0, 1, 1)
self.uiRemoteServersComboBox = QtWidgets.QComboBox(self.uiRemoteServersGroupBox)
self.uiRemoteServersComboBox.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiRemoteServersComboBox.sizePolicy().hasHeightForWidth())
self.uiRemoteServersComboBox.setSizePolicy(sizePolicy)
self.uiRemoteServersComboBox.setObjectName("uiRemoteServersComboBox")
self.gridLayout_7.addWidget(self.uiRemoteServersComboBox, 0, 1, 1, 1)
self.gridLayout_2.addWidget(self.uiRemoteServersGroupBox, 1, 0, 1, 1)
TraceNGNodeWizard.addPage(self.uiServerWizardPage)
self.uiNameWizardPage = QtWidgets.QWizardPage()
self.uiNameWizardPage.setObjectName("uiNameWizardPage")
self.gridLayout = QtWidgets.QGridLayout(self.uiNameWizardPage)
self.gridLayout.setObjectName("gridLayout")
self.uiNameLabel = QtWidgets.QLabel(self.uiNameWizardPage)
self.uiNameLabel.setObjectName("uiNameLabel")
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
self.uiNameLineEdit = QtWidgets.QLineEdit(self.uiNameWizardPage)
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
self.uiIPAddressLabel = QtWidgets.QLabel(self.uiNameWizardPage)
self.uiIPAddressLabel.setObjectName("uiIPAddressLabel")
self.gridLayout.addWidget(self.uiIPAddressLabel, 1, 0, 1, 1)
self.uiIPAddressLineEdit = QtWidgets.QLineEdit(self.uiNameWizardPage)
self.uiIPAddressLineEdit.setText("")
self.uiIPAddressLineEdit.setObjectName("uiIPAddressLineEdit")
self.gridLayout.addWidget(self.uiIPAddressLineEdit, 1, 1, 1, 1)
TraceNGNodeWizard.addPage(self.uiNameWizardPage)
self.retranslateUi(TraceNGNodeWizard)
QtCore.QMetaObject.connectSlotsByName(TraceNGNodeWizard)
def retranslateUi(self, TraceNGNodeWizard):
_translate = QtCore.QCoreApplication.translate
TraceNGNodeWizard.setWindowTitle(_translate("TraceNGNodeWizard", "New TraceNG node template"))
self.uiServerWizardPage.setTitle(_translate("TraceNGNodeWizard", "Server"))
self.uiServerWizardPage.setSubTitle(_translate("TraceNGNodeWizard", "Please choose a server type to run your new TraceNG node."))
self.uiServerTypeGroupBox.setTitle(_translate("TraceNGNodeWizard", "Server type"))
self.uiRemoteRadioButton.setText(_translate("TraceNGNodeWizard", "Run the TraceNG node on a remote computer"))
self.uiVMRadioButton.setText(_translate("TraceNGNodeWizard", "Run the TraceNG node on the GNS3 VM"))
self.uiLocalRadioButton.setText(_translate("TraceNGNodeWizard", "Run the TraceNG node on your local computer"))
self.uiRemoteServersGroupBox.setTitle(_translate("TraceNGNodeWizard", "Remote server"))
self.uiRemoteServersLabel.setText(_translate("TraceNGNodeWizard", "Run on:"))
self.uiNameWizardPage.setTitle(_translate("TraceNGNodeWizard", "Name and IP address"))
self.uiNameWizardPage.setSubTitle(_translate("TraceNGNodeWizard", "Please choose a descriptive name and IP address for the new TraceNG node."))
self.uiNameLabel.setText(_translate("TraceNGNodeWizard", "Name:"))
self.uiIPAddressLabel.setText(_translate("TraceNGNodeWizard", "IP address:"))

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TraceNGPreferencesPageWidget</class>
<widget class="QWidget" name="TraceNGPreferencesPageWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>623</width>
<height>280</height>
</rect>
</property>
<property name="windowTitle">
<string>TraceNG</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTabWidget" name="uiTabWidget">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="uiGeneralSettingsTabWidget">
<attribute name="title">
<string>Local settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="uiTraceNGPathLabel">
<property name="text">
<string>Path to TraceNG executable:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLineEdit" name="uiTraceNGPathLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="uiTraceNGPathToolButton">
<property name="text">
<string>&amp;Browse...</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>138</width>
<height>17</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="uiRestoreDefaultsPushButton">
<property name="text">
<string>Restore defaults</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
<designerdata>
<property name="gridDeltaX">
<number>10</number>
</property>
<property name="gridDeltaY">
<number>10</number>
</property>
<property name="gridSnapX">
<bool>true</bool>
</property>
<property name="gridSnapY">
<bool>true</bool>
</property>
<property name="gridVisible">
<bool>true</bool>
</property>
</designerdata>
</ui>

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/traceng/ui/traceng_preferences_page.ui'
#
# 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_TraceNGPreferencesPageWidget(object):
def setupUi(self, TraceNGPreferencesPageWidget):
TraceNGPreferencesPageWidget.setObjectName("TraceNGPreferencesPageWidget")
TraceNGPreferencesPageWidget.resize(623, 280)
self.verticalLayout_2 = QtWidgets.QVBoxLayout(TraceNGPreferencesPageWidget)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.uiTabWidget = QtWidgets.QTabWidget(TraceNGPreferencesPageWidget)
self.uiTabWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.uiTabWidget.setObjectName("uiTabWidget")
self.uiGeneralSettingsTabWidget = QtWidgets.QWidget()
self.uiGeneralSettingsTabWidget.setObjectName("uiGeneralSettingsTabWidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.uiGeneralSettingsTabWidget)
self.verticalLayout.setContentsMargins(10, 10, 10, 10)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTraceNGPathLabel = QtWidgets.QLabel(self.uiGeneralSettingsTabWidget)
self.uiTraceNGPathLabel.setObjectName("uiTraceNGPathLabel")
self.verticalLayout.addWidget(self.uiTraceNGPathLabel)
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.uiTraceNGPathLineEdit = QtWidgets.QLineEdit(self.uiGeneralSettingsTabWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiTraceNGPathLineEdit.sizePolicy().hasHeightForWidth())
self.uiTraceNGPathLineEdit.setSizePolicy(sizePolicy)
self.uiTraceNGPathLineEdit.setObjectName("uiTraceNGPathLineEdit")
self.horizontalLayout_5.addWidget(self.uiTraceNGPathLineEdit)
self.uiTraceNGPathToolButton = QtWidgets.QToolButton(self.uiGeneralSettingsTabWidget)
self.uiTraceNGPathToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
self.uiTraceNGPathToolButton.setObjectName("uiTraceNGPathToolButton")
self.horizontalLayout_5.addWidget(self.uiTraceNGPathToolButton)
self.verticalLayout.addLayout(self.horizontalLayout_5)
spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.uiTabWidget.addTab(self.uiGeneralSettingsTabWidget, "")
self.verticalLayout_2.addWidget(self.uiTabWidget)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
spacerItem1 = QtWidgets.QSpacerItem(138, 17, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem1)
self.uiRestoreDefaultsPushButton = QtWidgets.QPushButton(TraceNGPreferencesPageWidget)
self.uiRestoreDefaultsPushButton.setObjectName("uiRestoreDefaultsPushButton")
self.horizontalLayout_2.addWidget(self.uiRestoreDefaultsPushButton)
self.verticalLayout_2.addLayout(self.horizontalLayout_2)
self.retranslateUi(TraceNGPreferencesPageWidget)
self.uiTabWidget.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(TraceNGPreferencesPageWidget)
def retranslateUi(self, TraceNGPreferencesPageWidget):
_translate = QtCore.QCoreApplication.translate
TraceNGPreferencesPageWidget.setWindowTitle(_translate("TraceNGPreferencesPageWidget", "TraceNG"))
self.uiTraceNGPathLabel.setText(_translate("TraceNGPreferencesPageWidget", "Path to TraceNG executable:"))
self.uiTraceNGPathToolButton.setText(_translate("TraceNGPreferencesPageWidget", "&Browse..."))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiGeneralSettingsTabWidget), _translate("TraceNGPreferencesPageWidget", "Local settings"))
self.uiRestoreDefaultsPushButton.setText(_translate("TraceNGPreferencesPageWidget", "Restore defaults"))

View File

@@ -23,12 +23,10 @@ import os
import sys
import shutil
from gns3.qt import QtWidgets
from gns3.local_server_config import LocalServerConfig
from gns3.local_config import LocalConfig
from ..module import Module
from ..module_error import ModuleError
from .virtualbox_vm import VirtualBoxVM
from .settings import VBOX_SETTINGS
from .settings import VBOX_VM_SETTINGS
@@ -78,7 +76,8 @@ class VirtualBox(Module):
vboxmanage_path_osx = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage"
if os.path.exists(vboxmanage_path_osx):
vboxmanage_path = vboxmanage_path_osx
else:
if vboxmanage_path is None:
vboxmanage_path = shutil.which("vboxmanage")
if vboxmanage_path is None:

View File

@@ -22,6 +22,7 @@ Configuration page for VirtualBox VMs.
from gns3.qt import QtWidgets
from gns3.dialogs.node_properties_dialog import ConfigurationError
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.ports.port_name_factory import StandardPortNameFactory
from gns3.node import Node
from ..ui.virtualbox_vm_configuration_page_ui import Ui_virtualBoxVMConfigPageWidget
@@ -178,21 +179,21 @@ class VirtualBoxVMConfigurationPage(QtWidgets.QWidget, Ui_virtualBoxVMConfigPage
symbol_path = self.uiSymbolLineEdit.text()
settings["symbol"] = symbol_path
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
port_name_format = self.uiPortNameFormatLineEdit.text()
if '{0}' not in port_name_format and '{port0}' not in port_name_format and '{port1}' not in port_name_format:
QtWidgets.QMessageBox.critical(self, "Port name format", "The format must contain at least {0}, {port0} or {port1}")
else:
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
port_segment_size = self.uiPortSegmentSizeSpinBox.value()
if port_segment_size and '{1}' not in port_name_format and '{segment0}' not in port_name_format and '{segment1}' not in port_name_format:
QtWidgets.QMessageBox.critical(self, "Port name format", "If the segment size is not 0, the format must contain {1}, {segment0} or {segment1}")
else:
settings["port_segment_size"] = port_segment_size
first_port_name = self.uiFirstPortNameLineEdit.text().strip()
settings["first_port_name"] = self.uiFirstPortNameLineEdit.text().strip()
try:
StandardPortNameFactory(self.uiAdaptersSpinBox.value(), first_port_name, port_name_format, port_segment_size)
except (IndexError, ValueError, KeyError):
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
raise ConfigurationError()
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
settings["port_segment_size"] = port_segment_size
settings["first_port_name"] = first_port_name
settings["ram"] = self.uiVMRamSpinBox.value()
settings["adapter_type"] = self.uiAdapterTypesComboBox.currentText()

View File

@@ -50,7 +50,7 @@ class VMwareVMWizard(VMWizard, Ui_VMwareVMWizard):
if super().validateCurrentPage() is False:
return False
if self.currentPage() == self.uiVirtualBoxWizardPage:
if self.currentPage() == self.uiVMwareWizardPage:
if not self.uiVMListComboBox.count():
QtWidgets.QMessageBox.critical(self, "VMware VMs", "There is no VMware VM available!")
return False
@@ -59,7 +59,7 @@ class VMwareVMWizard(VMWizard, Ui_VMwareVMWizard):
def initializePage(self, page_id):
super().initializePage(page_id)
if self.page(page_id) == self.uiVirtualBoxWizardPage:
if self.page(page_id) == self.uiVMwareWizardPage:
self.uiVMListComboBox.clear()
Controller.instance().getCompute("/vmware/vms", self._compute_id, self._getVMwareVMsFromServerCallback, progressText="Listing VMware VMs...")
@@ -92,6 +92,8 @@ class VMwareVMWizard(VMWizard, Ui_VMwareVMWizard):
"""
index = self.uiVMListComboBox.currentIndex()
if index == -1:
return
vmname = self.uiVMListComboBox.itemText(index)
vminfo = self.uiVMListComboBox.itemData(index)

View File

@@ -22,6 +22,7 @@ Configuration page for VMware VMs.
from gns3.qt import QtWidgets
from gns3.dialogs.node_properties_dialog import ConfigurationError
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.ports.port_name_factory import StandardPortNameFactory
from gns3.node import Node
from ..ui.vmware_vm_configuration_page_ui import Ui_VMwareVMConfigPageWidget
@@ -175,21 +176,21 @@ class VMwareVMConfigurationPage(QtWidgets.QWidget, Ui_VMwareVMConfigPageWidget):
symbol_path = self.uiSymbolLineEdit.text()
settings["symbol"] = symbol_path
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
port_name_format = self.uiPortNameFormatLineEdit.text()
if '{0}' not in port_name_format and '{port0}' not in port_name_format and '{port1}' not in port_name_format:
QtWidgets.QMessageBox.critical(self, "Port name format", "The format must contain at least {0}, {port0} or {port1}")
else:
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
port_segment_size = self.uiPortSegmentSizeSpinBox.value()
if port_segment_size and '{1}' not in port_name_format and '{segment0}' not in port_name_format and '{segment1}' not in port_name_format:
QtWidgets.QMessageBox.critical(self, "Port name format", "If the segment size is not 0, the format must contain {1}, {segment0} or {segment1}")
else:
settings["port_segment_size"] = port_segment_size
first_port_name = self.uiFirstPortNameLineEdit.text().strip()
settings["first_port_name"] = self.uiFirstPortNameLineEdit.text().strip()
try:
StandardPortNameFactory(self.uiAdaptersSpinBox.value(), first_port_name, port_name_format, port_segment_size)
except (IndexError, ValueError, KeyError):
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
raise ConfigurationError()
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
settings["port_segment_size"] = port_segment_size
settings["first_port_name"] = first_port_name
settings["adapter_type"] = self.uiAdapterTypesComboBox.currentText()
settings["use_any_adapter"] = self.uiUseAnyAdapterCheckBox.isChecked()

View File

@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>598</width>
<width>755</width>
<height>453</height>
</rect>
</property>
@@ -81,7 +81,7 @@
</item>
</layout>
</widget>
<widget class="QWizardPage" name="uiVirtualBoxWizardPage">
<widget class="QWizardPage" name="uiVMwareWizardPage">
<property name="title">
<string>VMware Virtual Machine</string>
</property>

View File

@@ -2,8 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/vmware/ui/vmware_vm_wizard.ui'
#
# Created: Tue Sep 20 17:45:46 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_VMwareVMWizard(object):
def setupUi(self, VMwareVMWizard):
VMwareVMWizard.setObjectName("VMwareVMWizard")
VMwareVMWizard.resize(598, 453)
VMwareVMWizard.resize(755, 453)
VMwareVMWizard.setModal(True)
self.uiServerWizardPage = QtWidgets.QWizardPage()
self.uiServerWizardPage.setObjectName("uiServerWizardPage")
@@ -48,14 +47,14 @@ class Ui_VMwareVMWizard(object):
self.gridLayout_8.addWidget(self.uiRemoteServersLabel, 0, 0, 1, 1)
self.gridLayout_2.addWidget(self.uiRemoteServersGroupBox, 1, 0, 1, 1)
VMwareVMWizard.addPage(self.uiServerWizardPage)
self.uiVirtualBoxWizardPage = QtWidgets.QWizardPage()
self.uiVirtualBoxWizardPage.setObjectName("uiVirtualBoxWizardPage")
self.gridLayout = QtWidgets.QGridLayout(self.uiVirtualBoxWizardPage)
self.uiVMwareWizardPage = QtWidgets.QWizardPage()
self.uiVMwareWizardPage.setObjectName("uiVMwareWizardPage")
self.gridLayout = QtWidgets.QGridLayout(self.uiVMwareWizardPage)
self.gridLayout.setObjectName("gridLayout")
self.uiVMListLabel = QtWidgets.QLabel(self.uiVirtualBoxWizardPage)
self.uiVMListLabel = QtWidgets.QLabel(self.uiVMwareWizardPage)
self.uiVMListLabel.setObjectName("uiVMListLabel")
self.gridLayout.addWidget(self.uiVMListLabel, 0, 0, 1, 1)
self.uiVMListComboBox = QtWidgets.QComboBox(self.uiVirtualBoxWizardPage)
self.uiVMListComboBox = QtWidgets.QComboBox(self.uiVMwareWizardPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -63,11 +62,11 @@ class Ui_VMwareVMWizard(object):
self.uiVMListComboBox.setSizePolicy(sizePolicy)
self.uiVMListComboBox.setObjectName("uiVMListComboBox")
self.gridLayout.addWidget(self.uiVMListComboBox, 0, 1, 1, 1)
self.uiBaseVMCheckBox = QtWidgets.QCheckBox(self.uiVirtualBoxWizardPage)
self.uiBaseVMCheckBox = QtWidgets.QCheckBox(self.uiVMwareWizardPage)
self.uiBaseVMCheckBox.setEnabled(True)
self.uiBaseVMCheckBox.setObjectName("uiBaseVMCheckBox")
self.gridLayout.addWidget(self.uiBaseVMCheckBox, 1, 0, 1, 2)
VMwareVMWizard.addPage(self.uiVirtualBoxWizardPage)
VMwareVMWizard.addPage(self.uiVMwareWizardPage)
self.retranslateUi(VMwareVMWizard)
QtCore.QMetaObject.connectSlotsByName(VMwareVMWizard)
@@ -82,8 +81,8 @@ class Ui_VMwareVMWizard(object):
self.uiLocalRadioButton.setText(_translate("VMwareVMWizard", "Run this VMware VM on my local computer"))
self.uiRemoteServersGroupBox.setTitle(_translate("VMwareVMWizard", "Remote servers"))
self.uiRemoteServersLabel.setText(_translate("VMwareVMWizard", "Run on server:"))
self.uiVirtualBoxWizardPage.setTitle(_translate("VMwareVMWizard", "VMware Virtual Machine"))
self.uiVirtualBoxWizardPage.setSubTitle(_translate("VMwareVMWizard", "Please choose a VMware virtual machine from the list."))
self.uiVMwareWizardPage.setTitle(_translate("VMwareVMWizard", "VMware Virtual Machine"))
self.uiVMwareWizardPage.setSubTitle(_translate("VMwareVMWizard", "Please choose a VMware virtual machine from the list."))
self.uiVMListLabel.setText(_translate("VMwareVMWizard", "VM list:"))
self.uiBaseVMCheckBox.setText(_translate("VMwareVMWizard", "Use as a linked base VM (experimental)"))

View File

@@ -77,10 +77,10 @@ class VPCS(Module):
# save the settings
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
server_settings = copy.copy(self._settings)
if server_settings["vpcs_path"]:
server_settings = {}
if self._settings["vpcs_path"]:
# save some settings to the server config file
server_settings["vpcs_path"] = os.path.normpath(server_settings["vpcs_path"])
server_settings["vpcs_path"] = os.path.normpath(self._settings["vpcs_path"])
config = LocalServerConfig.instance()
config.saveSettings(self.__class__.__name__, server_settings)

View File

@@ -22,7 +22,7 @@ import pathlib
from gns3.controller import Controller
from gns3.ports.ethernet_port import EthernetPort
from gns3.ports.serial_port import SerialPort
from gns3.utils.bring_to_front import bring_window_to_front_from_title
from gns3.utils.bring_to_front import bring_window_to_front_from_process_name, bring_window_to_front_from_title
from gns3.qt import QtGui, QtCore
from .base_node import BaseNode
@@ -100,15 +100,11 @@ class Node(BaseNode):
for key in data:
if key not in self._settings or self._settings[key] != data[key]:
changed = True
if not changed:
return
# If it's the initialization we don't resend it
# to the server
if self._settings["x"] is not None:
self._update(data)
else:
self._settings.update(data)
self._update(data)
def setSymbol(self, symbol):
self._settings["symbol"] = symbol
@@ -227,10 +223,9 @@ class Node(BaseNode):
Update the node on the controller
"""
if self.initialized():
log.debug("{} is updating settings: {}".format(self.name(), params))
body = self._prepareBody(params)
self.controllerHttpPut("/nodes/{node_id}".format(node_id=self._node_id), self.updateNodeCallback, body=body, timeout=timeout, showProgress=False)
log.debug("{} is updating settings: {}".format(self.name(), params))
body = self._prepareBody(params)
self.controllerHttpPut("/nodes/{node_id}".format(node_id=self._node_id), self.updateNodeCallback, body=body, timeout=timeout, showProgress=False)
def updateNodeCallback(self, result, error=False, **kwargs):
"""
@@ -263,6 +258,7 @@ class Node(BaseNode):
node_id=self._node_id),
self._duplicateCallback,
body=body,
progressText="Duplicating node {}...".format(self.name()),
timeout=None)
def _duplicateCallback(self, result, error=False, **kwargs):
@@ -355,7 +351,6 @@ class Node(BaseNode):
return False
result = self._parseResponse(result)
self._created = True
self._createCallback(result)
if self._loading:
@@ -386,6 +381,8 @@ class Node(BaseNode):
"""
if not skip_controller:
for link in self.links():
link.setDeleting()
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback)
else:
self.deleted_signal.emit()
@@ -422,7 +419,7 @@ class Node(BaseNode):
return
log.debug("{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, timeout=None, progressText="{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, timeout=None, showProgress=False)
def _startCallback(self, result, error=False, **kwargs):
"""
@@ -448,7 +445,7 @@ class Node(BaseNode):
return
log.debug("{} is stopping".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/stop".format(node_id=self._node_id), self._stopCallback, progressText="{} is stopping".format(self.name()), timeout=None)
self.controllerHttpPost("/nodes/{node_id}/stop".format(node_id=self._node_id), self._stopCallback, showProgress=False, timeout=None)
def _stopCallback(self, result, error=False, **kwargs):
"""
@@ -551,6 +548,14 @@ class Node(BaseNode):
"""
if self.status() == Node.started:
console_command = self.consoleCommand()
if console_command:
process_name = console_command.split()[0]
if bring_window_to_front_from_process_name(process_name, self.name()):
return True
else:
log.debug("Could not find process name '' and window title '{}' to bring it to front".format(process_name, self.name()))
if bring_window_to_front_from_title(self.name()):
return True
else:

View File

@@ -22,7 +22,7 @@ on the QGraphics scene.
import tempfile
import json
import sip
from .qt import sip
from .qt import QtCore, QtGui, QtWidgets, qpartial
from .modules import MODULES
@@ -98,7 +98,7 @@ class NodesView(QtWidgets.QTreeWidget):
if self._show_installed_appliances:
for appliance in ApplianceManager.instance().appliances():
if category is not None and category != CATEGORY_TO_ID[appliance["category"]]:
if category is not None and category != CATEGORY_TO_ID.get(appliance["category"], "guest"):
continue
if search != "" and search.lower() not in appliance["name"].lower():
continue
@@ -117,7 +117,7 @@ class NodesView(QtWidgets.QTreeWidget):
if appliance["builtin"] and not self._show_builtin_available_appliances:
continue
if category is not None and category != CATEGORY_TO_ID[appliance["category"]]:
if category is not None and category != CATEGORY_TO_ID.get(appliance["category"], "guest"):
continue
if search != "" and search.lower() not in appliance["name"].lower():
continue
@@ -131,7 +131,6 @@ class NodesView(QtWidgets.QTreeWidget):
item.setData(1, QtCore.Qt.UserRole, "appliance_template")
item.setSizeHint(0, QtCore.QSize(32, 32))
Controller.instance().getSymbolIcon(appliance.get("symbol"), qpartial(self._setItemIcon, item), fallback=":/symbols/" + appliance["category"] + ".svg")
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
def _setItemIcon(self, item, icon):
@@ -155,7 +154,7 @@ class NodesView(QtWidgets.QTreeWidget):
"""
# Check that an item has been selected and right click
if self.currentItem() is not None and event.button() == QtCore.Qt.RightButton:
if event.button() == QtCore.Qt.RightButton:
self._showContextualMenu()
event.accept()
return
@@ -175,10 +174,12 @@ class NodesView(QtWidgets.QTreeWidget):
# retrieve the node class from the item data
if item.data(1, QtCore.Qt.UserRole) == "appliance_template":
f = tempfile.NamedTemporaryFile(mode="w+", suffix=".builtin.gns3a", delete=False)
json.dump(item.data(0, QtCore.Qt.UserRole), f)
f.close()
self._getMainWindow().loadPath(f.name)
try:
with tempfile.NamedTemporaryFile(mode="w+", suffix=".builtin.gns3a", delete=False) as f:
json.dump(item.data(0, QtCore.Qt.UserRole), f)
self._getMainWindow().loadPath(f.name)
except OSError as e:
QtWidgets.QMessageBox.critical(self, "Appliance", "Cannot install appliance: {}".format(e))
return
icon = item.icon(0)
@@ -199,40 +200,46 @@ class NodesView(QtWidgets.QTreeWidget):
event.accept()
def _showContextualMenu(self):
menu = QtWidgets.QMenu()
refresh_action = QtWidgets.QAction("Refresh templates", menu)
refresh_action.setIcon(QtGui.QIcon(":/icons/reload.svg"))
refresh_action.triggered.connect(self.refresh)
menu.addAction(refresh_action)
item = self.currentItem()
node = ApplianceManager.instance().getAppliance(item.data(0, QtCore.Qt.UserRole))
if not node:
return
for module in MODULES:
if node["node_type"] == "dynamips":
node_class = module.getNodeType(node["node_type"], node["platform"])
else:
node_class = module.getNodeType(node["node_type"])
if node_class:
break
# We can not edit stuff like EthernetSwitch
# or without config template like VPCS
if not node["builtin"] and hasattr(module, "vmConfigurationPage"):
vm = None
for vm_key, vm in module.instance().VMs().items():
if vm["name"] == node["name"]:
break
if vm is None:
if item:
node = ApplianceManager.instance().getAppliance(item.data(0, QtCore.Qt.UserRole))
if not node:
return
menu = QtWidgets.QMenu()
configuration = QtWidgets.QAction("Configure Template", menu)
configuration.setIcon(QtGui.QIcon(":/icons/configuration.svg"))
configuration.triggered.connect(qpartial(self._configurationSlot, vm, module))
menu.addAction(configuration)
for module in MODULES:
if node["node_type"] == "dynamips":
node_class = module.getNodeType(node["node_type"], node["platform"])
else:
node_class = module.getNodeType(node["node_type"])
configuration = QtWidgets.QAction("Delete Template", menu)
configuration.setIcon(QtGui.QIcon(":/icons/delete.svg"))
configuration.triggered.connect(qpartial(self._deleteSlot, vm_key, vm, module))
menu.addAction(configuration)
if node_class:
break
menu.exec_(QtGui.QCursor.pos())
# We can not edit stuff like EthernetSwitch
# or without config template like VPCS
if not node["builtin"] and hasattr(module, "vmConfigurationPage"):
vm = None
for vm_key, vm in module.instance().VMs().items():
if vm["name"] == node["name"]:
break
if vm is not None:
configure_action = QtWidgets.QAction("Configure template", menu)
configure_action.setIcon(QtGui.QIcon(":/icons/configuration.svg"))
configure_action.triggered.connect(qpartial(self._configurationSlot, vm, module))
menu.addAction(configure_action)
delete_action = QtWidgets.QAction("Delete template", menu)
delete_action.setIcon(QtGui.QIcon(":/icons/delete.svg"))
delete_action.triggered.connect(qpartial(self._deleteSlot, vm_key, vm, module))
menu.addAction(delete_action)
menu.exec_(QtGui.QCursor.pos())
def _configurationSlot(self, vm, module, source):
@@ -249,7 +256,7 @@ class NodesView(QtWidgets.QTreeWidget):
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
vms = module.instance().VMs()
vms.pop(vm_key)
vms.pop(vm_key, None)
module.instance().setVMs(vms)
LocalConfig.instance().writeConfig()
self.refresh()

View File

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

View File

@@ -316,8 +316,11 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
self.uiSceneWidthSpinBox.setValue(settings["scene_width"])
self.uiSceneHeightSpinBox.setValue(settings["scene_height"])
self.uiGridSizeSpinBox.setValue(settings["grid_size"])
self.uiRectangleSelectedItemCheckBox.setChecked(settings["draw_rectangle_selected_item"])
self.uiDrawLinkStatusPointsCheckBox.setChecked(settings["draw_link_status_points"])
self.uiShowInterfaceLabelsOnNewProject.setChecked(settings["show_interface_labels_on_new_project"])
self.uiLimitSizeNodeSymbolCheckBox.setChecked(settings["limit_size_node_symbols"])
qt_font = QtGui.QFont()
if qt_font.fromString(settings["default_label_font"]):
@@ -378,8 +381,11 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
new_graphics_view_settings = {"scene_width": self.uiSceneWidthSpinBox.value(),
"scene_height": self.uiSceneHeightSpinBox.value(),
"grid_size": self.uiGridSizeSpinBox.value(),
"draw_rectangle_selected_item": self.uiRectangleSelectedItemCheckBox.isChecked(),
"draw_link_status_points": self.uiDrawLinkStatusPointsCheckBox.isChecked(),
"show_interface_labels_on_new_project": self.uiShowInterfaceLabelsOnNewProject.isChecked(),
"limit_size_node_symbols": self.uiLimitSizeNodeSymbolCheckBox.isChecked(),
"default_label_font": self.uiDefaultLabelStylePlainTextEdit.font().toString(),
"default_label_color": self._default_label_color.name()}
MainWindow.instance().uiGraphicsView.setSettings(new_graphics_view_settings)

View File

@@ -20,7 +20,7 @@ Configuration page for GNS3 VM
"""
import copy
from gns3.qt import QtWidgets, QtCore, qpartial, qslot
from gns3.qt import QtWidgets, QtCore, qpartial, qslot, sip_is_deleted
from gns3.controller import Controller
from ..ui.gns3_vm_preferences_page_ui import Ui_GNS3VMPreferencesPageWidget
@@ -74,7 +74,11 @@ class GNS3VMPreferencesPage(QtWidgets.QWidget, Ui_GNS3VMPreferencesPageWidget):
"""
Controller.instance().get("/gns3vm", self._getSettingsCallback)
@qslot
def _getSettingsCallback(self, result, error=False, **kwargs):
if sip_is_deleted(self.uiRamSpinBox) or sip_is_deleted(self):
return
if error:
if "message" in result:
log.error("Error while getting settings : {}".format(result["message"]))
@@ -95,10 +99,15 @@ class GNS3VMPreferencesPage(QtWidgets.QWidget, Ui_GNS3VMPreferencesPageWidget):
@qslot
def _listEnginesCallback(self, result, error=False, ignore_error=False, **kwargs):
if sip_is_deleted(self.uiGNS3VMEngineComboBox) or sip_is_deleted(self):
return
if error:
if "message" in result:
log.error("Error while getting the list of GNS3 VM engines : {}".format(result["message"]))
return
self.uiGNS3VMEngineComboBox.clear()
self._engines = result
# We insert first the current engine to avoid triggering unexpected signals
@@ -115,22 +124,24 @@ class GNS3VMPreferencesPage(QtWidgets.QWidget, Ui_GNS3VMPreferencesPageWidget):
if engine_id:
Controller.instance().get("/gns3vm/engines/{}/vms".format(engine_id), qpartial(self._listVMsCallback, ignore_error=ignore_error))
@qslot
def _listVMsCallback(self, result, ignore_error=False, error=False, **kwargs):
if error:
if "message" in result:
if not ignore_error:
QtWidgets.QMessageBox.critical(self, "List vms", "Error while listing vms: {}".format(result["message"]))
return
self.uiVMListComboBox.clear()
for vm in result:
self.uiVMListComboBox.addItem(vm["vmname"], vm["vmname"])
index = self.uiVMListComboBox.findText(self._settings["vmname"])
if index == -1:
index = self.uiVMListComboBox.findText("GNS3 VM")
if not sip_is_deleted(self.uiVMListComboBox):
self.uiVMListComboBox.clear()
for vm in result:
self.uiVMListComboBox.addItem(vm["vmname"], vm["vmname"])
index = self.uiVMListComboBox.findText(self._settings["vmname"])
if index == -1:
index = 0
self.uiVMListComboBox.setCurrentIndex(index)
self._initialized = True
index = self.uiVMListComboBox.findText("GNS3 VM")
if index == -1:
index = 0
self.uiVMListComboBox.setCurrentIndex(index)
self._initialized = True
def savePreferences(self):
"""
@@ -162,5 +173,5 @@ class GNS3VMPreferencesPage(QtWidgets.QWidget, Ui_GNS3VMPreferencesPageWidget):
def _saveSettingsCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while save settings: {}".format(result["message"]))
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while saving settings: {}".format(result["message"]))
return

View File

@@ -19,7 +19,7 @@
Base class for port objects.
"""
import sip
from ..qt import sip
from ..qt import qslot
@@ -232,7 +232,7 @@ class Port:
if self._destination_node and self._destination_port:
if short:
return "<-> {port} {name}".format(port=self._destination_port.shortName(),
return "<=> {port} {name}".format(port=self._destination_port.shortName(),
name=self._destination_node.name())
return "connected to {name} on port {port}".format(name=self._destination_node.name(),
port=self._destination_port.name())

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python
#
# Copyright (C) 2018 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
log = logging.getLogger(__name__)
class StandardPortNameFactory:
"""
Generate default port names.
"""
def __new__(cls, ethernet_adapters, first_port_name, port_name_format, port_segment_size):
ports = []
adapter_number = interface_number = segment_number = 0
for adapter_number in range(adapter_number, ethernet_adapters + adapter_number):
if first_port_name and adapter_number == 0:
port_name = first_port_name
else:
port_name = port_name_format.format(interface_number,
segment_number,
adapter=adapter_number,
**cls._generate_replacement(interface_number, segment_number))
interface_number += 1
if port_segment_size:
if interface_number % port_segment_size == 0:
segment_number += 1
interface_number = 0
else:
segment_number += 1
ports.append(port_name)
return ports
@staticmethod
def _generate_replacement(interface_number, segment_number):
"""
This will generate replacement string for
{port0} => {port9}
{segment0} => {segment9}
"""
replacements = {}
for i in range(0, 9):
replacements["port" + str(i)] = interface_number + i
replacements["segment" + str(i)] = segment_number + i
return replacements

View File

@@ -15,7 +15,7 @@
# 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
from .qt import sip
import time
from contextlib import contextmanager
@@ -113,7 +113,7 @@ class Progress(QtCore.QObject):
@qslot
def _rejectSlot(self, *args):
if self._progress_dialog is not None and not sip.isdeleted(self._progress_dialog) or self._progress_dialog.wasCanceled():
if self._progress_dialog is not None and (not sip.isdeleted(self._progress_dialog) or self._progress_dialog.wasCanceled()):
self._progress_dialog.deleteLater()
self._progress_dialog = None
self._cancelSlot()
@@ -189,7 +189,10 @@ class Progress(QtCore.QObject):
# Due to Qt limitations for large numbers (above 32bit int) we calculate "progress" ourselves
current, maximum = self._normalize(query['current'], query['maximum'])
progress_dialog.setMaximum(maximum)
progress_dialog.setValue(current)
try:
progress_dialog.setValue(current)
except OverflowError:
progress_dialog.setValue(100)
if text and query["maximum"] > 1000:
text += "\n{} / {}".format(human_filesize(query["current"]), human_filesize(query["maximum"]))

View File

@@ -49,6 +49,7 @@ class Project(QtCore.QObject):
# Called when project is fully loaded
project_loaded_signal = QtCore.Signal()
def __init__(self):
self._id = None
@@ -60,14 +61,20 @@ class Project(QtCore.QObject):
self._auto_open = False
self._auto_close = False
graphic_settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, GRAPHICS_VIEW_SETTINGS)
config = LocalConfig.instance()
graphic_settings = LocalConfig.instance().loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS)
self._scene_width = graphic_settings["scene_width"]
self._scene_height = graphic_settings["scene_height"]
self._zoom = graphic_settings.get("zoom", None)
self._show_layers = graphic_settings.get("show_layers", False)
self._snap_to_grid = graphic_settings.get("snap_to_grid", False)
self._show_grid = graphic_settings.get("show_grid", False)
self._grid_size = graphic_settings.get("grid_size", 75)
self._show_interface_labels = graphic_settings.get("show_interface_labels", False)
self._show_interface_labels_on_new_project = config.showInterfaceLabelsOnNewProject()
self._variables = None
self._supplier = None
self._name = "untitled"
self._filename = None
@@ -176,6 +183,21 @@ class Project(QtCore.QObject):
"""
return self._show_grid
def setGridSize(self, grid_size):
"""
Sets the grid size
"""
self._grid_size = grid_size
def gridSize(self):
"""
Returns the grid size
:return: integer
"""
return self._grid_size
def setShowInterfaceLabels(self, show_interface_labels):
"""
Sets show interface labels mode
@@ -189,6 +211,32 @@ class Project(QtCore.QObject):
"""
return self._show_interface_labels
def setVariables(self, variables):
"""
Sets variables of project
"""
self._variables = variables
def variables(self):
"""
Returns variables assigned to the project
:return: boolean
"""
return self._variables
def setSupplier(self, supplier):
"""
Sets supplier of project
"""
self._supplier = supplier
def supplier(self):
"""
Returns supplier
:return: boolean
"""
return self._supplier
def setName(self, name):
"""
Set project name
@@ -265,12 +313,16 @@ class Project(QtCore.QObject):
"""
Duplicate a project
"""
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=self._id), qpartial(self._duplicateCallback, callback), body={"name": name, "path": path}, timeout=None)
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=self._id),
qpartial(self._duplicateCallback, callback),
body={"name": name, "path": path},
progressText="Duplicating project '{}'...".format(name),
timeout=None)
def _duplicateCallback(self, callback, result, error=False, **kwargs):
if error:
if "message" in result:
QtWidgets.QMessageBox.critical(None, "Duplicate project", "Error while duplicating: {}".format(result["message"]))
log.error("Error while duplicating project: {}".format(result["message"]))
return
if callback:
callback(result["project_id"])
@@ -373,7 +425,8 @@ class Project(QtCore.QObject):
"""
body = {
"name": self._name,
"path": self.filesDir()
"path": self.filesDir(),
"show_interface_labels": self._show_interface_labels_on_new_project
}
Controller.instance().post("/projects", self._projectCreatedCallback, body=body)
@@ -392,7 +445,10 @@ class Project(QtCore.QObject):
"show_layers": self._show_layers,
"snap_to_grid": self._snap_to_grid,
"show_grid": self._show_grid,
"show_interface_labels": self._show_interface_labels
"grid_size": self._grid_size,
"show_interface_labels": self._show_interface_labels,
"variables": self._variables,
"supplier": self._supplier
}
self.put("", self._projectUpdatedCallback, body=body)
@@ -412,7 +468,9 @@ class Project(QtCore.QObject):
self._closed = False
self._closing = False
self._startListenNotifications()
self.project_updated_signal.emit()
self.project_loaded_signal.emit()
def _parseResponse(self, result):
"""
@@ -431,6 +489,12 @@ class Project(QtCore.QObject):
self._show_layers = result.get("show_layers", False)
self._snap_to_grid = result.get("snap_to_grid", False)
self._show_grid = result.get("show_grid", False)
self._variables = result.get("variables", None)
self._supplier = result.get("supplier", None)
grid_size = result.get("grid_size", None)
if grid_size:
self._grid_size = grid_size
self._show_interface_labels = result.get("show_interface_labels", False)
def load(self, path=None):
@@ -444,7 +508,7 @@ class Project(QtCore.QObject):
def _projectOpenCallback(self, result, error=False, **kwargs):
if error:
self.project_creation_error_signal.emit(result["message"])
self.project_creation_error_signal.emit(result.get("message", "unknown"))
return
self._parseResponse(result)
@@ -459,7 +523,7 @@ class Project(QtCore.QObject):
def _listNodesCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while listing project: {}".format(result["message"]))
log.error("Error while listing project: {}".format(result.get("message", "unknown")))
return
topo = Topology.instance()
for node in result:
@@ -468,7 +532,7 @@ class Project(QtCore.QObject):
def _listLinksCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while listing links: {}".format(result["message"]))
log.error("Error while listing links: {}".format(result.get("message", "unknown")))
return
topo = Topology.instance()
for link in result:
@@ -477,7 +541,7 @@ class Project(QtCore.QObject):
def _listDrawingsCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while listing drawings: {}".format(result["message"]))
log.error("Error while listing drawings: {}".format(result.get("message", "unknown")))
return
topo = Topology.instance()
for drawing in result:

View File

@@ -24,7 +24,6 @@ Compatibility layer for Qt bindings, so it is easier to switch to PySide if need
import sys
import sip
import os
import re
import inspect
@@ -33,12 +32,17 @@ import functools
import logging
log = logging.getLogger("qt/__init__.py")
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, Qt
try:
from PyQt5 import sip
except ImportError:
import sip
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets
sys.modules[__name__ + '.QtCore'] = QtCore
sys.modules[__name__ + '.QtGui'] = QtGui
sys.modules[__name__ + '.QtNetwork'] = QtNetwork
sys.modules[__name__ + '.QtWidgets'] = QtWidgets
sys.modules[__name__ + '.Qt'] = Qt
sys.modules[__name__ + '.sip'] = sip
try:
from PyQt5 import QtSvg
@@ -115,14 +119,20 @@ class LogQMessageBox(QtWidgets.QMessageBox):
if message.startswith("QXcbConnection"): # Qt noise not relevant
return
LogQMessageBox._get_logger().critical(re.sub(r"<[^<]+?>", "", message), stack_info=LogQMessageBox.stack_info())
if sip_is_deleted(parent):
if parent is False:
# special case to display a QMessageBox before the main window is created.
parent = None
elif sip_is_deleted(parent):
return
return super(QtWidgets.QMessageBox, QtWidgets.QMessageBox).critical(parent, title, message, *args)
@staticmethod
def warning(parent, title, message, *args):
LogQMessageBox._get_logger().warning(re.sub(r"<[^<]+?>", "", message))
if sip_is_deleted(parent):
if parent is False:
# special case to display a QMessageBox before the main window is created.
parent = None
elif sip_is_deleted(parent):
return
return super(QtWidgets.QMessageBox, QtWidgets.QMessageBox).warning(parent, title, message, *args)
@@ -130,7 +140,7 @@ class LogQMessageBox(QtWidgets.QMessageBox):
def _get_logger():
"""
Return a logger in the context of the caller
in order to have the correct informations in the log
in order to have the correct information in the log
"""
if sys.version_info < (3, 5):
return logging.getLogger('qt')
@@ -230,31 +240,6 @@ class StatsQtWidgetsQDialog(QtWidgets.QDialog):
QtWidgets.QDialog = StatsQtWidgetsQDialog
class PatchNetworkAccessManager(QNetworkAccessManager):
"""
Patch the network acces manager in order to solve
hibernation issues on windows and Linux
See: https://github.com/GNS3/gns3-gui/issues/2104
"""
def __init__(self, *params, **kwargs):
super().__init__(*params, **kwargs)
self.setNetworkAccessible(self.Accessible)
self.networkAccessibleChanged.connect(self.networkAccessibleChangedSlot)
def networkAccessibleChangedSlot(self, status):
"""
When we lost the network we switch to another available network
"""
if status == self.Accessible:
return
self.setConfiguration(QtNetwork.QNetworkConfigurationManager().defaultConfiguration())
QtNetwork.QNetworkAccessManager = PatchNetworkAccessManager
def qpartial(func, *args, **kwargs):
"""
A functools partial that you can use on qobject. If the targeted qobject is

View File

@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import re
import xml.etree.ElementTree as ET
from . import QtCore
@@ -35,12 +36,14 @@ class QImageSvgRenderer(QtSvg.QSvgRenderer):
"""
def __init__(self, path_or_data=None, fallback=None):
super().__init__()
self._fallback = fallback
self._svg = """<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{width}" height="{height}"></svg>"""
self.load(path_or_data)
def load(self, path_or_data):
try:
path_exists = os.path.exists(path_or_data)
except ValueError: # On windows we can get an error because the path is too long (it's the svg data)
@@ -60,6 +63,14 @@ class QImageSvgRenderer(QtSvg.QSvgRenderer):
res = super().load(path_or_data)
# If we can't render a SVG we load and base64 the image to create a SVG
if self.isValid():
if not path_or_data.startswith(":") and path_exists:
try:
with open(path_or_data, "rb") as f:
self._svg = f.read().decode()
except UnicodeError as e:
log.error("Could not decode '{}' content: {}".format(path_or_data, e))
except OSError as e:
log.error("Could not read '{}': {}".format(path_or_data, e))
return res
except ET.ParseError:
pass
@@ -84,6 +95,43 @@ class QImageSvgRenderer(QtSvg.QSvgRenderer):
res = super().load(self._svg.encode())
return res
def resize(self, new_height, new_width=None):
if not self.isValid():
log.error("QSvgRenderer is not valid")
return
size = self.defaultSize()
height = size.height()
width = size.width()
if not new_width:
new_width = round(width / height * new_height)
add_attr = []
svg_header, svg_tag, svg_data = re.split(r'(<svg[^>]*>)', self._svg, maxsplit=1)
attr = 'height="{}"'.format(new_height)
svg_tag, count = re.subn(r'height="[^"]*"', attr, svg_tag, count=1)
if not count:
add_attr.append(attr)
attr = 'width="{}"'.format(new_width)
svg_tag, count = re.subn(r'width="[^"]*"', attr, svg_tag, count=1)
if not count:
add_attr.append(attr)
if 'viewBox="' not in svg_tag:
add_attr.append('viewBox="0 0 {} {}"'.format(width, height))
if add_attr:
svg_tag = svg_tag.replace('<svg', '<svg ' + ' '.join(add_attr), 1)
svg_image = svg_header + svg_tag + svg_data
res = super().load(svg_image.encode())
if res is False:
log.error("Could not resize QSvgRenderer")
def svg(self):
"""
:returns: SVG source code

View File

@@ -20,7 +20,10 @@
import json
import os
import urllib
import shutil
from ssl import CertificateError
from gns3.controller import Controller
from ..local_config import LocalConfig
from ..local_server_config import LocalServerConfig
from ..settings import LOCAL_SERVER_SETTINGS
@@ -43,13 +46,17 @@ class Config:
:params path: Path of the configuration file, otherwise detect it on the system
"""
self.path = path
if self.path is None:
self.path = LocalConfig.instance().configFilePath()
self._path = path
if self._path is None:
self._path = LocalConfig.instance().configFilePath()
with open(self.path, encoding="utf-8") as f:
with open(self._path, encoding="utf-8") as f:
self._config = json.load(f)
@property
def path(self):
return self._path
@property
def images_dir(self):
"""
@@ -91,19 +98,33 @@ class Config:
:param name: Appliance name
:returns: True if name is not already used
"""
for item in self._config["Qemu"].get("vms", []):
appliance_names = []
if "Qemu" in self._config:
appliance_names.extend(self._config["Qemu"].get("vms", []))
if "IOU" in self._config:
appliance_names.extend(self._config["IOU"].get("devices", []))
if "Dynamips" in self._config:
appliance_names.extend(self._config["Dynamips"].get("routers", []))
if "Docker" in self._config:
appliance_names.extend(self._config["Docker"].get("containers", []))
for item in appliance_names:
if item["name"] == name:
return False
return True
def add_appliance(self, appliance_config, server):
def add_appliance(self, appliance_config, server, controller_symbols=None):
"""
Add appliance to the user configuration
:param appliance_config: Dictionary with appliance configuration
:param server
:param controller_symbols: Symbols located on controller
"""
if controller_symbols is None:
controller_symbols = []
new_config = {
"server": server,
"name": appliance_config["name"]
@@ -124,7 +145,7 @@ class Config:
new_config["category"] = 1
if "symbol" in appliance_config:
new_config["symbol"] = self._set_symbol(appliance_config["symbol"])
new_config["symbol"] = self._set_symbol(appliance_config["symbol"], controller_symbols)
if new_config.get("symbol") is None:
if appliance_config["category"] == "guest":
@@ -157,7 +178,7 @@ class Config:
if "docker" in appliance_config:
self._add_docker_config(new_config, appliance_config)
return
raise ConfigException("{} no configuration found for know emulators".format(new_config["name"]))
raise ConfigException("{} no configuration found for known emulators".format(new_config["name"]))
def _add_docker_config(self, new_config, appliance_config):
new_config["adapters"] = appliance_config["docker"]["adapters"]
@@ -167,6 +188,7 @@ class Config:
new_config["console_type"] = appliance_config["docker"].get("console_type", "telnet")
new_config["console_http_port"] = appliance_config["docker"].get("console_http_port", 80)
new_config["console_http_path"] = appliance_config["docker"].get("console_http_path", "/")
new_config["extra_hosts"] = appliance_config["docker"].get("extra_hosts", "")
self._config["Docker"]["containers"].append(new_config)
def _add_dynamips_config(self, new_config, appliance_config):
@@ -257,8 +279,8 @@ class Config:
else:
new_config["qemu_path"] = "qemu-system-{}".format(appliance_config["qemu"]["arch"])
if "boot_priority" in appliance_config:
new_config["boot_priority"] = appliance_config["boot_priority"]
if "boot_priority" in appliance_config["qemu"]:
new_config["boot_priority"] = appliance_config["qemu"]["boot_priority"]
if "first_port_name" in appliance_config:
new_config["first_port_name"] = appliance_config["first_port_name"]
@@ -276,9 +298,9 @@ class Config:
self._config["Qemu"].setdefault("vms", [])
self._config["Qemu"]["vms"].append(new_config)
def _set_symbol(self, symbol):
def _set_symbol(self, symbol, controller_symbols):
"""
Download symbol for the web if need
Check if exists on controller or download symbol from the web if needed
"""
# GNS3 builtin symbol
@@ -289,11 +311,25 @@ class Config:
if os.path.exists(path):
return os.path.basename(path)
is_symbol_on_controller = len([s for s in controller_symbols
if s['symbol_id'] == symbol]) > 0
if is_symbol_on_controller:
cached = Controller.instance().getStaticCachedPath(symbol)
if os.path.exists(cached):
try:
shutil.copy(cached, path)
except IOError as e:
log.warning("Cannot copy cached symbol from `{}` to `{}` due `{}`".format(
cached, path, str(e)
))
return symbol
url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol)
try:
urllib.request.urlretrieve(url, path)
return os.path.basename(path)
except OSError:
except (OSError, CertificateError):
return None
def _relative_image_path(self, image_dir_type, path):

View File

@@ -95,20 +95,28 @@ class Image:
self._md5sum = from_cache
return self._md5sum
if os.path.exists(self.path + ".md5sum"):
with open(self.path + ".md5sum", encoding="utf-8") as f:
self._md5sum = f.read()
return self._md5sum
md5_file = self.path + ".md5sum"
if os.path.exists(md5_file):
try:
with open(md5_file) as f:
self._md5sum = f.read().strip()
return self._md5sum
except (OSError, UnicodeDecodeError) as e:
log.debug("Could not read '{}': {}".format(md5_file, e))
if not os.path.isfile(self.path):
try:
if not os.path.isfile(self.path):
return None
m = hashlib.md5()
with open(self.path, "rb") as f:
while True:
buf = f.read(4096)
if not buf:
break
m.update(buf)
except (OSError, PermissionError) as e:
log.debug("Cannot access '{}': {}".format(self.path, e))
return None
m = hashlib.md5()
with open(self.path, "rb") as f:
while True:
buf = f.read(4096)
if not buf:
break
m.update(buf)
self._md5sum = m.hexdigest()
Image._cache[self.path] = self._md5sum
return self._md5sum

View File

@@ -87,11 +87,11 @@ class Registry(QtCore.QObject):
for directory in self._images_dirs:
log.debug("Search images %s (%s) in %s", filename, md5sum, directory)
if os.path.exists(directory):
for file in os.listdir(directory):
if not file.endswith(".md5sum") and not file.startswith("."):
path = os.path.join(directory, file)
try:
try:
if os.path.exists(directory):
for file in os.listdir(directory):
if not file.endswith(".md5sum") and not file.startswith("."):
path = os.path.join(directory, file)
if os.path.isfile(path):
if md5sum is None:
if filename == os.path.basename(path):
@@ -105,7 +105,6 @@ class Registry(QtCore.QObject):
if image.md5sum == md5sum:
log.debug("Found images %s (%s) in %s", filename, md5sum, image.path)
return image
except (OSError, PermissionError) as e:
log.error("Can't scan {}: {}".format(path, str(e)))
except (OSError, PermissionError) as e:
log.error("Cannot scan {}: {}".format(path, e))
return None

View File

@@ -22,6 +22,7 @@
"enum": [
"router",
"multilayer_switch",
"switch",
"firewall",
"guest"
],
@@ -131,6 +132,10 @@
"console_http_path": {
"description": "Path of the web interface",
"type": "string"
},
"extra_hosts": {
"description": "Hosts which will be written to /etc/hosts into container" ,
"type": "string"
}
},
"required": [
@@ -283,8 +288,8 @@
"title": "Type of console connection for the administration of the appliance"
},
"boot_priority": {
"enum": ["d", "c", "dc", "cd", "n", "nc", "nd", "cn", "dn"],
"title": "Optional define the disk boot priory. Refer to -boot option in qemu manual for more details."
"enum": ["c", "d", "n", "cn", "cd", "dn", "dc", "nc", "nd"],
"title": "Disk boot priority. Refer to -boot option in qemu manual for more details."
},
"kernel_command_line": {
"type": "string",

View File

@@ -23,6 +23,7 @@ import os
import sys
import uuid
import platform
import shutil
# Default projects directory location
DEFAULT_PROJECTS_PATH = os.path.normpath(os.path.expanduser("~/GNS3/projects"))
@@ -41,6 +42,7 @@ DEFAULT_APPLIANCES_PATH = os.path.normpath(os.path.expanduser("~/GNS3/appliances
DEFAULT_LOCAL_SERVER_HOST = "127.0.0.1"
DEFAULT_LOCAL_SERVER_PORT = 3080
DEFAULT_DELAY_CONSOLE_ALL = 500
# Pre-configured Telnet console commands on various OSes
if sys.platform.startswith("win"):
@@ -66,16 +68,27 @@ if sys.platform.startswith("win"):
'ZOC 6': r'"{}\ZOC6\zoc.exe" "/TELNET:%h:%p" /TABBED "/TITLE:%d"'.format(program_files_x86)}
# default on Windows
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Putty (included with GNS3)"]
if shutil.which("Solar-PuTTY.exe"):
# Solar-Putty is the default if it is installed.
PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Solar-Putty (included with GNS3)"] = 'Solar-PuTTY.exe --telnet --hostname %h --port %p --name "%d"'
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Solar-Putty (included with GNS3)"]
DEFAULT_DELAY_CONSOLE_ALL = 1500
else:
PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Solar-Putty (included with GNS3 downloaded from gns3.com)"] = 'Solar-PuTTY.exe --telnet --hostname %h --port %p --name "%d"'
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Putty (included with GNS3)"]
elif sys.platform.startswith("darwin"):
# Mac OS X
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {
'Terminal': r"""osascript -e 'tell application "Terminal"'"""
'Terminal': r"""osascript"""
r""" -e 'set posix_path to do shell script "echo \"$PATH\""'"""
r""" -e 'tell application "Terminal"'"""
r""" -e 'activate'"""
r""" -e 'do script "echo -n -e \"\\033]0;%d\\007\"; clear; PATH=\"" & (system attribute "PATH") & "\" telnet %h %p ; exit"'"""
r""" -e 'do script "echo -n -e \"\\033]0;%d\\007\"; clear; PATH=" & quoted form of posix_path & " telnet %h %p ; exit"'"""
r""" -e 'end tell'""",
'Terminal tabbed (experimental)': r"""osascript -e 'tell application "Terminal"'"""
'Terminal tabbed (experimental)': r"""osascript"""
r""" -e 'set posix_path to do shell script "echo \"$PATH\""'"""
r""" -e 'tell application "Terminal"'"""
r""" -e 'activate'"""
r""" -e 'tell application "System Events" to tell process "Terminal" to keystroke "t" using command down'"""
r""" -e 'if (the (count of the window) = 0) then'"""
@@ -87,9 +100,11 @@ elif sys.platform.startswith("darwin"):
r""" -e 'repeat while the busy of window 1 = true'"""
r""" -e 'delay 0.01'"""
r""" -e 'end repeat'"""
r""" -e 'do script "echo -n -e \"\\033]0;%d\\007\"; clear; PATH=\"" & (system attribute "PATH") & "\" telnet %h %p ; exit" in window 1'"""
r""" -e 'do script "echo -n -e \"\\033]0;%d\\007\"; clear; PATH=" & quoted form of posix_path & " telnet %h %p ; exit" in window 1'"""
r""" -e 'end tell'""",
'iTerm2 2.x': r"""osascript -e 'tell application "iTerm"'"""
'iTerm2 2.x': r"""osascript"""
r""" -e 'set posix_path to do shell script "echo \"$PATH\""'"""
r""" -e 'tell application "iTerm"'"""
r""" -e 'activate'"""
r""" -e 'if (count of terminals) = 0 then'"""
r""" -e ' set t to (make new terminal)'"""
@@ -99,12 +114,14 @@ elif sys.platform.startswith("darwin"):
r""" -e 'tell t'"""
r""" -e ' set s to (make new session at the end of sessions)'"""
r""" -e ' tell s'"""
r""" -e ' exec command "sh -c \"PATH=\\\"" & (system attribute "PATH") & "\\\" telnet %h %p"'"""
r""" -e ' exec command "sh"'"""
r""" -e ' write text "PATH=" & quoted form of posix_path & " exec telnet %h %p"'"""
r""" -e ' end tell'"""
r""" -e 'end tell'"""
r""" -e 'end tell'""",
'iTerm2 3.x': r"""osascript -e 'tell application "iTerm"'"""
'iTerm2 3.x': r"""osascript"""
r""" -e 'set posix_path to do shell script "echo \"$PATH\""'"""
r""" -e 'tell application "iTerm"'"""
r""" -e 'activate'"""
r""" -e 'if (count of windows) = 0 then'"""
r""" -e ' set t to (create window with default profile)'"""
@@ -116,7 +133,7 @@ elif sys.platform.startswith("darwin"):
r""" -e ' set s to current session'"""
r""" -e ' tell s'"""
r""" -e ' set name to "%d"'"""
r""" -e ' write text "PATH=\"" & (system attribute "PATH") & "\" exec telnet %h %p"'"""
r""" -e ' write text "PATH=" & quoted form of posix_path & " exec telnet %h %p"'"""
r""" -e ' end tell'"""
r""" -e 'end tell'"""
r""" -e 'end tell'""",
@@ -153,7 +170,7 @@ if sys.platform.startswith("win"):
# Windows
PRECONFIGURED_VNC_CONSOLE_COMMANDS = {
'TightVNC (included with GNS3)': 'tvnviewer.exe %h:%p',
'UltraVNC': 'C:\\Program Files\\uvnc bvba\\UltraVNC\\vncviewer.exe %h:%p'
'UltraVNC': r'"{}\uvnc bvba\UltraVNC\vncviewer.exe" %h:%p'.format(program_files)
}
# default Windows VNC console command
@@ -189,11 +206,11 @@ else:
if sys.platform.startswith("win"):
# Windows
PRECONFIGURED_SPICE_CONSOLE_COMMANDS = {
'Remote Viewer (included with GNS3)': '"c:\\Program Files\\VirtViewer v5.0-256\\bin\\remote-viewer.exe" spice://%h:%p',
'Remote Viewer': r'"{}\VirtViewer v7.0-256\bin\remote-viewer.exe" spice://%h:%p'.format(program_files),
}
# default Windows SPICE console command
DEFAULT_SPICE_CONSOLE_COMMAND = PRECONFIGURED_SPICE_CONSOLE_COMMANDS['Remote Viewer (included with GNS3)']
DEFAULT_SPICE_CONSOLE_COMMAND = PRECONFIGURED_SPICE_CONSOLE_COMMANDS['Remote Viewer']
elif sys.platform.startswith("darwin"):
# Mac OS X
@@ -217,8 +234,8 @@ WIRESHARK_NORMAL_CAPTURE = "Wireshark Traditional Capture"
WIRESHARK_LIVE_TRAFFIC_CAPTURE = "Wireshark Live Traffic Capture"
if sys.platform.startswith("win"):
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: "{}\Wireshark\wireshark.exe %c".format(os.environ["PROGRAMFILES"]),
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail.exe -f -c +0b %c | "{}\Wireshark\wireshark.exe" -o "gui.window_title:%d" -k -i -'.format(os.environ["PROGRAMFILES"])}
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: "{}\Wireshark\wireshark.exe %c".format(program_files),
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail.exe -f -c +0b %c | "{}\Wireshark\wireshark.exe" -o "gui.window_title:%d" -k -i -'.format(program_files)}
elif sys.platform.startswith("darwin"):
# Mac OS X
@@ -237,9 +254,9 @@ else:
DEFAULT_PACKET_CAPTURE_READER_COMMAND = PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS[WIRESHARK_LIVE_TRAFFIC_CAPTURE]
DEFAULT_PACKET_CAPTURE_ANALYZER_COMMAND = ""
if sys.platform.startswith("win") and "PROGRAMFILES(X86)" in os.environ:
if sys.platform.startswith("win"):
# Windows 64-bit
DEFAULT_PACKET_CAPTURE_ANALYZER_COMMAND = r'"{}\SolarWinds\ResponseTimeViewer\ResponseTimeViewer.exe" %c'.format(os.environ["PROGRAMFILES(X86)"])
DEFAULT_PACKET_CAPTURE_ANALYZER_COMMAND = r'"{}\SolarWinds\ResponseTimeViewer\ResponseTimeViewer.exe" %c'.format(program_files_x86)
STYLES = ["Charcoal", "Classic", "Legacy"]
@@ -259,7 +276,7 @@ GENERAL_SETTINGS = {
"telnet_console_command": DEFAULT_TELNET_CONSOLE_COMMAND,
"vnc_console_command": DEFAULT_VNC_CONSOLE_COMMAND,
"spice_console_command": DEFAULT_SPICE_CONSOLE_COMMAND,
"delay_console_all": 500,
"delay_console_all": DEFAULT_DELAY_CONSOLE_ALL,
"hide_getting_started_dialog": False,
"hide_setup_wizard": False,
"hide_new_appliance_template_button": False,
@@ -281,6 +298,7 @@ NODES_VIEW_SETTINGS = {
GRAPHICS_VIEW_SETTINGS = {
"scene_width": 2000,
"scene_height": 1000,
"grid_size": 75,
"draw_rectangle_selected_item": False,
"draw_link_status_points": True,
"default_label_font": "TypeWriter,10,-1,5,75,0,0,0,0,0",
@@ -289,7 +307,9 @@ GRAPHICS_VIEW_SETTINGS = {
"show_layers": False,
"snap_to_grid": False,
"show_grid": False,
"show_interface_labels": False
"show_interface_labels": False,
"show_interface_labels_on_new_project": False,
"limit_size_node_symbols": True
}
LOCAL_SERVER_SETTINGS = {

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