Compare commits

...

348 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
ziajka
ee2bea7cdd Release v2.1.1 2017-12-22 13:26:55 +01:00
grossmj
7cbc25cbbf Bump version to 2.1.1dev2 2017-12-20 11:20:07 +01:00
ziajka
7237cf1b88 Merge pull request #2373 from GNS3/fix-2363
Fix dragging appliance into topology from nodes window, fixes: #2363
2017-12-20 09:54:05 +01:00
ziajka
965923900b Fix dragging appliance into topology from nodes window, fixes: #2363 2017-12-20 09:53:19 +01:00
ziajka
a5a3a4e8cc Merge pull request #2372 from GNS3/fix-2362-2
Fix Appliances in Docked mode, fixes: #2362
2017-12-20 09:29:21 +01:00
ziajka
d898b30d84 Fix Appliances in Docked mode, fixes: #2362 2017-12-20 09:28:33 +01:00
ziajka
e86ced750e Merge pull request #2370 from GNS3/fix-2366
Create local variable in order to debug issue in the next occurrence,…
2017-12-19 14:48:44 +01:00
ziajka
e15b717cb0 Create local variable in order to debug issue in the next occurrence, #2366 2017-12-19 14:46:24 +01:00
ziajka
d8bd33f0e7 Merge pull request #2369 from GNS3/fix-2364
Fix ParseError: not well-formed (invalid token), #2364
2017-12-19 12:52:25 +01:00
ziajka
bc2fbe33ef Fix ParseError: not well-formed (invalid token), #2364 2017-12-19 12:51:20 +01:00
ziajka
b99b26f463 Merge pull request #2368 from GNS3/fix-2365
Fix local variable 'vm' referenced before assignment #2365
2017-12-19 11:41:30 +01:00
ziajka
5b7606793f Fix local variable 'vm' referenced before assignment #2365 2017-12-19 11:40:24 +01:00
ziajka
b8b5e8739e Merge pull request #2367 from GNS3/fix-2362
Fix: 'NodesDockWidget' object has no attribute 'uiNodesView', #2362
2017-12-19 11:38:11 +01:00
ziajka
0126c30887 Fix: 'NodesDockWidget' object has no attribute 'uiNodesView', #2362 2017-12-18 14:30:26 +01:00
grossmj
a89086ff60 Fix test. 2017-12-07 14:31:41 -06:00
grossmj
9ca35c56de Tentative fix for packet capture not working correctly when remote main server is configured. Ref #2111. 2017-12-07 13:59:02 -06:00
grossmj
3ddccf40a8 Log Qt messages with log.debug() instead of log.info(). 2017-12-05 14:24:24 -06:00
Jeremy Grossmann
1398ef323a Merge pull request #2345 from GNS3/no-timeout-snapshots
Snapshoting project without timeout but with Cancel button. Ref. #2314
2017-11-24 18:19:26 +07:00
grossmj
d52c4d839d Fix auto idle-pc from preferences. Fixes #2344. 2017-11-23 23:01:01 +07:00
ziajka
6989ee2c8b Snapshot creation, busy all the time as we cannot calculate progress. 2017-11-23 15:06:44 +01:00
ziajka
5c182e95ca Snapshoting project without timeout but with button. Ref. #2314 2017-11-23 14:13:56 +01:00
grossmj
bcb7a8e57b Improve validation for idle-pc. 2017-11-23 10:41:30 +07:00
grossmj
dba75e844e Activate faulthandler. 2017-11-21 13:44:29 +07:00
Jeremy Grossmann
6733739fa5 Merge pull request #2340 from ehlers/fix-osx-telnet
Fix OS X Telnet
2017-11-20 11:44:02 +07:00
Bernhard Ehlers
f5de62aa05 Add PATH to OS X console commands 2017-11-19 15:40:54 +01:00
Bernhard Ehlers
4087d35f6a Use raw triple quotes in large console settings
This eliminates one level of quoting
2017-11-19 15:31:26 +01:00
grossmj
f6a1af46a0 Fix issue in node summary when console is not supported by a node. 2017-11-19 20:30:58 +07:00
Bernhard Ehlers
6cef2fed5a Revert "Add preferred path to use Telnet console in DMG package. Ref #2274." 2017-11-19 12:37:21 +01:00
Bernhard Ehlers
e16c8db311 Revert "Force to use the telnet client embedded in DMG. Ref #2274." 2017-11-19 12:36:33 +01:00
Bernhard Ehlers
61c95a93ca Revert "Add debug when using Telnet path on OSX. Ref #2274." 2017-11-19 12:35:55 +01:00
Bernhard Ehlers
0df36dab30 Revert "Fix bug when replacing Telnet path on OSX. Ref #2274." 2017-11-19 12:33:54 +01:00
Bernhard Ehlers
9a5da633e0 Revert "Fix problem when embedded telnet client path contains a space on macOS. Ref #2328." 2017-11-19 12:32:29 +01:00
Bernhard Ehlers
02fed964f2 Revert "Support Telnet path containing spaces. Ref #2328." 2017-11-19 12:30:15 +01:00
grossmj
84ba56ae74 Remove unused symbols. Fixes #2320. 2017-11-19 14:50:12 +07:00
grossmj
e8c4758cb7 Show console information in Topology Summary Dock. Fixes #2258. 2017-11-19 13:56:54 +07:00
grossmj
d8dc31965f New option: require KVM. If false, Qemu VMs will not be prevented to run without KVM. 2017-11-19 12:39:37 +07:00
grossmj
9af48ba9a3 Implement variable replacement for Qemu VM options. 2017-11-18 17:36:11 +07:00
grossmj
67d8e317e0 Show on what server a node is installed in the servers summary pane. Fixes #2279. 2017-11-18 16:02:31 +07:00
grossmj
64392780c5 Add more info when cannot remove capture file after stopping packet capture in a remote project. Ref #1223. 2017-11-17 18:32:18 +07:00
grossmj
81bb159d45 Do not overwrites the disk images when copied to default directory. Fixes #2326. 2017-11-17 15:42:33 +07:00
ziajka
85352af9bd Update pyup config to use 2.2 branch 2017-11-15 13:21:14 +01:00
grossmj
65cfdf6b33 Revert "Only replace quoted telnet for macOS Telnet commands. Ref #2328." 2017-11-15 10:39:24 +07:00
grossmj
5d45dbebf6 Only replace quoted telnet for macOS Telnet commands. Ref #2328. 2017-11-14 18:42:17 +07:00
grossmj
0a7b6d81d8 Support Telnet path containing spaces. Ref #2328. 2017-11-14 18:34:06 +07:00
grossmj
588dcadd3a Fix problem when embedded telnet client path contains a space on macOS. Ref #2328. 2017-11-14 17:22:44 +07:00
grossmj
f24c93a55f Do not launch console for builtin nodes when using the "Console to all nodes" button. Fixes #2309. 2017-11-12 17:02:00 +08:00
grossmj
26e5c80406 Merge remote-tracking branch 'remotes/origin/master' into 2.1 2017-11-12 14:22:13 +08:00
ziajka
eb502232a2 Development on 2.1.1dev1 2017-11-09 10:50:12 +01:00
ziajka
25e17d718c Release 2.1.0 2017-11-09 07:29:33 +01:00
ziajka
89108070df Development on v2.1.0dev10 2017-11-07 10:19:46 +01:00
ziajka
f4df3ff9c0 Release v2.1.0rc4 2017-11-07 08:48:00 +01:00
Jeremy Grossmann
90f80b9804 Merge pull request #2322 from GNS3/upload-dialogs-progress
Accurate upload progress dialogs for large files
2017-11-06 20:50:59 +08:00
ziajka
3e86044132 Fix tests 2017-11-06 09:31:39 +01:00
ziajka
78d805cebc Disable showProgress during obtaining endpoint 2017-11-06 09:28:07 +01:00
ziajka
289f754108 Accurate upload progress dialogs for large files 2017-11-03 11:38:04 +01:00
Jeremy Grossmann
1a0c1f826b Merge pull request #2313 from GNS3/disable-direct-file-upload
Disable direct file upload on default
2017-10-30 17:18:36 +07:00
ziajka
9837d661a5 Disable direct file upload on default 2017-10-26 13:41:27 +02:00
grossmj
ff60776769 Merge remote-tracking branch 'origin/2.1' into 2.1 2017-10-26 15:47:02 +07:00
grossmj
7ac442631a Add registry version 5 2017-10-26 15:46:39 +07:00
Jeremy Grossmann
8da2ff3a97 Merge pull request #2310 from GNS3/image-upload-manager
Safe approach to send files to computes and dialogs fixes, Fixes: #2307, Ref: #2188
2017-10-26 13:01:14 +07:00
Jeremy Grossmann
a04d9784f2 Merge branch '2.1' into image-upload-manager 2017-10-26 12:24:17 +07:00
ziajka
623aa4a2de Code style and tests 2017-10-25 16:10:24 +02:00
ziajka
ef3c2afab9 Direct file upload enabled on default 2017-10-25 15:22:39 +02:00
ziajka
73e59d92ca Progress Dialog: don't count finished queries done in background 2017-10-25 13:50:35 +02:00
ziajka
8f3d5bf038 Add debug messages to file upload 2017-10-25 10:27:17 +02:00
ziajka
d638c6e0d7 Image Upload Manager for uploading 2017-10-24 17:20:30 +02:00
grossmj
c5688cacf9 Restore timer for refreshing the progress dialog status. 2017-10-24 20:12:01 +07:00
Jeremy Grossmann
b34bcd6369 Merge pull request #2305 from GNS3/race-nodes-view
Fix race condition on NodesDockWidget, fixes: #2304
2017-10-23 16:10:40 +07:00
ziajka
5e0fc3675f Fix race condition on NodesDockWidget, fixes: #2304 2017-10-23 11:08:33 +02:00
grossmj
e75a21e2ed Do not write an error message when importing non existing config from a directory. Fixes #2296. 2017-10-21 12:30:08 +07:00
grossmj
aeee44e597 Fix bug when replacing Telnet path on OSX. Ref #2274. 2017-10-19 16:32:16 +07:00
ziajka
3cebee64ad Back to development on 2.1.0rc3 2017-10-19 09:56:56 +02:00
ziajka
fd1619cfd3 Fix Travis deploy - urlib3 2017-10-19 09:51:05 +02:00
ziajka
4573d2aed8 Fix Travis deploy - urlib3 2017-10-19 09:47:22 +02:00
ziajka
7f29c497cc Fix Travis deploy 2017-10-19 09:21:57 +02:00
ziajka
6da42b5013 Development on 2.1.0dev9 2017-10-19 08:58:15 +02:00
ziajka
430366947f Release 2.1.0 rc3 2017-10-19 08:56:10 +02:00
grossmj
3870f8ecdc Add debug when using Telnet path on OSX. Ref #2274. 2017-10-18 18:47:24 +07:00
grossmj
f68626e4cc Force to use the telnet client embedded in DMG. Ref #2274. 2017-10-18 17:26:44 +07:00
Jeremy Grossmann
6b4126b688 Merge pull request #2297 from GNS3/direct-file-upload
Direct file upload, Fixes #2264
2017-10-18 00:36:28 +07:00
ziajka
2ef9890dc1 Upload directly to compute 2017-10-17 12:32:38 +02:00
ziajka
100e3dbf27 Load endpoing and execute post image 2017-10-16 09:44:16 +02:00
Jeremy Grossmann
b85db6e24f Merge pull request #2294 from ddragic/qxcb-log-filter
Filter additional QXcbConnection log messages
2017-10-14 15:38:52 +02:00
Dušan Dragić
05d2077b16 Filter additional QXcbConnection log messages 2017-10-14 13:21:55 +02:00
ziajka
357d039434 Preparation to load endpoint before usage 2017-10-13 12:05:31 +02:00
ziajka
0288384c85 Direct file upload settings 2017-10-13 11:22:32 +02:00
grossmj
84347848e9 Do not add missing file extension for screenshot file names on Mac. Fixes #2287. 2017-10-11 04:58:59 +08:00
grossmj
24e5ef885c Add preferred path to use Telnet console in DMG package. Ref #2274. 2017-10-10 23:57:34 +08:00
grossmj
0d8255ecaf Merge remote-tracking branch 'origin/2.1' into 2.1 2017-10-10 22:32:18 +08:00
grossmj
679548e4ad Log Qt messages as info instead of error. Ref #2281. 2017-10-10 22:13:22 +08:00
ziajka
e5384af45d Fix Travis bug with missing twine library. Fixes: #2283 2017-10-06 09:55:43 +02:00
ziajka
ae68d4d84b Development on 2.1.0dev8 2017-10-04 11:40:14 +02:00
ziajka
2ab81816ef Release 2.1.0 rc2 2017-10-04 11:36:51 +02:00
grossmj
4c4241183a Only show "can't get settings from controller" message in debug mode. 2017-10-04 16:24:29 +08:00
grossmj
46406d1e7b Remove explicit Telnet path on OS X. Ref #2274 2017-10-03 04:31:08 +08:00
Jeremy Grossmann
a92573394f Merge pull request #2280 from GNS3/fix-pyqt-version-check
Disable WebSocket notification for lower PyQT version than 5.6. Fixes…
2017-10-02 18:03:31 +02:00
ziajka
6baf628997 Disable WebSocket notification for lower PyQT version than 5.6. Fixes #2272 2017-10-02 10:44:28 +02:00
grossmj
8f9190e094 Increase timeout to 5 minutes when creating and restoring a snapshot. 2017-10-02 05:02:26 +08:00
grossmj
7e14e734b2 Bump version to 2.1.0dev7 2017-10-02 04:04:38 +08:00
grossmj
76fc2f07ce Add more information when a request timeouts. Ref #2277. 2017-10-02 00:46:23 +08:00
grossmj
eee066d5f3 Do not show the progress dialog when moving a node. Ref #2275. 2017-10-02 00:44:58 +08:00
grossmj
9dead47a37 Increase timer before showing a progress dialog from 250ms to 500ms. Ref #2275. 2017-10-01 23:56:28 +08:00
grossmj
a22bd8e9be Use embedded Telnet client on OS X. Ref #2274. 2017-10-01 23:33:12 +08:00
grossmj
a4b897d458 Fix small bug when adding an appliance template and the name already exists. 2017-09-19 16:32:19 +07:00
grossmj
e784f21c0f Use RAW sockets by default on Linux for VMware VM connections. 2017-09-19 12:47:30 +07:00
grossmj
3a000cdc60 Increase timeout to get compute servers from controller. Ref #2269. 2017-09-15 19:40:42 +07:00
grossmj
405f3b3382 Fix "Node doesn't exist" after deletion, but still on the canvas. Fixes #2266. 2017-09-15 17:23:15 +07:00
grossmj
7397f76566 Remove debug test. 2017-09-15 16:24:57 +07:00
grossmj
178cb35d6a Make sure the warning button icon appears in cloud properties dialog on Windows. Fixes #2245. 2017-09-15 16:21:05 +07:00
grossmj
012e5d331d Fix bug when cancelling the importation of a configuration file. Fixes #2260. 2017-09-15 15:52:36 +07:00
ziajka
bd81d36635 Development on v2.1.0dev6 2017-09-13 09:30:56 +02:00
ziajka
234eab57c8 Release 2.1.0rc1 2017-09-13 09:28:55 +02:00
grossmj
e4b19714f4 Fix missing spice console option in appliance template schema. Fixes #2255. 2017-09-13 13:55:30 +07:00
ziajka
7bfba1015b Back to development at 2.1.0dev5 2017-09-05 11:24:08 +02:00
ziajka
498ba2d2b1 Re-release 2.1.0b2 2017-09-05 11:16:02 +02:00
ziajka
f3756b8401 Fix unicode error during appliance tests 2017-09-05 09:56:56 +02:00
ziajka
68f6d37aab Fix link tests 2017-09-05 09:43:33 +02:00
ziajka
6ce35fa5b5 Development on 2.1.0dev5 2017-09-05 08:41:08 +02:00
ziajka
e376753859 Release 2.1.0 beta 2 2017-09-05 08:37:42 +02:00
Jeremy Grossmann
7b03c3eae7 Merge pull request #2250 from GNS3/dont-move-under-layer-0
Disabled possibility of moving items under zero layer (Fixes #2220)
2017-09-01 16:20:04 +07:00
ziajka
902ba42be1 Merge pull request #2252 from GNS3/fix-resources
Fix resources dependencies for cloud configuration page (Fixes: #2251)
2017-09-01 11:06:58 +02:00
ziajka
73fe898eda Fix resources dependencies for cloud configuration page (Fixes: #2251) 2017-09-01 11:05:53 +02:00
ziajka
1ff488d39a Disabled possibility of moving items under zero layer (Fixes #2220) 2017-09-01 10:13:19 +02:00
Jeremy Grossmann
1622a79383 Merge pull request #2248 from GNS3/dialog-warning-fallback
dialog-warning.svg fallback for themed icon (Ref. #2245)
2017-09-01 12:51:38 +07:00
Jeremy Grossmann
1564c63a42 Merge pull request #2247 from GNS3/wide-packet-filters-dialog
Change width of packet filters dialog (Fixes #2244)
2017-09-01 12:50:24 +07:00
ziajka
29f651aaea dialog-warning.svg fallback for themed icon (Ref. #2245) 2017-08-31 11:37:32 +02:00
ziajka
9ee0222339 Change width of packet filters dialog (Fixes #2244) 2017-08-31 09:46:08 +02:00
grossmj
6e1384c985 Fix high CPU usage when using packet filters. Fixes #2240. 2017-08-28 11:40:50 +07:00
Jeremy Grossmann
20190c5816 Merge pull request #2232 from GNS3/toggle-node-menu-item
Toggle Node menu item (Fixes #2227)
2017-08-25 16:32:49 +08:00
ziajka
cab3412ddc Toggle Node menu item (Fixes #2227) 2017-08-22 13:01:50 +02:00
Jeremy Grossmann
6d74ce4070 Merge pull request #2215 from GNS3/fix-qemu-edit-symbol
Fixes loading symbols for QEMU at Edit Page (#2214)
2017-08-10 21:41:58 +08:00
Jeremy Grossmann
159d21af9a Merge pull request #2217 from GNS3/fixes-lineitem
Fixes multiselection styles change crash on LineItem (#2216)
2017-08-10 21:39:42 +08:00
ziajka
713feff11f Fixes multiselection styles change crash on LineItem (#2216) 2017-08-10 09:38:22 +02:00
ziajka
64c5ca712e Fixes loading symbols for QEMU at Edit Page (#2214) 2017-08-10 09:09:07 +02:00
Jeremy Grossmann
1572a6f67f Merge pull request #2212 from GNS3/2211
Fixes exception when right click on Dynamips router in the device dock
2017-08-08 22:16:53 +08:00
ziajka
fcee5c6916 Fixes exception when right click on Dynamips router in the device dock (#2211) 2017-08-08 13:59:47 +02:00
Jeremy Grossmann
3d21f9a997 Merge pull request #2210 from CapnCheapo/patch-1
Update frame_relay_switch_configuration_page_ui.py
2017-08-08 11:14:19 +08:00
Jeremy Grossmann
d93ad5e9d5 Merge pull request #2209 from CapnCheapo/2.1
Update frame_relay_switch_configuration_page.ui
2017-08-08 11:14:08 +08:00
Stephen C. Moore
13739281da Update frame_relay_switch_configuration_page_ui.py
Fixes #2205
2017-08-07 14:04:44 -05:00
Stephen C. Moore
1f281a807b Update frame_relay_switch_configuration_page.ui
Fixes #2205
2017-08-07 14:03:00 -05:00
ziajka
2ca250d2c2 Development on 2.1.0dev4 2017-08-04 11:36:47 +02:00
ziajka
b82b031168 Release 2.1.0 beta 1 2017-08-04 11:35:21 +02:00
Julien Duponchelle
c48048f013 Info added to the Nat node
Ref #2197
2017-08-02 13:19:24 +02:00
Julien Duponchelle
9aaca9955a Add missing popup information in cloud and docker node
Fix #2197
2017-08-02 12:14:30 +02:00
Julien Duponchelle
a0e6a82ea2 Handle invalid json in websockets
Fix #2192
2017-08-01 16:32:52 +02:00
Julien Duponchelle
9a3e320e95 Avoid invalid bad request error when receiving partial answer
Fix #2194
2017-08-01 16:29:31 +02:00
Julien Duponchelle
c3fce51493 Catch parse error for broken SVG
Fix #2193
2017-08-01 16:14:08 +02:00
Julien Duponchelle
116cf55758 Filter QXcbConnection log messages
It's Qt noise on Linux we can't do nothing to avoid it.

Fix #2191
2017-08-01 16:14:08 +02:00
Julien Duponchelle
269c6bd0cd Catch class 'PyQt5.QtNetwork.QNetworkReply'> returned a result with an error set
Fix #2195
2017-08-01 16:14:08 +02:00
Julien Duponchelle
31aa612a62 Fix KeyError: 'overlay_notifications'
Fix #2196
2017-08-01 16:14:07 +02:00
ziajka
55f396694f Development on 2.1.0dev3 2017-07-31 11:35:40 +02:00
Julien Duponchelle
20efde749c Turn off timeout for node creation
The timeout is an issue if you use a local GNS3 server and
a remote over a slow link.

Fix https://github.com/GNS3/gns3-server/issues/1126
2017-07-19 15:26:51 +02:00
188 changed files with 107891 additions and 158584 deletions

View File

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

View File

@@ -6,8 +6,13 @@ notifications:
script:
- docker build -t gns3-gui-test .
- docker run gns3-gui-test
before_deploy:
- sudo pip install twine
- sudo pip install urllib3[secure]
deploy:
provider: pypi
edge:
branch: v1.8.45
user: noplay
password:
secure: FofcqlJjgqf2jaDaXpLHeigVoexbrOz3WwnDuiJpwJxeFUlPY8s2cQs/Bm+dzxzZaOaGiVE0A83v/Xa10yD5tflThHt4sqYJK3iQCinA7wgeAlDimB4xrWUNplfNJZ/Eod5Ssa++E02W+3i29PxpXY//mjCY7qDxaoxul1gnFJY=

276
CHANGELOG
View File

@@ -1,5 +1,281 @@
# 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
* Fix Appliances in Docked mode, fixes: #2362
* Create local variable in order to debug issue in the next occurrence, #2366
* Fix ParseError: not well-formed (invalid token), #2364
* Fix local variable 'vm' referenced before assignment #2365
* Fix: 'NodesDockWidget' object has no attribute 'uiNodesView', #2362
* Tentative fix for packet capture not working correctly when remote main server is configured. Ref #2111.
* Log Qt messages with log.debug() instead of log.info().
* Fix auto idle-pc from preferences. Fixes #2344.
* Snapshoting project without timeout but with button. Ref. #2314
* Improve validation for idle-pc.
* Activate faulthandler.
* Add PATH to OS X console commands
* Use raw triple quotes in large console settings This eliminates one level of quoting
* Fix issue in node summary when console is not supported by a node.
* Remove unused symbols. Fixes #2320.
* Show console information in Topology Summary Dock. Fixes #2258.
* New option: require KVM. If false, Qemu VMs will not be prevented to run without KVM.
* Implement variable replacement for Qemu VM options.
* Show on what server a node is installed in the servers summary pane. Fixes #2279.
* Add more info when cannot remove capture file after stopping packet capture in a remote project. Ref #1223.
* Do not overwrites the disk images when copied to default directory. Fixes #2326.
* Only replace quoted telnet for macOS Telnet commands. Ref #2328.
* Support Telnet path containing spaces. Ref #2328.
* Fix problem when embedded telnet client path contains a space on macOS. Ref #2328.
* Do not launch console for builtin nodes when using the "Console to all nodes" button. Fixes #2309.
* Update frame_relay_switch_configuration_page_ui.py
* Turn off timeout for node creation
## 2.1.0 09/11/2017
* Update dynamips binary on OSX
## 2.1.0rc4 07/11/2017
* Accurate upload progress dialogs for large files
* Disable direct file upload on default
* Add registry version 5
* Direct file upload enabled on default
* Progress Dialog: don't count finished queries done in background
* Add debug messages to file upload
* Image Upload Manager for uploading
* Fix race condition on NodesDockWidget, fixes: #2304
* Do not write an error message when importing non existing config from a directory. Fixes #2296.
* Fix bug when replacing Telnet path on OSX. Ref #2274.
* Back to development on 2.1.0rc3
## 2.1.0rc3 19/10/2017
* Add debug when using Telnet path on OSX. Ref #2274.
* Force to use the telnet client embedded in DMG. Ref #2274.
* Upload directly to compute - experimental feature
* Filter additional QXcbConnection log messages
* Do not add missing file extension for screenshot file names on Mac. Fixes #2287.
* Log Qt messages as info instead of error. Ref #2281.
## 2.1.0rc2 04/10/2017
* Only show "can't get settings from controller" message in debug mode.
* Remove explicit Telnet path on OS X. Ref #2274
* Disable WebSocket notification for lower PyQT version than 5.6. Fixes #2272
* Increase timeout to 5 minutes when creating and restoring a snapshot.
* Add more information when a request timeouts. Ref #2277.
* Do not show the progress dialog when moving a node. Ref #2275.
* Increase timer before showing a progress dialog from 250ms to 500ms. Ref #2275.
* Use embedded Telnet client on OS X. Ref #2274.
* Fix small bug when adding an appliance template and the name already exists.
* Use RAW sockets by default on Linux for VMware VM connections.
* Increase timeout to get compute servers from controller. Ref #2269.
* Fix "Node doesn't exist" after deletion, but still on the canvas. Fixes #2266.
* Make sure the warning button icon appears in cloud properties dialog on Windows. Fixes #2245.
* Fix bug when cancelling the importation of a configuration file. Fixes #2260.
## 2.1.0rc1 13/09/2017
* Fix missing spice console option in appliance template schema. Fixes #2255.
## 2.1.0b2 05/09/2017
* Fix resources dependencies for cloud configuration page (Fixes: #2251)
* Disabled possibility of moving items under zero layer (Fixes #2220)
* dialog-warning.svg fallback for themed icon (Ref. #2245)
* Change width of packet filters dialog (Fixes #2244)
* Fix high CPU usage when using packet filters. Fixes #2240.
* Toggle Node menu item (Fixes #2227)
* Fixes multiselection styles change crash on LineItem (#2216)
* Fixes loading symbols for QEMU at Edit Page (#2214)
* Fixes exception when right click on Dynamips router in the device dock (#2211)
* Update frame_relay_switch_configuration_page.ui
## 2.1.0b1 04/08/2017
* Info added to the Nat node
* Add missing popup information in cloud and docker node
* Handle invalid json in websockets
* Avoid invalid bad request error when receiving partial answer
* Catch parse error for broken SVG
* Filter QXcbConnection log messages
* Catch class 'PyQt5.QtNetwork.QNetworkReply'> returned a result with an error set
* Fix KeyError: 'overlay_notifications'
## 2.1.0a2 31/07/2017
* Fix permission error when importing a project on a remote server

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()
@@ -79,22 +79,34 @@ class ApplianceManager(QtCore.QObject):
for appliance in self._appliances:
if appliance["appliance_id"] == appliance_id:
break
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"])
if server is None:
return False
self._controller.post("/projects/" + project.id() + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
self._controller.post("/projects/" + project_id + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
"compute_id": server.id(),
"x": int(x),
"y": int(y)
},
showProgress=False,
timeout=None)
else:
self._controller.post("/projects/" + project.id() + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
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"]
@@ -390,7 +392,7 @@ class BaseNode(QtCore.QObject):
file=file), self._importConfigCallback,
pathlib.Path(os.path.join(directory, filename)))
else:
self.error_signal.emit(self.id(), "no script file could be found, expected file name: {}".format(filename))
log.warning("{}: config file '{}' not found".format(self.name(), filename))
def _importConfigCallback(self, result, error=False, **kwargs):
if error:

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,15 +55,15 @@ 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=15)
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
def _controllerConnectedSlot(self):
if self._controller.connected():
self._refreshingComputes = True
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=15)
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
def _controllerDisconnectedSlot(self):
for compute_id in list(self._computes):
@@ -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

@@ -19,10 +19,10 @@
Compute summary view that list all the compute, their status.
"""
import sip
from .qt import QtGui, QtCore, QtWidgets
from .compute_manager import ComputeManager
from .topology import Topology
from .node import Node
import logging
log = logging.getLogger(__name__)
@@ -62,22 +62,44 @@ 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)
# add nodes belonging to this compute
self.takeChildren()
nodes = Topology.instance().nodes()
for node in nodes:
if node.compute().id() == self._compute.id():
item = QtWidgets.QTreeWidgetItem()
item.setText(0, node.name())
if node.status() == Node.started:
item.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
elif node.status() == Node.suspended:
item.setIcon(0, QtGui.QIcon(':/icons/led_yellow.svg'))
else:
item.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
self.addChild(item)
self.sortChildren(0, QtCore.Qt.AscendingOrder)
class ComputeSummaryView(QtWidgets.QTreeWidget):

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
@@ -92,6 +96,12 @@ class Controller(QtCore.QObject):
self._http_client.connection_disconnected_signal.connect(self._httpClientDisconnectedSlot)
self._connectingToServer()
def getHttpClient(self):
"""
:return: Instance of HTTP client to communicate with the server
"""
return self._http_client
def setDisplayError(self, val):
"""
Allow error to be visible or not
@@ -115,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:
@@ -136,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:
@@ -181,6 +192,14 @@ class Controller(QtCore.QObject):
return compute_id
return compute_id
def getEndpoint(self, path, compute_id, *args, **kwargs):
"""
API post on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/endpoint/{}{}".format(compute_id, path)
return self.get(path, *args, **kwargs)
def put(self, *args, **kwargs):
return self.createHTTPQuery("PUT", *args, **kwargs)
@@ -194,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)
@@ -223,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:
@@ -249,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:
@@ -264,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
@@ -284,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://524bf289665d45ac91f2f621e8d9982f:35233ad66db0472b903e19305987c34f@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
@@ -32,6 +32,10 @@ from ..utils.progress_dialog import ProgressDialog
from ..compute_manager import ComputeManager
from ..controller import Controller
from ..local_config import LocalConfig
from ..image_upload_manager import ImageUploadManager
import logging
log = logging.getLogger(__name__)
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
@@ -40,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)
@@ -75,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 = None
try:
destination = Config().appliances_dir
os.makedirs(destination, exist_ok=True)
destination = os.path.join(destination, os.path.basename(path))
shutil.copy(path, destination)
except OSError as e:
QtWidgets.QMessageBox.warning(self.parent(), "Can't copy {} to {}".format(path, destination), str(e))
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.
@@ -109,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"])
@@ -315,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
@@ -414,12 +438,17 @@ 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(self._compute_id, callback=self._imageUploadedCallback)
image_upload_manger = ImageUploadManager(image, Controller.instance(), self._compute_id, self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger.upload()
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
"""
@@ -454,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)
@@ -470,12 +502,17 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
while len(appliance_configuration["name"]) == 0 or not config.is_name_available(appliance_configuration["name"]):
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "The name \"{}\" is already used by another appliance".format(appliance_configuration["name"]))
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add appliance", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
if not ok:
return False
appliance_configuration["name"] = appliance_configuration["name"].strip()
if "qemu" in appliance_configuration:
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_():
@@ -497,7 +534,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
for image in appliance_configuration["images"]:
if image["location"] == "local":
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
image.upload(self._compute_id, self._applianceImageUploadedCallback)
image_upload_manger = ImageUploadManager(
image, Controller.instance(), self._compute_id,
self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger.upload()
self._image_uploading_count += 1
def _applianceImageUploadedCallback(self, result, error=False, **kwargs):
@@ -529,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)
@@ -540,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

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

View File

@@ -19,7 +19,6 @@
Display error to the user in an overlay popup
"""
import os
import time
from gns3.qt import QtWidgets, QtCore, qslot
@@ -79,7 +78,7 @@ class NotifDialog(QtWidgets.QWidget):
@qslot
def addNotif(self, level, message):
if not self.parent().settings()["overlay_notifications"]:
if not self.parent().settings().get("overlay_notifications", True):
return
# This unicode char prevent the wordwrap at /

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

@@ -85,12 +85,18 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
if ok and snapshot_name and self._project:
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()), self._createSnapshotsCallback, {"name": snapshot_name})
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()),
self._createSnapshotsCallback,
{"name": snapshot_name},
progressText="Creation of snapshot '{}' in progress...".format(snapshot_name),
timeout=None)
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()
@@ -105,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"])
@@ -127,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)
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

@@ -21,6 +21,7 @@ Style editor to edit Shape items.
from ..qt import QtCore, QtWidgets, QtGui
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
from ..items.shape_item import ShapeItem
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
@@ -115,7 +116,9 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
for item in self._items:
item.setPen(pen)
if brush:
# on multiselection it's possible to select many type of items
# but brush can be applied only on ShapeItem,
if brush and isinstance(item, ShapeItem):
item.setBrush(brush)
item.setRotation(self.uiRotationSpinBox.value())

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
@@ -31,6 +31,7 @@ from .link import Link
from .node import Node
from .modules import MODULES
from .modules.module_error import ModuleError
from .modules.builtin import Builtin
from .settings import GRAPHICS_VIEW_SETTINGS
from .topology import Topology
from .appliance_manager import ApplianceManager
@@ -59,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__)
@@ -87,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)
@@ -126,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:
@@ -135,6 +156,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
self.scene().addItem(item)
super().setEnabled(enabled)
self.toggleUiDeviceMenu()
def reset(self):
"""
Remove all the items from the scene and
@@ -272,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.
@@ -392,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):
@@ -417,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():
@@ -473,6 +504,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
else:
super().mousePressEvent(event)
self.toggleUiDeviceMenu()
def mouseReleaseEvent(self, event):
"""
Handles all mouse release events.
@@ -495,6 +528,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
item.setSelected(True)
super().mouseReleaseEvent(event)
self.toggleUiDeviceMenu()
def wheelEvent(self, event):
"""
Handles zoom in or out using the mouse wheel.
@@ -1038,6 +1073,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._main_window.run_later(counter, callback)
counter += delay
def consoleFromAllItems(self):
"""
Console from all scene items, except builtin devices.
"""
items = [item for item in self.scene().items()
if not (isinstance(item, NodeItem) and isinstance(item.node().module(), Builtin))]
self.consoleFromItems(items)
def consoleActionSlot(self):
"""
Slot to receive events from the console action in the
@@ -1140,6 +1184,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._import_config_dir,
"All files (*.*);;Config files (*.cfg)",
"Config files (*.cfg)")
if not path:
continue
self._import_config_dir = os.path.dirname(path)
item.node().importFile(config_file, path)
@@ -1187,12 +1233,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
for item in items:
for config_file in item.node().configFiles():
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_configs_to_dir, item.node().name() + "_" + os.path.basename(config_file)), "All files (*.*);;Config files (*.cfg)")
if not path:
continue
self._export_configs_to_dir = os.path.dirname(path)
item.node().exportFile(config_file, path)
def getCommandLineSlot(self):
@@ -1454,6 +1497,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
elif item.parentItem() is None:
item.delete()
self.scene().clearSelection()
self.toggleUiDeviceMenu()
def allocateCompute(self, node_data, module_instance):
"""
Allocates a server.
@@ -1501,9 +1547,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
node = Topology.instance().getNode(node_id)
name = "Node"
if node:
if node.name():
name = node.name()
if node and node.name():
name = node.name()
if self._main_window and not sip.isdeleted(self._main_window):
QtWidgets.QMessageBox.critical(self._main_window, name, message.strip())
@@ -1529,20 +1574,28 @@ 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):
""" Hook which enables/disables uiDeviceMenu based on the current items selection"""
items = self.scene().selectedItems()
if len(items) > 0:
self._main_window.uiDeviceMenu.setEnabled(True)
else:
self._main_window.uiDeviceMenu.setEnabled(False)

View File

@@ -15,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
@@ -25,6 +25,8 @@ import base64
import datetime
import ipaddress
import urllib.request
import urllib.parse
from .version import __version__, __version_info__
from .qt import QtCore, QtNetwork, qpartial, sip_is_deleted, QtWebSockets
@@ -45,21 +47,17 @@ 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
"""
# How many times we need to retry a connection
MAX_RETRY_CONNECTION = 5
# Callback class used for displaying progress
_progress_callback = None
connection_connected_signal = QtCore.Signal()
connection_disconnected_signal = QtCore.Signal()
def __init__(self, settings, network_manager=None):
def __init__(self, settings, network_manager=None, max_retry_connection=5):
super().__init__()
self._protocol = settings.get("protocol", "http")
@@ -74,8 +72,9 @@ class HTTPClient(QtCore.QObject):
self._port = int(settings["port"])
self._user = settings.get("user", None)
self._password = settings.get("password", None)
# How many time we have retry connection
# How many time we have already retried connection
self._retry = 0
self._max_retry_connection = max_retry_connection
self._connected = False
self._shutdown = False # Shutdown in progress
self._accept_insecure_certificate = settings.get("accept_insecure_certificate", None)
@@ -84,7 +83,6 @@ class HTTPClient(QtCore.QObject):
# query and disconnect if time is too long between two query
self._last_query_timestamp = None
self._max_time_difference_between_queries = None
if network_manager:
self._network_manager = network_manager
else:
@@ -169,6 +167,25 @@ class HTTPClient(QtCore.QObject):
self.createHTTPQuery("POST", "/shutdown", None, showProgress=False)
self._shutdown = True
def getNetworkManager(self):
"""
:return: instance of NetworkManager
"""
return self._network_manager
def setMaxRetryConnection(self, retries):
"""
Sets how many times we need to retry a connection
:param retries: integer
"""
self._max_retry_connection = retries
def getMaxRetryConnection(self):
"""
Returns how many times we need to retry a connection
"""
return self._max_retry_connection
def _notify_progress_start_query(self, query_id, progress_text, response):
"""
Called when a query start
@@ -192,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, sent, 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, sent, abs(total))
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
@classmethod
def setProgressCallback(cls, progress_callback):
@@ -255,6 +272,7 @@ class HTTPClient(QtCore.QObject):
prefix="/v2",
params={},
networkManager=None,
eventsHandler=None,
**kwargs):
"""
Call the remote server, if not connected, check connection before
@@ -272,6 +290,8 @@ class HTTPClient(QtCore.QObject):
:param timeout: Delay in seconds before raising a timeout
:param prefix: Prefix to the path
:param networkManager: QNetworkAccessManager None use the default
:param eventsHandler: Handler receiving and triggering events like `updated`, `cancelled`.
If not specified and showProgress is `True` then `ProgressDialog` receives them.
:param params: Query arguments parameters
:returns: QNetworkReply
"""
@@ -283,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,
@@ -303,16 +324,17 @@ class HTTPClient(QtCore.QObject):
server=server,
timeout=timeout,
prefix=prefix,
eventsHandler=eventsHandler,
params=params)
if self._connected:
return request()
else:
self._query_waiting_connections.append((request, callback))
# If we are not connected and we enqueue the first query we open the conection
# enqueue the first query and open the connection if we are not connected
if len(self._query_waiting_connections) == 1:
log.debug("Connection to {}".format(self.url()))
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5, showProgress=False)
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=10, showProgress=False)
def _connectionError(self, callback, msg="", server=None):
"""
@@ -329,7 +351,7 @@ class HTTPClient(QtCore.QObject):
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall. And that server version is {}.".format(self.url(), __version__)
for request, callback in self._query_waiting_connections:
if callback is not None:
callback({"message": msg}, error=True, server=server)
callback({"message": msg}, error=True, server=server, connection_error=True)
self._query_waiting_connections = []
def _retryConnection(self, server=None):
@@ -349,7 +371,7 @@ class HTTPClient(QtCore.QObject):
"""
if error is not False:
if self._retry < self.MAX_RETRY_CONNECTION:
if self._retry < self.getMaxRetryConnection():
self._retryConnection(server=server)
return
for request, callback in self._query_waiting_connections:
@@ -358,7 +380,7 @@ class HTTPClient(QtCore.QObject):
return
if "version" not in params or "local" not in params:
if self._retry < self.MAX_RETRY_CONNECTION:
if self._retry < self.getMaxRetryConnection():
self._retryConnection(server=server)
return
msg = "The remote server {} is not a GNS3 server".format(self.url())
@@ -370,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
@@ -469,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 == {}:
@@ -483,7 +506,7 @@ class HTTPClient(QtCore.QObject):
query_string += urllib.parse.urlencode(params)
return query_string
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, networkManager=None, **kwargs):
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, networkManager=None, eventsHandler=None, **kwargs):
"""
Call the remote server
@@ -499,6 +522,8 @@ class HTTPClient(QtCore.QObject):
:param ignoreErrors: Ignore connection error (usefull to not closing a connection when notification feed is broken)
:param server: The server where the query is executed
:param timeout: Delay in seconds before raising a timeout
:param eventsHandler: Handler receiving and triggering events like `updated`, `cancelled`.
If not specified and showProgress is `True` then `ProgressDialog` receives them.
:param params: Query arguments parameters
:returns: QNetworkReply
"""
@@ -523,7 +548,11 @@ class HTTPClient(QtCore.QObject):
if not networkManager:
networkManager = self._network_manager
response = networkManager.sendCustomRequest(request, method.encode(), body)
try:
response = networkManager.sendCustomRequest(request, method.encode(), body)
except SystemError as e:
log.error("Can't send query: {}".format(str(e)))
return
context = copy.copy(context)
context["query_id"] = str(uuid.uuid4())
@@ -534,8 +563,11 @@ class HTTPClient(QtCore.QObject):
if downloadProgressCallback is not None:
response.readyRead.connect(qpartial(self._readyReadySlot, response, downloadProgressCallback, context, server))
if not sip_is_deleted(HTTPClient._progress_callback) and HTTPClient._progress_callback.progress_dialog():
request_canceled = qpartial(self._requestCanceled, response, context)
request_canceled = qpartial(self._requestCanceled, response, context)
if eventsHandler is not None:
eventsHandler.canceled.connect(request_canceled)
elif not sip_is_deleted(HTTPClient._progress_callback) and HTTPClient._progress_callback.progress_dialog():
HTTPClient._progress_callback.progress_dialog().canceled.connect(request_canceled)
if showProgress:
@@ -546,7 +578,7 @@ class HTTPClient(QtCore.QObject):
self._notify_progress_start_query(context["query_id"], progressText, response)
if timeout is not None:
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response))
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response, timeout))
return response
@@ -581,14 +613,14 @@ class HTTPClient(QtCore.QObject):
else:
callback(content, server=server, context=context)
def _timeoutSlot(self, response):
def _timeoutSlot(self, response, timeout):
"""
Beware it's call for all request you need to check the status of the response
"""
# We check if we received HTTP headers
if not sip.isdeleted(response) and response.isRunning() and not len(response.rawHeaderList()) > 0:
if not response.error() != QtNetwork.QNetworkReply.NoError:
log.warn("Timeout request {}".format(response.url().toString()))
log.warning("Timeout after {} seconds for request {}. Please check the connection is not blocked by a firewall or an anti-virus.".format(timeout, response.url().toString()))
response.abort()
def disconnect(self):
@@ -601,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)
@@ -618,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:
@@ -669,12 +704,12 @@ class HTTPClient(QtCore.QObject):
except UnicodeDecodeError:
body = None
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
# log.debug(body)
if body and len(body.strip(" \n\t")) > 0 and content_type == "application/json":
try:
params = json.loads(body)
except ValueError: # Partial JSON
raise HttpBadRequest(body)
params = {}
status = 504
else:
params = {}
if callback is not None:
@@ -692,44 +727,72 @@ 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
def fromUrl(cls, url, network_manager=None, base_settings=None):
"""
Returns HttpClient instance based on the url
:param url: Url to parse
:param network_manager: Optional network_manager
:param base_settings: Source of the settings, if necessary
:return: HttpClient
"""
settings = {}
if base_settings is not None:
settings.update(**base_settings)
parse_results = urllib.parse.urlparse(url)
settings['protocol'] = parse_results.scheme
settings['host'] = parse_results.hostname
settings['port'] = parse_results.port
settings['user'] = parse_results.username
settings['password'] = parse_results.password
return cls(settings, network_manager)

View File

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

View File

@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2017 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pathlib
import urllib.parse
from gns3.http_client import HTTPClient
import logging
log = logging.getLogger(__name__)
class ImageUploadManager(object):
"""
Manager over the image upload. Encapsulates file uploads to computes or via controller.
"""
def __init__(self, image, controller, compute_id, callback=None, directFileUpload=False):
self._image = image
self._compute_id = compute_id
self._callback = callback
self._directFileUpload = directFileUpload
self._controller = controller
def upload(self):
if self._directFileUpload:
# first obtain endpoint and know when target request
self._controller.getEndpoint(
self._getComputePath(), self._compute_id, self._onLoadEndpointCallback, showProgress=False)
else:
self._fileUploadToController()
def _getComputePath(self):
return '/{emulator}/images/{filename}'.format(
emulator=self._image.emulator, filename=self._image.filename)
def _onLoadEndpointCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
log.error("Error while getting endpoint: {}".format(result["message"]))
return
# we know where is the endpoint and we trying to post there a file
endpoint = result['endpoint']
self._fileUploadToCompute(endpoint)
def _checkIfSuccessfulCallback(self, result, error=False, **kwargs):
if error:
connection_error = kwargs.get('connection_error', False)
if connection_error:
log.debug("During direct file upload compute is not visible. Fallback to upload via controller.")
# there was an issue with connection, probably we don't have a direct access to compute
# we need to fallback to uploading files via controller
self._fileUploadToController()
else:
if "message" in result:
log.error("Error while direct file upload: {}".format(result["message"]))
return
self._callback(result, error, **kwargs)
def _fileUploadToCompute(self, endpoint):
log.info("Uploading file to compute: {}".format(endpoint))
parse_results = urllib.parse.urlparse(endpoint)
network_manager = self._controller.getHttpClient().getNetworkManager()
client = HTTPClient.fromUrl(endpoint, network_manager=network_manager)
# We don't retry connection as in case of fail we try direct file upload
client.setMaxRetryConnection(0)
client.createHTTPQuery(
'POST', parse_results.path, self._checkIfSuccessfulCallback, body=pathlib.Path(self._image.path),
progressText="Uploading {}".format(self._image.filename), timeout=None, prefix="")
def _fileUploadToController(self):
log.info("Uploading file to controller: {}".format(self._getComputePath()))
self._controller.postCompute(
self._getComputePath(), self._compute_id, self._callback, body=pathlib.Path(self._image.path),
progressText="Uploading {}".format(self._image.filename), timeout=None)

View File

@@ -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,14 +82,14 @@ 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:
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__())
if self._id and not self.deleting() and self._project:
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__(), showProgress=False)
@qslot
def updateDrawingCallback(self, result, error=False, **kwargs):
@@ -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"])
@@ -165,6 +166,7 @@ class DrawingItem:
"""
QtWidgets.QGraphicsItem.setZValue(self, value)
if self.zValue() < 0:
self.setFlag(self.ItemIsSelectable, False)
self.setFlag(self.ItemIsMovable, False)
@@ -172,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.
@@ -179,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)
@@ -187,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

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

View File

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

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

View File

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

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):
"""
@@ -483,7 +484,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._suspend_item is None:
self._suspend_item = SvgIconItem(':/icons/pause.svg', self)
self._suspend_item.setScale(0.6)
self._suspend_item.setScale(0.6)
if not self._suspend_item.isVisible():
self._suspend_item.show()
self._suspend_item.setPos(link_center)
@@ -537,7 +538,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._filter_item is None:
self._filter_item = SvgIconItem(':/icons/filter.svg', self)
self._filter_item.setScale(0.6)
self._filter_item.setScale(0.6)
if not self._filter_item.isVisible():
self._filter_item.show()
self._filter_item.setPos(link_center)

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

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

View File

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

@@ -111,6 +111,13 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def setZValue(self, value):
"""
Sets Z value of the item
:param value: z layer
"""
return DrawingItem.setZValue(self, value)
def toSvg(self):
"""
Return an SVG version of the text
@@ -138,7 +145,19 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
return svg
def fromSvg(self, svg):
svg = ET.fromstring(svg)
# sometimes we receive \0 at the end of string inside <svg> element
try:
svg = svg.replace("\u0000", "")
except AttributeError:
pass
try:
svg = ET.fromstring(svg)
except ET.ParseError:
self.setPlainText("Unable to parse `text_item`")
return
text = svg[0]
font = QtGui.QFont()

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
@@ -105,23 +106,24 @@ class Link(QtCore.QObject):
self._capturing = result.get("capturing", False)
# If the controller is remote the capture path should be rewrite to something local
if Controller.instance().isRemote():
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
self._capture_file = QtCore.QTemporaryFile()
self._capture_file.open(QtCore.QFile.WriteOnly)
self._capture_file.setAutoRemove(True)
self._capture_file_path = self._capture_file.fileName()
Controller.instance().get(
"/projects/{project_id}/links/{link_id}/pcap".format(
project_id=self.project().id(),
link_id=self._link_id),
None,
showProgress=False,
downloadProgressCallback=self._downloadPcapProgress,
ignoreErrors=True, # If something is wrong avoid disconnect us from server
timeout=None)
else:
self._capture_file_path = result["capture_file_path"]
if self._capturing:
if Controller.instance().isRemote():
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
self._capture_file = QtCore.QTemporaryFile()
self._capture_file.open(QtCore.QFile.WriteOnly)
self._capture_file.setAutoRemove(True)
self._capture_file_path = self._capture_file.fileName()
Controller.instance().get(
"/projects/{project_id}/links/{link_id}/pcap".format(
project_id=self.project().id(),
link_id=self._link_id),
None,
showProgress=False,
downloadProgressCallback=self._downloadPcapProgress,
ignoreErrors=True, # If something is wrong avoid disconnect us from server
timeout=None)
else:
self._capture_file_path = result["capture_file_path"]
if "nodes" in result:
self._nodes = result["nodes"]
@@ -157,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)
@@ -170,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)
@@ -220,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
@@ -243,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?
@@ -305,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):
"""
@@ -358,11 +375,11 @@ class Link(QtCore.QObject):
if self._capture_file:
self._capture_file.close()
self._capture_file = None
if self._capture_file_path:
if self._capture_file_path and os.path.exists(self._capture_file_path):
try:
os.remove(self._capture_file_path)
except OSError as e:
log.error("Can't remove file {}".format(self._capture_file_path))
log.error("Can't remove file {}: {}".format(self._capture_file_path, e))
self._capture_file_path = None
Controller.instance().post(
"/projects/{project_id}/links/{link_id}/stop_capture".format(

View File

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

View File

@@ -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,18 +123,23 @@ 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:
return False
except pywintypes.error as e:
if e.winerror == 1060:
if e.winerror == 1060: # service is not installed
return False
else:
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
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

@@ -18,6 +18,7 @@
import sys
import os
import faulthandler
# Try to install updates & restart application if an update is installed
try:
@@ -110,6 +111,9 @@ def main():
Entry point for GNS3 GUI.
"""
# Get Python tracebacks explicitly, on a fault like segfault
faulthandler.enable()
# Sometimes (for example at first launch) the OSX app service launcher add
# an extra argument starting with -psn_. We filter it
if sys.platform.startswith("darwin"):
@@ -141,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", "")
@@ -263,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
@@ -287,7 +295,6 @@ def main():
mainwindow.show()
exit_code = app.exec_()
signal.signal(signal.SIGINT, orig_sigint)
signal.signal(signal.SIGTERM, orig_sigterm)
@@ -296,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
@@ -640,10 +660,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
return
self._screenshots_dir = os.path.dirname(path)
# add the extension if missing
file_format = "." + selected_filter[:4].lower().strip()
if not path.endswith(file_format):
path += file_format
# add the extension if missing (Mac OS automatically adds an extension already)
if not sys.platform.startswith("darwin"):
file_format = "." + selected_filter[:4].lower().strip()
if not path.endswith(file_format):
path += file_format
if not self.createScreenshot(path):
QtWidgets.QMessageBox.critical(self, "Screenshot", "Could not create screenshot file {}".format(path))
@@ -815,7 +836,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot called when connecting to all the nodes using the console.
"""
self.uiGraphicsView.consoleFromItems(self.uiGraphicsView.scene().items())
self.uiGraphicsView.consoleFromAllItems()
def _addNoteActionSlot(self):
"""
@@ -1049,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):
@@ -94,7 +95,9 @@ class Cloud(Node):
info = """Cloud device {name} is always-on
This is a node for external connections
""".format(name=self.name())
Device run on {host}
""".format(name=self.name(),
host=self.compute().name())
port_info = ""
for port in self._ports:

View File

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

View File

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

View File

@@ -19,7 +19,7 @@
Configuration page for clouds.
"""
from gns3.qt import QtCore, QtWidgets
from gns3.qt import QtCore, QtGui, QtWidgets
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.controller import Controller
from gns3.node import Node
@@ -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
@@ -68,6 +70,25 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
self.uiShowSpecialInterfacesCheckBox.stateChanged.connect(self._showSpecialInterfacesSlot)
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
# add an icon to warning button
icon = QtGui.QIcon.fromTheme("dialog-warning")
if icon.isNull():
icon = QtGui.QIcon(':/icons/dialog-warning.svg')
self.uiEthernetWarningPushButton.setIcon(icon)
def _refreshInterfaces(self):
"""
Refresh the network interfaces.
"""
if self._node:
self._interfaces = self._node.interfaces()
self._loadNetworkInterfaces(self._interfaces)
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.
@@ -115,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.
@@ -193,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>
@@ -82,9 +82,6 @@
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="dialog-warning"/>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
@@ -94,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>
@@ -102,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>
@@ -118,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>
@@ -145,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">
@@ -168,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

@@ -11,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_cloudConfigPageWidget(object):
def setupUi(self, cloudConfigPageWidget):
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
cloudConfigPageWidget.resize(758, 299)
cloudConfigPageWidget.resize(1000, 378)
self.verticalLayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
@@ -39,20 +39,21 @@ 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("")
icon = QtGui.QIcon.fromTheme("dialog-warning")
self.uiEthernetWarningPushButton.setIcon(icon)
self.uiEthernetWarningPushButton.setObjectName("uiEthernetWarningPushButton")
self.gridLayout_3.addWidget(self.uiEthernetWarningPushButton, 0, 1, 1, 1)
self.uiShowSpecialInterfacesCheckBox = QtWidgets.QCheckBox(self.EthernetTab)
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_()
@@ -60,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")
@@ -68,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)
@@ -93,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")
@@ -243,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

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

View File

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

View File

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

View File

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

@@ -217,13 +217,15 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
"""
image = self.uiIOSImageLineEdit.text()
platform = self.uiPlatformComboBox.currentText()
Controller.instance().postCompute("/autoidlepc",
ram = self.uiRamSpinBox.value()
Controller.instance().postCompute("/auto_idlepc",
self._compute_id,
self._computeAutoIdlepcCallback,
timeout=None,
body={
"image": image,
"platform": platform
"platform": platform,
"ram": ram
})
self.uiIdlePCFinderPushButton.setEnabled(False)

View File

@@ -302,7 +302,7 @@ class Router(Node):
specific_info=router_specific_info,
ram=self._settings["ram"],
nvram=self._settings["nvram"],
host=self.compute().id(),
host=self.compute().name(),
console=self._settings["console"],
aux=self._settings["aux"],
image_name=os.path.basename(self._settings["image"]),

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

@@ -75,6 +75,7 @@ class Qemu(Module):
if sys.platform.startswith("linux"):
server_settings = {
"enable_kvm": self._settings["enable_kvm"],
"require_kvm": self._settings["require_kvm"]
}
LocalServerConfig.instance().saveSettings(self.__class__.__name__, server_settings)

View File

@@ -40,11 +40,24 @@ class QemuPreferencesPage(QtWidgets.QWidget, Ui_QemuPreferencesPageWidget):
# connect signals
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
self.uiKVMAccelerationCheckBox.stateChanged.connect(self._kvmAccelerationSlot)
if not sys.platform.startswith("linux"):
# KVM can only run on Linux
self.uiKVMAccelerationCheckBox.hide()
def _kvmAccelerationSlot(self, state):
"""
Slot to enable or not the require KVM acceleration check box.
"""
if state:
self.uiRequireKVMAccelerationCheckBox.setEnabled(True)
self.uiRequireKVMAccelerationCheckBox.setChecked(True)
else:
self.uiRequireKVMAccelerationCheckBox.setEnabled(False)
self.uiRequireKVMAccelerationCheckBox.setChecked(False)
def _restoreDefaultsSlot(self):
"""
Slot to populate the page widgets with the default settings.
@@ -60,6 +73,7 @@ class QemuPreferencesPage(QtWidgets.QWidget, Ui_QemuPreferencesPageWidget):
"""
self.uiKVMAccelerationCheckBox.setChecked(settings["enable_kvm"])
self.uiRequireKVMAccelerationCheckBox.setChecked(settings["require_kvm"])
def loadPreferences(self):
"""
@@ -74,5 +88,6 @@ class QemuPreferencesPage(QtWidgets.QWidget, Ui_QemuPreferencesPageWidget):
Saves QEMU preferences.
"""
new_settings = {"enable_kvm": self.uiKVMAccelerationCheckBox.isChecked()}
new_settings = {"enable_kvm": self.uiKVMAccelerationCheckBox.isChecked(),
"require_kvm": self.uiRequireKVMAccelerationCheckBox.isChecked()}
Qemu.instance().setSettings(new_settings)

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

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

View File

@@ -20,7 +20,6 @@ QEMU VM implementation.
"""
from gns3.node import Node
from gns3.image_manager import ImageManager
from .settings import QEMU_VM_SETTINGS

View File

@@ -23,6 +23,7 @@ from gns3.node import Node
QEMU_SETTINGS = {
"enable_kvm": True,
"require_kvm": True,
}
QEMU_VM_SETTINGS = {

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>366</width>
<height>183</height>
<width>414</width>
<height>223</height>
</rect>
</property>
<property name="windowTitle">
@@ -37,6 +37,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="uiRequireKVMAccelerationCheckBox">
<property name="text">
<string>Require KVM acceleration</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">

View File

@@ -2,8 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_preferences_page.ui'
#
# Created: Wed Dec 7 21:25:31 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_QemuPreferencesPageWidget(object):
def setupUi(self, QemuPreferencesPageWidget):
QemuPreferencesPageWidget.setObjectName("QemuPreferencesPageWidget")
QemuPreferencesPageWidget.resize(366, 183)
QemuPreferencesPageWidget.resize(414, 223)
self.verticalLayout = QtWidgets.QVBoxLayout(QemuPreferencesPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(QemuPreferencesPageWidget)
@@ -26,6 +25,9 @@ class Ui_QemuPreferencesPageWidget(object):
self.uiKVMAccelerationCheckBox = QtWidgets.QCheckBox(self.uiServerSettingsTabWidget)
self.uiKVMAccelerationCheckBox.setObjectName("uiKVMAccelerationCheckBox")
self.verticalLayout_2.addWidget(self.uiKVMAccelerationCheckBox)
self.uiRequireKVMAccelerationCheckBox = QtWidgets.QCheckBox(self.uiServerSettingsTabWidget)
self.uiRequireKVMAccelerationCheckBox.setObjectName("uiRequireKVMAccelerationCheckBox")
self.verticalLayout_2.addWidget(self.uiRequireKVMAccelerationCheckBox)
spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_2.addItem(spacerItem)
self.uiTabWidget.addTab(self.uiServerSettingsTabWidget, "")
@@ -47,6 +49,7 @@ class Ui_QemuPreferencesPageWidget(object):
_translate = QtCore.QCoreApplication.translate
QemuPreferencesPageWidget.setWindowTitle(_translate("QemuPreferencesPageWidget", "QEMU"))
self.uiKVMAccelerationCheckBox.setText(_translate("QemuPreferencesPageWidget", "Enable KVM acceleration"))
self.uiRequireKVMAccelerationCheckBox.setText(_translate("QemuPreferencesPageWidget", "Require KVM acceleration"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("QemuPreferencesPageWidget", "Local settings"))
self.uiRestoreDefaultsPushButton.setText(_translate("QemuPreferencesPageWidget", "Restore defaults"))

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>594</width>
<height>645</height>
<width>655</width>
<height>696</height>
</rect>
</property>
<property name="windowTitle">
@@ -24,16 +24,7 @@
<string>General settings</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>10</number>
</property>
<item row="4" column="0">
@@ -203,16 +194,7 @@
<string>HDD</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>10</number>
</property>
<item>
@@ -435,16 +417,7 @@
<string>CD/DVD</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>10</number>
</property>
<item>
@@ -500,16 +473,7 @@
<string>Network</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>10</number>
</property>
<item row="6" column="1">
@@ -633,16 +597,7 @@
<string>Advanced settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>10</number>
</property>
<item>
@@ -836,7 +791,18 @@
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="uiQemuOptionsLineEdit"/>
<widget class="QLineEdit" name="uiQemuOptionsLineEdit">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Variable replacements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;%vm-name% =VM name&lt;/li&gt;
&lt;li&gt;%vm-id% =VM ID&lt;/li&gt;
&lt;li&gt;%project-id% = project ID&lt;/li&gt;
&lt;li&gt;%project-path% = project path&lt;/li&gt;
&lt;/ul&gt;
&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="uiBaseVMCheckBox">

View File

@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
#
# Created by: PyQt5 UI code generator 5.8
# 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_QemuVMConfigPageWidget(object):
def setupUi(self, QemuVMConfigPageWidget):
QemuVMConfigPageWidget.setObjectName("QemuVMConfigPageWidget")
QemuVMConfigPageWidget.resize(594, 645)
QemuVMConfigPageWidget.resize(655, 696)
self.verticalLayout = QtWidgets.QVBoxLayout(QemuVMConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiQemutabWidget = QtWidgets.QTabWidget(QemuVMConfigPageWidget)
@@ -493,6 +493,14 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiProcessPriorityComboBox.setItemText(5, _translate("QemuVMConfigPageWidget", "Very low"))
self.groupBox.setTitle(_translate("QemuVMConfigPageWidget", "Additional settings"))
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:"))
self.uiQemuOptionsLineEdit.setToolTip(_translate("QemuVMConfigPageWidget", "<html><head/><body><p>Variable replacements:</p>\n"
"<ul>\n"
"<li>%vm-name% =VM name</li>\n"
"<li>%vm-id% =VM ID</li>\n"
"<li>%project-id% = project ID</li>\n"
"<li>%project-path% = project path</li>\n"
"</ul>\n"
"</body></html>"))
self.uiBaseVMCheckBox.setText(_translate("QemuVMConfigPageWidget", "Use as a linked base VM"))
self.uiACPIShutdownCheckBox.setText(_translate("QemuVMConfigPageWidget", "Enable ACPI shutdown (experimental)"))
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiAdvancedSettingsTab), _translate("QemuVMConfigPageWidget", "Advanced settings"))

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>

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