mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-06-07 19:16:26 +03:00
Compare commits
348 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
450fbc9af3 | ||
|
|
469ee8fab8 | ||
|
|
6ccfcaf76e | ||
|
|
520e857874 | ||
|
|
012c7b4241 | ||
|
|
1d71cd5bf0 | ||
|
|
d96277882a | ||
|
|
ecec917752 | ||
|
|
ea9c1a8ee1 | ||
|
|
cfbb09fb57 | ||
|
|
dc8aa1fb92 | ||
|
|
786cc8aa65 | ||
|
|
4a353e08e3 | ||
|
|
2a59013604 | ||
|
|
7732aaf9a5 | ||
|
|
1f566a31cf | ||
|
|
10d75e15da | ||
|
|
17def7e00a | ||
|
|
f68a8ea829 | ||
|
|
50066b2f12 | ||
|
|
21a99d4376 | ||
|
|
f97d3041b8 | ||
|
|
31d6a065b0 | ||
|
|
8f077456b1 | ||
|
|
a29f3e35c0 | ||
|
|
fc3781550a | ||
|
|
d285e62c04 | ||
|
|
44d70de687 | ||
|
|
752c516f82 | ||
|
|
e1ec6c5771 | ||
|
|
e8308869d9 | ||
|
|
484c5abe9d | ||
|
|
fe222b873f | ||
|
|
f8bb6661dd | ||
|
|
0f9aab9230 | ||
|
|
a5cf5e16b7 | ||
|
|
097458d108 | ||
|
|
f4cafac9c7 | ||
|
|
7f132fdc36 | ||
|
|
6b7d629755 | ||
|
|
b7ccc37ea5 | ||
|
|
bbe2826c77 | ||
|
|
68e2a0ee39 | ||
|
|
52418ed94a | ||
|
|
a1496bffd4 | ||
|
|
911f6305fa | ||
|
|
c6594d4845 | ||
|
|
538adc4817 | ||
|
|
961c5652ea | ||
|
|
c0ecf3ccc4 | ||
|
|
cf73db25b4 | ||
|
|
dd79939140 | ||
|
|
45b3c17c97 | ||
|
|
1ddf3e6388 | ||
|
|
1c8e166393 | ||
|
|
ec324f9b01 | ||
|
|
cbfd59498e | ||
|
|
0216bc8b4d | ||
|
|
a8477597ab | ||
|
|
e240dbad6b | ||
|
|
8d183a3283 | ||
|
|
3af5046d0f | ||
|
|
c8397a1ef7 | ||
|
|
b419891950 | ||
|
|
2c1ba697bd | ||
|
|
3000a9aa7f | ||
|
|
2f8541c543 | ||
|
|
5c3d4b2ab6 | ||
|
|
f44ac8cba5 | ||
|
|
7ef49fbca7 | ||
|
|
5ccf5778a2 | ||
|
|
6030d5e019 | ||
|
|
6de8880937 | ||
|
|
08c89c4fac | ||
|
|
e411d497c4 | ||
|
|
e037835769 | ||
|
|
a5f4ec0135 | ||
|
|
154f10a686 | ||
|
|
e5320c318f | ||
|
|
07ea6207c1 | ||
|
|
12398881f8 | ||
|
|
27a8e3c7f8 | ||
|
|
d92ff1abe3 | ||
|
|
e97b3b6a42 | ||
|
|
5ee3f73213 | ||
|
|
a30aa2f5f1 | ||
|
|
98bb6590aa | ||
|
|
4250e961a3 | ||
|
|
3c46a3a72d | ||
|
|
c55442a517 | ||
|
|
45e0080726 | ||
|
|
95558ec2e6 | ||
|
|
5b56d54030 | ||
|
|
d5ee1ea5d2 | ||
|
|
69b8c07c0a | ||
|
|
dbe73eb8d7 | ||
|
|
706f89debb | ||
|
|
ec0be9e22b | ||
|
|
0e6fa597ec | ||
|
|
f81450c65a | ||
|
|
38cbe70aaa | ||
|
|
47d335f4c9 | ||
|
|
20d4f73f56 | ||
|
|
5204184029 | ||
|
|
9915beeb8e | ||
|
|
1ea383fce2 | ||
|
|
2744e669b4 | ||
|
|
6ab2d63bdc | ||
|
|
0de6bfe7e1 | ||
|
|
f144103bca | ||
|
|
c0b26aff48 | ||
|
|
9601e4e6f2 | ||
|
|
88708c2a8d | ||
|
|
8eff12194d | ||
|
|
b0520b2bd4 | ||
|
|
17d2c023bf | ||
|
|
ce9fdea0a0 | ||
|
|
24d7dacb4e | ||
|
|
bb36765407 | ||
|
|
250db92ce0 | ||
|
|
d59ec39505 | ||
|
|
5e9ae04dc1 | ||
|
|
ddb0fccda3 | ||
|
|
9b22a52f14 | ||
|
|
948878bfdd | ||
|
|
7340abbaa9 | ||
|
|
4ea0528bf2 | ||
|
|
49005e6add | ||
|
|
5484c039b5 | ||
|
|
daaf71b6d2 | ||
|
|
450f0e006b | ||
|
|
a6a967fbde | ||
|
|
1a6293709e | ||
|
|
2ed53225e0 | ||
|
|
b8798fbda5 | ||
|
|
368de32faa | ||
|
|
98d01cbfa0 | ||
|
|
ad62bb7832 | ||
|
|
637061663a | ||
|
|
c137198985 | ||
|
|
946efb61de | ||
|
|
4c610acfa4 | ||
|
|
37f74824f1 | ||
|
|
5ccf8c414d | ||
|
|
913f0d5e4a | ||
|
|
061bac0cc6 | ||
|
|
ec59cd87bd | ||
|
|
05d9ee8499 | ||
|
|
a72ece5c18 | ||
|
|
63baa2eff0 | ||
|
|
b91fd4a0c2 | ||
|
|
718217e332 | ||
|
|
c202c5e4be | ||
|
|
71830dd69f | ||
|
|
37a7fdfa68 | ||
|
|
0efe006cad | ||
|
|
4a663a5910 | ||
|
|
a559bd4ae4 | ||
|
|
5ebb3011d3 | ||
|
|
81300fd40e | ||
|
|
d4dda2a285 | ||
|
|
5a4342d4b8 | ||
|
|
94fc5e6c4f | ||
|
|
a3e81fbf2e | ||
|
|
514eb97eac | ||
|
|
7637039cb2 | ||
|
|
ac989b191b | ||
|
|
c971cef31b | ||
|
|
c1af2df780 | ||
|
|
eaaa141be9 | ||
|
|
226169cdc6 | ||
|
|
42a4c89f20 | ||
|
|
1482b0e804 | ||
|
|
8ebe3435c4 | ||
|
|
a1cd34d7c4 | ||
|
|
1e4a44135c | ||
|
|
a407f1ec90 | ||
|
|
faab113384 | ||
|
|
c158b7fc46 | ||
|
|
16de9e830f | ||
|
|
25c625c0bb | ||
|
|
bf42d1a355 | ||
|
|
1c0f3493ee | ||
|
|
c3c1f87c5e | ||
|
|
6b80914385 | ||
|
|
a114d9ace7 | ||
|
|
4dca4d057a | ||
|
|
17af21e29a | ||
|
|
7fbce0266d | ||
|
|
d5cdbdbf90 | ||
|
|
e5a790f4b2 | ||
|
|
f3769df0d6 | ||
|
|
a21db74941 | ||
|
|
d1e1f6dfb6 | ||
|
|
cc45c9631a | ||
|
|
d16a52e389 | ||
|
|
ee2bea7cdd | ||
|
|
7cbc25cbbf | ||
|
|
7237cf1b88 | ||
|
|
965923900b | ||
|
|
a5a3a4e8cc | ||
|
|
d898b30d84 | ||
|
|
e86ced750e | ||
|
|
e15b717cb0 | ||
|
|
d8bd33f0e7 | ||
|
|
bc2fbe33ef | ||
|
|
b99b26f463 | ||
|
|
5b7606793f | ||
|
|
b8b5e8739e | ||
|
|
0126c30887 | ||
|
|
a89086ff60 | ||
|
|
9ca35c56de | ||
|
|
3ddccf40a8 | ||
|
|
1398ef323a | ||
|
|
d52c4d839d | ||
|
|
6989ee2c8b | ||
|
|
5c182e95ca | ||
|
|
bcb7a8e57b | ||
|
|
dba75e844e | ||
|
|
6733739fa5 | ||
|
|
f5de62aa05 | ||
|
|
4087d35f6a | ||
|
|
f6a1af46a0 | ||
|
|
6cef2fed5a | ||
|
|
e16c8db311 | ||
|
|
61c95a93ca | ||
|
|
0df36dab30 | ||
|
|
9a5da633e0 | ||
|
|
02fed964f2 | ||
|
|
84ba56ae74 | ||
|
|
e8c4758cb7 | ||
|
|
d8dc31965f | ||
|
|
9af48ba9a3 | ||
|
|
67d8e317e0 | ||
|
|
64392780c5 | ||
|
|
81bb159d45 | ||
|
|
85352af9bd | ||
|
|
65cfdf6b33 | ||
|
|
5d45dbebf6 | ||
|
|
0a7b6d81d8 | ||
|
|
588dcadd3a | ||
|
|
f24c93a55f | ||
|
|
26e5c80406 | ||
|
|
eb502232a2 | ||
|
|
25e17d718c | ||
|
|
89108070df | ||
|
|
f4df3ff9c0 | ||
|
|
90f80b9804 | ||
|
|
3e86044132 | ||
|
|
78d805cebc | ||
|
|
289f754108 | ||
|
|
1a0c1f826b | ||
|
|
9837d661a5 | ||
|
|
ff60776769 | ||
|
|
7ac442631a | ||
|
|
8da2ff3a97 | ||
|
|
a04d9784f2 | ||
|
|
623aa4a2de | ||
|
|
ef3c2afab9 | ||
|
|
73e59d92ca | ||
|
|
8f3d5bf038 | ||
|
|
d638c6e0d7 | ||
|
|
c5688cacf9 | ||
|
|
b34bcd6369 | ||
|
|
5e0fc3675f | ||
|
|
e75a21e2ed | ||
|
|
aeee44e597 | ||
|
|
3cebee64ad | ||
|
|
fd1619cfd3 | ||
|
|
4573d2aed8 | ||
|
|
7f29c497cc | ||
|
|
6da42b5013 | ||
|
|
430366947f | ||
|
|
3870f8ecdc | ||
|
|
f68626e4cc | ||
|
|
6b4126b688 | ||
|
|
2ef9890dc1 | ||
|
|
100e3dbf27 | ||
|
|
b85db6e24f | ||
|
|
05d2077b16 | ||
|
|
357d039434 | ||
|
|
0288384c85 | ||
|
|
84347848e9 | ||
|
|
24e5ef885c | ||
|
|
0d8255ecaf | ||
|
|
679548e4ad | ||
|
|
e5384af45d | ||
|
|
ae68d4d84b | ||
|
|
2ab81816ef | ||
|
|
4c4241183a | ||
|
|
46406d1e7b | ||
|
|
a92573394f | ||
|
|
6baf628997 | ||
|
|
8f9190e094 | ||
|
|
7e14e734b2 | ||
|
|
76fc2f07ce | ||
|
|
eee066d5f3 | ||
|
|
9dead47a37 | ||
|
|
a22bd8e9be | ||
|
|
a4b897d458 | ||
|
|
e784f21c0f | ||
|
|
3a000cdc60 | ||
|
|
405f3b3382 | ||
|
|
7397f76566 | ||
|
|
178cb35d6a | ||
|
|
012e5d331d | ||
|
|
bd81d36635 | ||
|
|
234eab57c8 | ||
|
|
e4b19714f4 | ||
|
|
7bfba1015b | ||
|
|
498ba2d2b1 | ||
|
|
f3756b8401 | ||
|
|
68f6d37aab | ||
|
|
6ce35fa5b5 | ||
|
|
e376753859 | ||
|
|
7b03c3eae7 | ||
|
|
902ba42be1 | ||
|
|
73fe898eda | ||
|
|
1ff488d39a | ||
|
|
1622a79383 | ||
|
|
1564c63a42 | ||
|
|
29f651aaea | ||
|
|
9ee0222339 | ||
|
|
6e1384c985 | ||
|
|
20190c5816 | ||
|
|
cab3412ddc | ||
|
|
6d74ce4070 | ||
|
|
159d21af9a | ||
|
|
713feff11f | ||
|
|
64c5ca712e | ||
|
|
1572a6f67f | ||
|
|
fcee5c6916 | ||
|
|
3d21f9a997 | ||
|
|
d93ad5e9d5 | ||
|
|
13739281da | ||
|
|
1f281a807b | ||
|
|
2ca250d2c2 | ||
|
|
b82b031168 | ||
|
|
c48048f013 | ||
|
|
9aaca9955a | ||
|
|
a0e6a82ea2 | ||
|
|
9a3e320e95 | ||
|
|
c3fce51493 | ||
|
|
116cf55758 | ||
|
|
269c6bd0cd | ||
|
|
31aa612a62 | ||
|
|
55f396694f | ||
|
|
20efde749c |
@@ -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
276
CHANGELOG
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
include README.rst
|
||||
include AUTHORS
|
||||
include INSTALL
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
|
||||
@@ -13,7 +13,7 @@ GNS3 GUI repository.
|
||||
Installation
|
||||
------------
|
||||
|
||||
https://gns3.com/support/docs
|
||||
Please see https://docs.gns3.com/
|
||||
|
||||
Development
|
||||
-------------
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 /
|
||||
|
||||
@@ -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:
|
||||
|
||||
88
gns3/dialogs/project_welcome_dialog.py
Normal file
88
gns3/dialogs/project_welcome_dialog.py
Normal 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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
91
gns3/image_upload_manager.py
Normal file
91
gns3/image_upload_manager.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
136
gns3/items/logo_item.py
Normal 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')
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
67
gns3/link.py
67
gns3/link.py
@@ -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(
|
||||
|
||||
@@ -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():
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
15
gns3/main.py
15
gns3/main.py
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>&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>&Refresh</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="UDPTab">
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<string>Frame Relay Switch</string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><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></string>
|
||||
<string><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></string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0" colspan="2">
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"]),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -20,7 +20,6 @@ QEMU VM implementation.
|
||||
"""
|
||||
|
||||
from gns3.node import Node
|
||||
from gns3.image_manager import ImageManager
|
||||
from .settings import QEMU_VM_SETTINGS
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from gns3.node import Node
|
||||
|
||||
QEMU_SETTINGS = {
|
||||
"enable_kvm": True,
|
||||
"require_kvm": True,
|
||||
}
|
||||
|
||||
QEMU_VM_SETTINGS = {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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><html><head/><body><p>Variable replacements:</p>
|
||||
<ul>
|
||||
<li>%vm-name% =VM name</li>
|
||||
<li>%vm-id% =VM ID</li>
|
||||
<li>%project-id% = project ID</li>
|
||||
<li>%project-path% = project path</li>
|
||||
</ul>
|
||||
</body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiBaseVMCheckBox">
|
||||
|
||||
@@ -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"))
|
||||
|
||||
249
gns3/modules/traceng/__init__.py
Normal file
249
gns3/modules/traceng/__init__.py
Normal 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
|
||||
0
gns3/modules/traceng/dialogs/__init__.py
Normal file
0
gns3/modules/traceng/dialogs/__init__.py
Normal file
86
gns3/modules/traceng/dialogs/traceng_node_wizard.py
Normal file
86
gns3/modules/traceng/dialogs/traceng_node_wizard.py
Normal 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
|
||||
0
gns3/modules/traceng/pages/__init__.py
Normal file
0
gns3/modules/traceng/pages/__init__.py
Normal file
155
gns3/modules/traceng/pages/traceng_node_configuration_page.py
Normal file
155
gns3/modules/traceng/pages/traceng_node_configuration_page.py
Normal 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
|
||||
189
gns3/modules/traceng/pages/traceng_node_preferences_page.py
Normal file
189
gns3/modules/traceng/pages/traceng_node_preferences_page.py
Normal 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)
|
||||
127
gns3/modules/traceng/pages/traceng_preferences_page.py
Normal file
127
gns3/modules/traceng/pages/traceng_preferences_page.py
Normal 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)
|
||||
36
gns3/modules/traceng/settings.py
Normal file
36
gns3/modules/traceng/settings.py
Normal 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,
|
||||
}
|
||||
172
gns3/modules/traceng/traceng_node.py
Normal file
172
gns3/modules/traceng/traceng_node.py
Normal 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"
|
||||
0
gns3/modules/traceng/ui/__init__.py
Normal file
0
gns3/modules/traceng/ui/__init__.py
Normal file
119
gns3/modules/traceng/ui/traceng_node_configuration_page.ui
Executable file
119
gns3/modules/traceng/ui/traceng_node_configuration_page.ui
Executable 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>&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>
|
||||
@@ -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:"))
|
||||
|
||||
160
gns3/modules/traceng/ui/traceng_node_preferences_page.ui
Normal file
160
gns3/modules/traceng/ui/traceng_node_preferences_page.ui
Normal 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>&New</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiEditTraceNGPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Edit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiDeleteTraceNGPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&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>
|
||||
83
gns3/modules/traceng/ui/traceng_node_preferences_page_ui.py
Normal file
83
gns3/modules/traceng/ui/traceng_node_preferences_page_ui.py
Normal 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"))
|
||||
|
||||
140
gns3/modules/traceng/ui/traceng_node_wizard.ui
Normal file
140
gns3/modules/traceng/ui/traceng_node_wizard.ui
Normal 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
Reference in New Issue
Block a user