Compare commits

...

892 Commits

Author SHA1 Message Date
grossmj
8d1bff782c Release v2.2.0rc5 2019-09-09 15:06:14 +07:00
grossmj
4e3eee2383 Adjust size for setup dialog and remove question about running the wizard again. Ref #2846 2019-09-07 23:46:02 +07:00
grossmj
da8aa0d2fd Release v2.2.0rc4 2019-08-30 15:23:32 +07:00
grossmj
5b4481c43a Fix issue when asking to run the setup wizard again. Ref #2846 2019-08-29 15:59:40 +07:00
grossmj
593cb8c1fd Remove warning about VirtualBox not supporting nested virtualization. Ref https://github.com/GNS3/gns3-server/issues/1610 2019-08-27 17:28:44 +07:00
grossmj
210cf63fe2 Ask user if they want to see the wizard again. Ref #2846 2019-08-27 16:15:50 +07:00
grossmj
3b178013c0 Bump version to 2.2.0dev17 2019-08-23 18:20:44 +07:00
grossmj
6e44d6b919 Merge branch 'master' into 2.2
# Conflicts:
#	gns3/version.py
2019-08-20 17:35:13 +07:00
grossmj
6b520b8036 Bump version to 2.2.0dev16 2019-08-20 17:33:48 +07:00
grossmj
803782b9d8 Release v2.2.0rc3 2019-08-11 19:14:56 -07:00
grossmj
d3d6ca3f2e Revert to jsonschema 2.6.0 due to packaging problem. 2019-08-11 19:11:41 -07:00
grossmj
f545c793f8 Release v2.2.0rc2 2019-08-10 12:04:19 -05:00
grossmj
47d6a4fef6 Release v2.2.0rc1 2019-08-10 11:42:13 -05:00
grossmj
8862b608cf Bump jsonschema to version 3.0.2 2019-08-09 13:47:58 -05:00
grossmj
76832ab83f Fix "Unable to change Remote Main Server IP". Fixes #2823 2019-08-02 17:23:31 -02:30
grossmj
fed245fd34 Fix "AttributeError: 'QGraphicsTextItem' object has no attribute 'locked'". Fixes #2814 2019-07-30 15:50:40 -02:30
grossmj
3e0f1affd0 Merge branch '2.2' 2019-07-12 12:07:33 +02:00
grossmj
2110c2805e Development on 2.2.0dev15 2019-07-11 17:34:56 +02:00
grossmj
46cfdd8314 Release v2.2.0b4 2019-07-11 16:58:35 +02:00
grossmj
f8f648c2b6 Fix issue preventing to open the QFileDialog in the correct directory. 2019-07-11 16:50:59 +02:00
grossmj
7cd0187f33 Remove unused edit readme action. Fixes #2816 2019-07-10 12:10:17 +02:00
grossmj
4d8f362f11 Remove deprecated Qemu parameter to run legacy ASA VMs. Fixes #2827 2019-07-10 11:33:04 +02:00
grossmj
469eaa4737 Upload images on remote controller. Fixes #2828 2019-07-10 11:23:29 +02:00
grossmj
c921224b30 Preferences dialog: send API request only if connected to controller 2019-07-05 18:07:48 +02:00
grossmj
61487b2e2f Fix AttributeError: 'QGraphicsTextItem' object has no attribute 'locked'. Fixes #2814 2019-06-24 15:29:07 +02:00
grossmj
9affca495e Fix KeyError: 'chassis' when converting old IOS templates. Fixes #2813 2019-06-24 15:14:20 +02:00
grossmj
9d8886a640 Development on 2.2.0dev14 2019-06-15 16:38:06 +02:00
grossmj
98cfec1b77 Release v2.2.0b3 2019-06-15 15:39:32 +02:00
grossmj
aed174953e Merge 2.1 into 2.2 branch. 2019-06-15 15:26:20 +02:00
grossmj
f0feea8262 Fix template migration issues from GUI to controller. Fixes https://github.com/GNS3/gns3-gui/issues/2803 2019-06-15 12:52:50 +02:00
grossmj
e2aeaf0a78 Development on 2.1.22dev1 2019-06-14 19:42:18 +02:00
grossmj
b92bb94875 Release v2.1.21 2019-06-14 10:47:08 +02:00
grossmj
c56db59353 Increase timeout from 2 to 5 seconds for synchronous check. Ref #2805 2019-06-10 22:20:40 +02:00
grossmj
a87c4e21d7 %guest-cid% variable implementation for Qemu VMs. Fixes https://github.com/GNS3/gns3-gui/issues/2804 2019-06-04 18:00:44 +02:00
grossmj
ed99a989d7 Fix "General Preferences dialog displays misleading information". Fixes #2801 2019-06-04 17:05:01 +02:00
grossmj
f9a4c9399a Increase timeout from 2 to 5 seconds for synchronous check. Ref #2805 2019-05-31 09:17:34 +02:00
grossmj
efb5c8ca9a Development on 2.2.0dev13 2019-05-29 17:52:38 +07:00
grossmj
0946dff3a0 Release v2.2.0b2 2019-05-29 17:16:59 +07:00
grossmj
d7d96b10e5 Merging 2.1 into 2.2 branch 2019-05-29 16:50:36 +07:00
grossmj
0c0b2d5cb3 Development on 2.1.21dev1 2019-05-29 16:37:43 +07:00
grossmj
450fbc9af3 Release v2.1.20 2019-05-29 15:44:25 +07:00
grossmj
469ee8fab8 Fix KeyError: 'endpoint' issue. Fixes #2802 2019-05-28 23:17:55 +07:00
grossmj
6ccfcaf76e Development on 2.1.20dev1 2019-05-28 16:33:43 +07:00
grossmj
520e857874 Release v2.1.19 2019-05-28 15:23:35 +07:00
grossmj
012c7b4241 Fix wrong aligment of symbols in saved/exported projects. Fixes #2800 2019-05-27 16:33:51 +07:00
grossmj
1d71cd5bf0 Replace urllib.request by Qt implementation for local server synchronous check. Fixes #2793 2019-05-27 16:03:55 +07:00
grossmj
17d1a7f4ed Support snapshots for portable projects. Fixes https://github.com/GNS3/gns3-gui/issues/2792 2019-05-27 15:35:47 +07:00
grossmj
0cd5c08c6b Fix event notification problem for projects and how snapshots are restored. 2019-05-27 15:24:36 +07:00
grossmj
20ac503fe9 Do not close the nodes dock widget when creating project. 2019-05-26 15:20:53 +07:00
grossmj
5f737c2c7c Fix no scan for images on remote controller. Fixes #2799 2019-05-26 15:12:22 +07:00
grossmj
eb1a37be36 Remove problematic tests. 2019-05-25 17:49:33 +07:00
grossmj
07c64b5432 Use QNetworkAccessManager to download custom appliance symbols. 2019-05-25 16:25:02 +07:00
grossmj
ce981d1c49 Experimental auto upgrade should not be available for "frozen" app. Fixes #2797 2019-05-25 15:17:23 +07:00
grossmj
32a9f2556e Use QNetworkAccessManager for synchronous local server check. Ref #2793
Remove unused code.
2019-05-25 15:04:37 +07:00
grossmj
7f08675121 Merging 2.2 into master 2019-05-24 15:27:07 +07:00
grossmj
1dc3c13df2 Don't allow link labels to be moved for locked nodes. Fixes #2794 2019-05-23 15:10:40 +07:00
grossmj
6a6e86b325 Merge 2.1 into 2.2 branch 2019-05-23 14:51:53 +07:00
grossmj
d96277882a Set grid's minimum to 5. Fixes #2795 2019-05-23 14:41:53 +07:00
grossmj
ecec917752 Development on 2.1.19dev1 2019-05-23 14:37:37 +07:00
grossmj
ea9c1a8ee1 Release v2.1.18 2019-05-22 16:13:28 +07:00
grossmj
cfbb09fb57 Fix error in HTTPConnection.request for Python3.6. Fixes #2793 2019-05-22 16:05:34 +07:00
grossmj
dc8aa1fb92 Catch more OSError/PermissionError when checking md5 on remote images. Fixes #2582 2019-05-22 15:23:56 +07:00
grossmj
786cc8aa65 Fix exception when grid size is 0. Fixes #2790 2019-05-22 15:13:16 +07:00
grossmj
4a353e08e3 Catch PermissionError when scanning local image directories. Fixes #2791 2019-05-22 14:55:07 +07:00
grossmj
1371921586 Development on 2.2.0dev12 2019-05-21 19:16:19 +07:00
grossmj
cd8696a714 Release v2.2.0b1 2019-05-21 15:26:54 +07:00
grossmj
17799719d6 Merge branch '2.1' into 2.2 2019-05-21 15:16:19 +07:00
grossmj
2a59013604 Revert "Make sure the latest PyQt5 version 5.12.x is used on Windows." Ref #2778 2019-05-20 12:01:16 +07:00
grossmj
1c46299dd9 Change behavior when an IOU license is verified. Fixes https://github.com/GNS3/gns3-server/issues/1555 2019-05-20 10:51:25 +07:00
grossmj
628d7cb909 Fix cannot load new profile. Fixes #2784 2019-05-19 16:33:33 +07:00
grossmj
b23c92c0fb Fix Docker extra volumes support 2019-05-19 14:26:03 +07:00
Jeremy Grossmann
49ce5a9f38 Merge pull request #2775 from kazkansouh/2.2-docker-volumes
Custom persistent docker volumes
2019-05-18 20:17:20 +07:00
Jeremy Grossmann
4575ea9f6d Merge pull request #2789 from GNS3/update-dockerfile
Update Dockerfile to Ubuntu 18.04
2019-05-18 20:10:31 +07:00
grossmj
fd6a00df6a Update Dockerfile to Ubuntu 18.04 2019-05-18 20:03:04 +07:00
grossmj
58ab4b424a Fix remote packet capture when controller is also remote. Fixes #2785 2019-05-18 17:33:34 +07:00
grossmj
1ea1abf582 Set console type to "none" by default for Ethernet switches and add a warning if trying to use "telnet". Fixes https://github.com/GNS3/gns3-gui/issues/2776 2019-05-18 14:28:20 +07:00
grossmj
e8caab74f4 Bump version to 2.2.0dev11 2019-05-18 14:11:07 +07:00
grossmj
9fce393fd1 Add tooltip for symbol theme support in general preferences. Fixes #2770 2019-05-18 14:01:08 +07:00
grossmj
827c11ae97 Merge remote-tracking branch 'origin/2.2' into 2.2 2019-05-18 13:45:57 +07:00
grossmj
eb370d5672 Merge 2.1 branch into 2.2 2019-05-18 13:45:39 +07:00
grossmj
7732aaf9a5 Release v2.1.17 2019-05-17 15:10:28 +07:00
Karim Kanso
63161eb760 Minor ui tweak to align extra hosts text edit look and feel with extra volume text edit. 2019-04-22 13:03:23 +01:00
Karim Kanso
5dba814d1b Support for persistent docker volumes to be configured via ui (requires corresponding commit on gns3-server) 2019-04-22 11:09:28 +01:00
Jeremy Grossmann
aecdc71f3a Merge pull request #2772 from GNS3/pyup-update-pytest-4.4.0-to-4.4.1
Update pytest to 4.4.1
2019-04-16 20:04:42 +07:00
pyup-bot
3209c1d0e6 Update pytest from 4.4.0 to 4.4.1 2019-04-16 03:33:58 +02:00
grossmj
2b3fb53ef2 Release v2.2.0a5 2019-04-15 17:05:20 +07:00
grossmj
cbbbece0e5 Revert "Drop old Qemu support (Windows and macOS) and legacy ASA support." Ref https://github.com/GNS3/gns3-server/issues/1579
This reverts commit 3e47267e35.
2019-04-15 15:56:01 +07:00
grossmj
56d742b19f Merge 2.1 into 2.2 2019-04-15 15:54:08 +07:00
grossmj
1f566a31cf Development on 2.1.17dev1 2019-04-15 12:41:41 +07:00
grossmj
10d75e15da Release v2.1.16 2019-04-15 12:00:18 +07:00
grossmj
17def7e00a Do not make NPF or NPCAP service mandatory to start the local server on Windows. 2019-04-15 10:33:27 +07:00
grossmj
106afd0987 Do not try to upload a local image that is already installed on the local server. 2019-04-15 00:29:43 +07:00
grossmj
bba9c5e1d8 Back to the major.minor version for config files. Ref https://github.com/GNS3/gns3-gui/issues/2756 2019-04-14 21:31:41 +07:00
grossmj
ae8e8013d4 Some adjustments with compute WebSocket handling. Ref https://github.com/GNS3/gns3-server/issues/1564 2019-04-14 16:48:12 +07:00
grossmj
3a5f1d60f9 Fix AttributeError: 'GraphicsView' object has no attribute '_import_config_dir'. Fixes #2768 2019-04-13 18:45:16 +07:00
grossmj
3f6eb61382 Do not try to lock a SvgIconItem. Fixes #2766 2019-04-13 18:40:54 +07:00
grossmj
32bfff381d Merge 2.1 into 2.2 2019-04-13 18:11:41 +07:00
grossmj
f68a8ea829 Fix OverflowError error with progress dialog. Fixes #2767 2019-04-13 17:38:43 +07:00
grossmj
50066b2f12 More fixes for stuck progress window. Fixes #2765 2019-04-13 17:10:24 +07:00
grossmj
21a99d4376 Fix adding multiple devices - stuck progress window. Fixes #2765 2019-04-13 17:04:23 +07:00
grossmj
f97d3041b8 Make sure the latest PyQt5 version 5.12.x is used on Windows. 2019-04-13 15:43:23 +07:00
grossmj
31d6a065b0 Show a warning when a config export is not supported. Ref #2762 2019-04-11 15:32:22 +07:00
grossmj
20bf63dbbf Prevent locked nodes to be deleted. Fixes https://github.com/GNS3/gns3-gui/issues/2764 2019-04-10 15:43:52 +07:00
grossmj
1c3e0ef640 Fix default telnet console command. 2019-04-09 21:18:55 +07:00
grossmj
5b58d3ab6d Bump version to 2.2.0dev10 2019-04-09 19:20:21 +07:00
grossmj
554c9205f3 Add PuTTY 0.71 and mark GNS3 PuTTY as deprecated. Fixes #2758 2019-04-09 19:19:56 +07:00
grossmj
543a8e7c33 Fix bug with IOS platform detection. Fixes #2760 2019-04-09 16:24:07 +07:00
grossmj
69ef35c674 Development on 2.2.0dev9 2019-04-05 22:01:35 +08:00
grossmj
45102a07b6 Release v2.2.0a4 2019-04-05 19:10:04 +08:00
grossmj
f0b8b22e8a Use the full version number for path to config files. Ref https://github.com/GNS3/gns3-gui/issues/2756 2019-04-05 18:44:31 +08:00
grossmj
d94f5a2d8c Fix error message when shutting down GUI without a started server. 2019-04-01 21:08:17 +07:00
grossmj
a768661c05 Fix remote packet capture and make sure packet capture is stopped when deleting an NIO. Fixes https://github.com/GNS3/gns3-gui/issues/2753 2019-04-01 19:47:32 +07:00
grossmj
4657b005b6 Restore migrate old settings. 2019-04-01 16:20:26 +07:00
grossmj
e71da830b0 Merge remote-tracking branch 'origin/2.2' into 2.2 2019-04-01 15:53:59 +07:00
grossmj
ebf2563200 Store config files in version specific location 2019-04-01 15:53:39 +07:00
Jeremy Grossmann
e8eaa00244 Merge pull request #2755 from GNS3/pyup-update-pytest-4.3.1-to-4.4.0
Update pytest to 4.4.0
2019-04-01 12:00:43 +07:00
pyup-bot
d750e7a427 Update pytest from 4.3.1 to 4.4.0 2019-04-01 06:53:29 +02:00
grossmj
bfc8adc904 Fix error messages on closing GNS3 application. Fixes https://github.com/GNS3/gns3-gui/issues/2750 2019-03-30 17:20:15 +07:00
grossmj
4de38ea590 Fix bug when list of files for an appliance is not displayed. 2019-03-30 15:44:30 +07:00
ziajka
cc0c6d0a7a Update 'local' to 'bundled' in server & gui, Fixes: #1561 2019-03-27 11:56:32 +01:00
grossmj
d1d0810233 Development on 2.2.0dev8 2019-03-25 23:44:19 +08:00
grossmj
ee3c758bb7 Release v2.2.0a3 2019-03-25 19:35:22 +08:00
grossmj
8f077456b1 Development on 2.1.16dev1 2019-03-21 13:56:11 +08:00
grossmj
a29f3e35c0 Release v2.1.15 2019-03-21 11:41:44 +08:00
grossmj
b12cb5c939 Fix bug when changing symbol. Fixes #2740 2019-03-20 15:15:29 +08:00
grossmj
ba646f5efa Fix image upload tests, second try. 2019-03-18 17:34:26 +07:00
grossmj
edafc29cdc Fix image upload tests. 2019-03-18 17:19:23 +07:00
grossmj
5aa67d18c0 Fix issue when images are not uploaded from appliance wizard. Ref https://github.com/GNS3/gns3-gui/issues/2738 2019-03-18 15:33:37 +07:00
grossmj
8067aaadd4 Development on 2.2.0dev7 2019-03-14 23:27:11 +07:00
grossmj
4a012c4d88 Release v2.2.0a2 2019-03-14 17:09:53 +07:00
grossmj
7f234aa648 Try to handle stacked widget layout differently. Ref #2605 2019-03-13 15:46:41 +07:00
Jeremy Grossmann
dbbcdf0f73 Merge pull request #2735 from GNS3/pyup-update-pytest-4.3.0-to-4.3.1
Update pytest to 4.3.1
2019-03-13 11:43:19 +07:00
pyup-bot
a6c56a0963 Update pytest from 4.3.0 to 4.3.1 2019-03-13 00:37:53 +01:00
Jeremy Grossmann
466d427295 Merge pull request #2732 from GNS3/symbol-management-refactoring
Symbol management refactoring
2019-03-12 18:21:23 +07:00
grossmj
e5b8bdc106 Early support for symbol themes. 2019-03-12 18:13:33 +07:00
grossmj
25a6b6b3b1 Download custom appliance symbols from GitHub
Fix symbol cache issue. Ref https://github.com/GNS3/gns3-gui/issues/2671
Fix temporary directory for symbols was not deleted
Fix temporary appliance file was not deleted
2019-03-11 16:55:16 +07:00
Jeremy Grossmann
39723a2212 Merge pull request #2730 from GNS3/lock-unlock-items
Display available appliances in a hierarchical folder structure. Fixes #2702
2019-03-08 18:03:59 +07:00
grossmj
9cd0597879 Change the size of template list on the Preferences window to relative. Fixes #2605 2019-03-08 11:46:43 +07:00
grossmj
c2472bcb22 New export project wizard. 2019-03-07 17:38:27 +07:00
grossmj
b9caf7216a Update paths for binaries moved to the MacOS directory in GNS3.app 2019-03-04 16:07:04 +07:00
grossmj
6b23de94b0 Bump version to 2.2.0dev2 2019-03-04 14:48:57 +07:00
grossmj
ab1324ffba Prevent to change layer position for locked items. Ref #2679 2019-03-02 18:49:19 +07:00
grossmj
21bcfde8f3 Display available appliances in a hierarchical folder structure. Fixes #2702 2019-03-02 17:52:47 +07:00
Jeremy Grossmann
3616bd6c85 Merge pull request #2725 from GNS3/lock-unlock-items
Refactoring for locking/unlocking items
2019-03-02 16:44:22 +07:00
grossmj
740e9bab87 Handle locking/unlocking items independently from the layer position. 2019-03-02 16:26:41 +07:00
grossmj
198cf833e9 Stay with jsonschema 2.6.0 2019-03-01 17:26:23 +07:00
grossmj
21f5a64b07 Merge 2.1 into 2.2 2019-03-01 17:23:49 +07:00
grossmj
fc3781550a Development on 2.1.15dev1 2019-02-27 15:59:16 +07:00
Jeremy Grossmann
a9a2a541c0 Update gns3-feature-request.md 2019-02-27 15:46:57 +07:00
Jeremy Grossmann
8998c07e0e Update gns3-bug-report.md 2019-02-27 15:46:28 +07:00
Jeremy Grossmann
ba01a89af1 Update gns3-feature-request.md 2019-02-27 15:43:07 +07:00
Jeremy Grossmann
eae07d62ad Update gns3-bug-report.md 2019-02-27 15:42:21 +07:00
Jeremy Grossmann
23903cf0c9 Delete feature_request.md 2019-02-27 15:36:33 +07:00
Jeremy Grossmann
4d908fd855 Delete bug_report.md 2019-02-27 15:36:18 +07:00
Jeremy Grossmann
bb0e67be4f Update issue templates 2019-02-27 15:32:18 +07:00
grossmj
d285e62c04 Release v2.1.14 2019-02-27 14:58:52 +07:00
grossmj
44d70de687 Better description to why an appliance cannot be installed. 2019-02-27 14:51:12 +07:00
grossmj
752c516f82 Development on 2.1.14dev1 2019-02-26 18:09:56 +07:00
grossmj
e1ec6c5771 Merge remote-tracking branch 'origin/2.1' into 2.1 2019-02-26 16:43:27 +07:00
grossmj
e8308869d9 Release v2.1.13 2019-02-26 16:43:14 +07:00
Jeremy Grossmann
484c5abe9d Force jsonschema dependency to 2.6.0
This is to fix this issue when starting the (frozen) application on Windows:

```
  File "C:\Python36-x64\lib\site-packages\jsonschema\validators.py", line 505, in <module>
  File "C:\Python36-x64\lib\site-packages\jsonschema\_utils.py", line 57, in load_schema
  File "C:\Python36-x64\lib\pkgutil.py", line 634, in get_data
OSError: [Errno 34] Result too large: 'jsonschema\\schemas\\draft6.json'
```
2019-02-26 15:28:16 +07:00
grossmj
c85112978d Fix broken idle-pc support. Fixes #1515 2019-02-22 22:22:31 +07:00
grossmj
e57f6db9f0 Merge remote-tracking branch 'origin/2.2' into 2.2 2019-02-22 17:37:24 +07:00
grossmj
edee26c77c Merging 2.1 into 2.2 2019-02-22 17:36:59 +07:00
grossmj
fe222b873f Disable computer hibernation detection mechanism. Ref #2678 2019-02-22 17:04:12 +07:00
Jeremy Grossmann
1acf44de21 Merge pull request #2719 from GNS3/drop-old-qemu
Drop old Qemu support (Windows/macOS) and ASA legacy image support. Ref #1516
2019-02-20 17:48:29 +07:00
grossmj
f8bb6661dd Add some advice for request timeout message. Fixes #2652 2019-02-20 00:14:15 +07:00
grossmj
ac50dffabd Fix appliance to template tests. 2019-02-19 23:29:20 +07:00
grossmj
fbb28a4325 Remove -nographic Qemu option when importing appliance. 2019-02-19 23:20:21 +07:00
grossmj
3e47267e35 Drop old Qemu support (Windows and macOS) and legacy ASA support. 2019-02-19 23:14:19 +07:00
grossmj
0f9aab9230 Merge remote-tracking branch 'origin/2.1' into 2.1 2019-02-19 16:07:51 +07:00
grossmj
a5cf5e16b7 Show/Hide interface labels when status points are not shown. Fixes #2690 2019-02-19 16:07:39 +07:00
Jeremy Grossmann
7f8269bb44 Merge pull request #2717 from GNS3/pyup-update-pytest-4.1.1-to-4.3.0
Update pytest to 4.3.0
2019-02-19 15:56:28 +07:00
Jeremy Grossmann
097458d108 Merge pull request #2715 from GNS3/critical-messages-before-running
Show critical messages before the main window runs. Fixes #2710
2019-02-19 15:19:53 +07:00
pyup-bot
2e0ce6afe0 Update pytest from 4.1.1 to 4.3.0 2019-02-19 04:26:58 +01:00
grossmj
f4cafac9c7 Do not print critical message twice on stderr.
Replace QMessageBox calls with no parent by log.error()/log.warning().
2019-02-18 22:09:23 +08:00
grossmj
7f132fdc36 Show critical messages before the main window runs. 2019-02-18 11:22:13 +08:00
grossmj
6b7d629755 Avoid using PyQt5.Qt, which imports unneeded stuff. Fixes #2592 2019-02-16 15:05:42 +08:00
grossmj
b7ccc37ea5 Fix SIP import error with recent PyQt versions. Fixes #2709 2019-02-16 14:38:44 +08:00
Jeremy Grossmann
bbe2826c77 Upgrade to Qt 5.12. Fixes #2636 2019-02-12 11:55:09 +08:00
grossmj
dda0447839 Release v2.2.0a1 2019-01-29 14:39:42 +08:00
grossmj
9398dd0840 Resize some more dialogs. 2019-01-29 11:44:15 +08:00
grossmj
6e3c5c1bb8 Resize some dialogs. 2019-01-28 18:01:52 +08:00
grossmj
a83208178b Fix default NAT interface not restored on Windows. Fixes #2681 2019-01-28 15:39:35 +08:00
grossmj
f1b7c0e176 Fix tests. 2019-01-28 15:24:40 +08:00
grossmj
429c2ab650 Merge and improvements to the setup wizard. Fixes #2676. 2019-01-28 15:13:37 +08:00
grossmj
68e2a0ee39 Adjust the setup wizard (VMware image size, layouts). 2019-01-27 22:44:56 +08:00
Jeremy Grossmann
b27c024449 Merge pull request #2684 from GNS3/refactor-appliance-wizard
Additional improvements for the appliance wizard. Fixes #2224
2019-01-25 15:27:31 +08:00
grossmj
7bbd337801 Refactor appliance wizard. 2019-01-25 14:31:37 +08:00
grossmj
7c545e3860 Natural sorting support for custom adapters tree widget. 2019-01-24 16:14:20 +08:00
grossmj
5b6491a23f Add the word "template" to configuration dialog titles. 2019-01-24 15:56:50 +08:00
grossmj
12e4f8445d Reorder node contextual menu. 2019-01-24 14:57:01 +08:00
grossmj
52418ed94a Development on 2.1.13dev1 2019-01-23 15:25:48 +08:00
grossmj
a1496bffd4 Release 2.1.12 2019-01-23 15:22:26 +08:00
Jeremy Grossmann
911f6305fa Merge pull request #2677 from GNS3/svg-symbols-fixes
Resize SVG node symbols that are too big. Fixes #2674
2019-01-22 22:25:13 +07:00
grossmj
c6594d4845 Checkbox to activate/deactivate the size limit of node symbols. 2019-01-22 22:16:45 +07:00
grossmj
538adc4817 Option to limit the size of node symbols (activated by default). Ref #2674.
(cherry picked from commit 6b38b58633)
2019-01-22 00:28:06 +07:00
grossmj
961c5652ea Resize SVG node symbol only when height is above 80px. Ref #2674
Work on str instead of binary when resizing SVG symbol.

(cherry picked from commit 938e9129cd)
2019-01-22 00:24:30 +07:00
grossmj
c0ecf3ccc4 Automatically resize SVG symbols that are too big. Ref #2674.
(cherry picked from commit 8f381a4720)
2019-01-22 00:24:11 +07:00
grossmj
33bc644688 Resize some dialogs. 2019-01-21 18:19:04 +07:00
grossmj
6b38b58633 Option to limit the size of node symbols (activated by default). Ref #2674. 2019-01-21 18:11:26 +07:00
grossmj
938e9129cd Resize SVG node symbol only when height is above 80px. Ref #2674
Work on str instead of binary when resizing SVG symbol.
2019-01-21 17:22:03 +07:00
grossmj
8f381a4720 Automatically resize SVG symbols that are too big. Ref #2674. 2019-01-21 16:41:46 +07:00
grossmj
c1fd434f75 Bigger new template wizard. 2019-01-17 18:37:07 +07:00
grossmj
2440faf3f5 Fix DeprecationWarning: invalid escape sequence. Fixes https://github.com/GNS3/gns3-gui/issues/2670 2019-01-17 18:01:58 +07:00
grossmj
0bd480eabb Use theme icons in other contextual menus. Fixes #2669 2019-01-17 17:34:30 +07:00
grossmj
dc3d762799 Resize new template dialog. Ref #2671 2019-01-16 19:31:03 +07:00
Jeremy Grossmann
1d139cee4d Merge pull request #2668 from GNS3/new_appliance_installation
Template creation from appliance. Fixes #2642
2019-01-14 17:25:17 +07:00
grossmj
d033268cd9 Change some text regarding appliance installation. 2019-01-14 17:19:37 +07:00
grossmj
82fdeb3a49 Handle errors when creating template from appliance. 2019-01-14 16:45:58 +07:00
grossmj
3462109ef2 Template creation from an appliance. 2019-01-14 16:14:31 +07:00
grossmj
b6e5a588bf Basic support to create new template from appliance. 2019-01-13 15:23:14 +07:00
Jeremy Grossmann
4e2a80e379 Merge pull request #2667 from GNS3/pyup-update-pytest-4.0.2-to-4.1.1
Update pytest to 4.1.1
2019-01-13 12:54:08 +07:00
pyup-bot
fc61079132 Update pytest from 4.0.2 to 4.1.1 2019-01-12 22:56:48 +01:00
grossmj
f48f4eacd2 Fix race condition when trying to automatically open a console and the project is already running. Fixes #1493. 2019-01-12 16:08:21 +07:00
grossmj
92e2920212 Fix issue with IOS c7200 templates and usage variable. 2019-01-12 12:30:20 +07:00
grossmj
ebb2d8bb73 Add usage instructions to node tooltip. Ref #2662. 2019-01-10 15:02:41 +08:00
grossmj
ade748c3b1 Smaller node info dialog. 2018-12-30 21:51:51 +07:00
grossmj
ec334508a6 New node information dialog to display general, usage and command line information.
Ref https://github.com/GNS3/gns3-gui/issues/2662 https://github.com/GNS3/gns3-gui/issues/2656
2018-12-30 19:35:25 +07:00
grossmj
ed88eaa620 Support "usage" field for Dynamips, IOU, VirtualBox and VMware. Fixes https://github.com/GNS3/gns3-gui/issues/2657 2018-12-21 16:54:13 +08:00
grossmj
524f911293 Add "new template" entry to File menu. Fixes #2658 2018-12-21 16:07:06 +08:00
grossmj
fa9fc0ff8d Merge 2.1 into 2.2 branch. 2018-12-21 15:24:34 +08:00
grossmj
cf73db25b4 Update VMware banners and links. 2018-12-21 13:18:29 +08:00
grossmj
dd79939140 Allow users to refresh the template list in the nodes view panel. 2018-12-18 17:52:50 -06:00
grossmj
6d0afd39d7 Merge remote-tracking branch 'origin/2.2' into 2.2 2018-12-14 21:00:15 -06:00
grossmj
1d7a6611bd Fix bug with filter in add template. Fixes #2651. 2018-12-14 21:00:02 -06:00
Jeremy Grossmann
15aa4c6001 Merge pull request #2654 from GNS3/pyup-update-pytest-4.0.1-to-4.0.2
Update pytest to 4.0.2
2018-12-14 11:30:25 -06:00
pyup-bot
6f42208323 Update pytest from 4.0.1 to 4.0.2 2018-12-14 18:20:51 +01:00
grossmj
45b3c17c97 Remove typo. Ref #2648. 2018-12-13 22:37:00 -06:00
grossmj
1ddf3e6388 Fix Dynamips decompress doesn't work with relative images. Fixes #2648. 2018-12-13 22:32:06 -06:00
grossmj
d4f0f76e57 Merge branch '2.1' into 2.2 2018-11-30 12:37:25 +08:00
grossmj
e0c06ecd78 Fix missing method '_newApplianceActionSlot'. Fixes #2643. 2018-11-29 15:00:11 +07:00
Jeremy Grossmann
f839bbf877 Merge pull request #2641 from GNS3/naming-consistency
Review and apply consistent naming
2018-11-28 16:32:34 +07:00
grossmj
52c06f4730 Use "template" to name what we use to create new nodes. 2018-11-28 16:12:58 +07:00
grossmj
a8593bb39e Use project instead of topology where appropriate. 2018-11-27 18:30:16 +07:00
grossmj
396e871b3b Make sure nothing is named "compute server". 2018-11-27 18:14:51 +07:00
Jeremy Grossmann
ba9298735a Merge pull request #2640 from GNS3/kazkansouh-2.2-grid-new-project
Improved grid support
2018-11-27 16:34:03 +07:00
grossmj
f4bae20592 Use "node" instead of "appliance" for grid support. 2018-11-27 16:13:22 +07:00
Karim Kanso
189852862e Support for differing grid sizes for appliances and drawings. Requires corresponding commit on gns3-server. 2018-11-26 15:59:34 +00:00
Karim Kanso
ba59d69536 New projects can be created with show grid/snap to grid. 2018-11-26 15:13:39 +00:00
Jeremy Grossmann
26de22f105 Merge pull request #2597 from GNS3/pyup-update-pytest-pythonpath-0.7.2-to-0.7.3
Update pytest-pythonpath to 0.7.3
2018-11-24 16:11:43 +07:00
Jeremy Grossmann
d47b5040cf Merge branch '2.2' into pyup-update-pytest-pythonpath-0.7.2-to-0.7.3 2018-11-24 16:11:33 +07:00
Jeremy Grossmann
dcf133b297 Merge pull request #2634 from GNS3/pyup-update-pytest-4.0.0-to-4.0.1
Update pytest to 4.0.1
2018-11-24 15:24:49 +07:00
pyup-bot
b50fe81d86 Update pytest from 4.0.0 to 4.0.1 2018-11-24 09:20:07 +01:00
Jeremy Grossmann
1c8e166393 Update download URL for "Check For Update". 2018-11-23 16:28:59 +07:00
grossmj
6afdb18bdb Disallow changing layer of a locked object. Ref #2513. 2018-11-20 14:55:30 +07:00
grossmj
a454283357 Bump version to 2.2.0dev5 2018-11-20 14:37:38 +07:00
Jeremy Grossmann
9369ad9645 Merge pull request #2625 from GNS3/pyup-update-pytest-3.8.1-to-4.0.0
Update pytest to 4.0.0
2018-11-19 17:46:28 +07:00
Jeremy Grossmann
be997d4d25 Merge pull request #2627 from GNS3/pyup-update-pytest-timeout-1.3.1-to-1.3.3
Update pytest-timeout to 1.3.3
2018-11-19 17:46:09 +07:00
grossmj
d1b9185764 Cosmetic changes regarding appliances. 2018-11-19 14:37:33 +07:00
grossmj
2b98e48420 Fix issue when duplicating an appliance on GUI side. 2018-11-19 01:21:03 +07:00
grossmj
ec21134920 Fix issue to access configuration pages for Ethernet switch and hub appliances. 2018-11-18 00:00:17 +07:00
grossmj
9b73d652d3 Fix small bugs when using the new appliance management API. 2018-11-17 22:16:18 +07:00
grossmj
3c67b70ff3 Fix bug with custom adapters and categories for Docker VM. Fixes https://github.com/GNS3/gns3-gui/issues/2613 2018-11-17 21:50:00 +07:00
grossmj
60f58064b3 Fix bug with categories with Docker appliances. 2018-11-17 20:10:22 +07:00
pyup-bot
ca305cefa4 Update pytest-timeout from 1.3.1 to 1.3.3 2018-11-16 13:10:50 +01:00
Jeremy Grossmann
8ce928aec2 Merge pull request #2626 from GNS3/appliance-api
New appliance management API. Fixes #1427
2018-11-15 22:52:28 +07:00
grossmj
9ff8816273 Schema validation for appliance API. Ref #1427. 2018-11-15 17:28:17 +07:00
pyup-bot
42d5d4b542 Update pytest from 3.8.1 to 4.0.0 2018-11-15 00:56:49 +01:00
grossmj
2cf64a99de Remove generic controller settings API endpoint. 2018-11-14 16:24:30 +08:00
grossmj
7e942a7753 Fix tests. 2018-11-13 15:40:18 +08:00
grossmj
23d467f688 Working dedicated appliance management API. Ref https://github.com/GNS3/gns3-server/issues/1427 2018-11-13 14:59:18 +08:00
grossmj
fede614716 Base for dedicated appliance management API. Ref https://github.com/GNS3/gns3-server/issues/1427 2018-11-11 20:13:58 +08:00
grossmj
c31b5dd7a9 Bump version to 2.2.0dev4 2018-10-15 17:06:10 +07:00
grossmj
3ba811e675 Fix tests. 2018-10-15 14:33:23 +07:00
grossmj
f6a738fe3e Fix conflict between the two websocket streams (project & controller). 2018-10-15 14:19:46 +07:00
grossmj
4b577e96dd Fix platform.linux_distribution() is deprecated. Fixes https://github.com/GNS3/gns3-gui/issues/2578 2018-10-04 16:32:50 +02:00
grossmj
c6ed354629 Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/appliance_manager.py
#	gns3/dialogs/appliance_wizard.py
#	gns3/nodes_view.py
#	gns3/version.py
2018-10-04 15:50:00 +02:00
grossmj
ec324f9b01 Development on 2.1.12dev1 2018-09-28 20:44:52 +02:00
grossmj
cbfd59498e Release v2.1.11 2018-09-28 20:40:58 +02:00
grossmj
0216bc8b4d Handle deleted SIP objects. 2018-09-28 15:01:22 +02:00
grossmj
a8477597ab Update paths for UltraVNC and VirtViewer. 2018-09-27 22:22:36 +02:00
pyup-bot
4c7965d70f Update pytest-pythonpath from 0.7.2 to 0.7.3 2018-09-27 21:46:54 +02:00
Jeremy Grossmann
28acb2911f Merge pull request #2594 from GNS3/pyup-update-pytest-3.7.0-to-3.8.1
Update pytest to 3.8.1
2018-09-27 21:45:48 +02:00
grossmj
e240dbad6b Indicate if Solar-PuTTY is included or not. Fixes #2595 2018-09-27 21:08:51 +02:00
pyup-bot
fac27d9df9 Update pytest from 3.7.0 to 3.8.1 2018-09-23 03:52:10 +02:00
grossmj
8d183a3283 Fix bad link to installation instructions in README.rst. Fixes #2590 2018-09-20 14:58:41 +02:00
grossmj
3af5046d0f Downgrade to Qt 5.9. Fixes #2592. 2018-09-20 14:30:57 +02:00
grossmj
c8397a1ef7 Development on 2.1.11dev1 2018-09-15 11:13:39 +02:00
grossmj
b419891950 Release v2.1.10 2018-09-15 11:11:24 +02:00
grossmj
2c1ba697bd Fix small errors like unhandled exceptions etc. 2018-09-11 15:06:01 +02:00
grossmj
3000a9aa7f Fix when appliance version is not available for Dynamips/IOU/Qemu. Fixes #2585. 2018-09-05 15:32:38 +08:00
grossmj
2f8541c543 Fix issue when installing appliance with no version selected. Fixes #2585. 2018-09-05 14:53:47 +08:00
grossmj
5c3d4b2ab6 Check for existing appliance name across all emulator types. Fixes #2584. 2018-09-05 14:08:05 +08:00
grossmj
f44ac8cba5 Improve the invalid port format detection. Fixes https://github.com/GNS3/gns3-gui/issues/2580 2018-09-05 13:35:42 +08:00
grossmj
7ef49fbca7 Catch OSError/PermissionError when checking md5 on remote image. Fixes #2582. 2018-09-05 13:24:01 +08:00
grossmj
5ccf5778a2 Fix UnicodeDecodeError in file editor. Fixes #2581. 2018-09-04 21:52:35 +08:00
grossmj
6030d5e019 Catch import error for win32serviceutil. Fixes #2583. 2018-09-04 21:18:03 +08:00
grossmj
6de8880937 Fix bug with empty project ID when creating a new node. Fixes #2366
..
2018-09-04 20:51:30 +08:00
grossmj
08c89c4fac Fix various small errors, mostly about non-existing C/C++ objects. 2018-09-03 16:48:23 +07:00
grossmj
e411d497c4 Send extra controller and compute information in crash reports. 2018-09-02 21:47:33 +07:00
grossmj
e037835769 Update setup.py and fix minor issues. 2018-09-02 15:32:34 +07:00
grossmj
a5f4ec0135 Set the default delay console all value to 1500ms if using Solar-PuTTY. 2018-08-29 21:15:40 +07:00
grossmj
3ee68b22bd Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/compute.py
#	gns3/nodes_view.py
#	gns3/version.py
2018-08-29 15:35:31 +07:00
grossmj
154f10a686 Make Solar-Putty the default if installed. Ref #2519. 2018-08-27 16:24:38 +07:00
grossmj
e5320c318f Fix tests. 2018-08-22 20:37:55 +07:00
grossmj
07ea6207c1 Fix issue with custom appliance. Fixes https://github.com/GNS3/gns3-registry/issues/361 2018-08-22 20:18:35 +07:00
grossmj
12398881f8 Forbid controller and compute servers to be different versions.
Report last compute server error to clients and display in the server summary.
2018-08-22 16:54:43 +07:00
grossmj
27a8e3c7f8 Fix issue with appliance categories. Fixes https://github.com/GNS3/gns3-registry/issues/361 2018-08-22 15:52:32 +07:00
grossmj
d92ff1abe3 Add compute information to crash reports. 2018-08-21 20:40:01 +07:00
grossmj
e97b3b6a42 Add controller version in Sentry bug reports. 2018-08-21 19:16:49 +07:00
grossmj
5ee3f73213 Backport: Fix "Network session error" issues. Fixes #2560. 2018-08-21 18:29:57 +07:00
grossmj
a30aa2f5f1 Add SolarPutty command line. Fixes #2519. 2018-08-21 18:16:51 +07:00
grossmj
98bb6590aa Add missing Qemu boot priority values. Fixes https://github.com/GNS3/gns3-server/issues/1385 2018-08-21 17:49:58 +07:00
grossmj
4250e961a3 Merge remote-tracking branch 'origin/2.1' into 2.1 2018-08-21 17:32:24 +07:00
grossmj
3c46a3a72d Update PyQt5 from version 5.8 to version 5.10. Fixes #2564. 2018-08-21 17:32:09 +07:00
grossmj
c82d262975 Allow multiple appliances to be installed. Ref #2490 2018-08-21 17:06:03 +07:00
grossmj
aa84d100b1 Add more information about appliance templates. 2018-08-20 16:50:47 +07:00
grossmj
e51477d989 New appliance wizard to install an appliance from different sources. Ref #2490 2018-08-19 16:51:48 +07:00
grossmj
e4a29f30e3 Fix tests. 2018-08-16 22:01:24 +07:00
grossmj
3d8bd16536 Redesign appliance handling part 1. Ref #2490
- Removed appliance templates from device dock
 - Use new controller notification stream
 - Fixed device update and remove from device dock
2018-08-16 21:47:52 +07:00
ziajka
c55442a517 Development on 2.1.10dev1 2018-08-13 13:50:31 +02:00
ziajka
45e0080726 Release v2.1.9 2018-08-13 13:14:20 +02:00
grossmj
bb013804d4 Merge branch '2.1' into 2.2 2018-08-13 16:10:06 +07:00
grossmj
95558ec2e6 Fix incorrect short port names in topology summary. Fixes https://github.com/GNS3/gns3-gui/issues/2562 2018-08-13 15:10:21 +07:00
grossmj
f1cd31baa6 Fix "Network session error" issues. Fixes #2560. 2018-08-12 21:06:00 +07:00
grossmj
cf7176559d Set default layer for newly created nodes to 1 and 2 for all other drawings. Ref #2513. 2018-08-08 14:34:58 +07:00
grossmj
2504085db2 Deactivate TraceNG module 2018-08-08 14:02:23 +07:00
grossmj
75d3b61783 Merge remote-tracking branch 'origin/2.2' into 2.2 2018-08-07 21:02:40 +07:00
grossmj
5da8e77d01 Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/dialogs/appliance_wizard.py
#	gns3/node.py
#	gns3/version.py
2018-08-07 21:02:10 +07:00
grossmj
5b56d54030 Add compute version in server summary tooltip. 2018-08-07 15:32:16 +07:00
Jeremy Grossmann
3de38d2ccb Merge pull request #2552 from GNS3/pyup-update-pytest-3.6.1-to-3.7.0
Update pytest to 3.7.0
2018-07-31 21:10:18 -05:00
pyup-bot
76553ff102 Update pytest from 3.6.1 to 3.7.0 2018-07-31 08:03:53 +02:00
Jeremy Grossmann
67b2d145da Merge pull request #2543 from GNS3/pyup-update-pytest-timeout-1.2.1-to-1.3.1
Update pytest-timeout to 1.3.1
2018-07-30 11:30:58 -05:00
Jeremy Grossmann
d5ee1ea5d2 Merge pull request #2538 from ehlers/osx_telnet_utf8_path
Support PATH with UTF-8 characters in OSX telnet console, fixes #2537
2018-07-30 10:13:52 -05:00
grossmj
69b8c07c0a Fix test for Qemu boot priority. Fixes #2548. 2018-07-30 09:48:38 -05:00
grossmj
dbe73eb8d7 Fix boot priority missing when installing an appliance. Fixes #2548. 2018-07-30 09:29:51 -05:00
pyup-bot
ba0559bf08 Update pytest-timeout from 1.2.1 to 1.3.1 2018-07-23 22:51:41 +02:00
Bernhard Ehlers
706f89debb Support PATH with UTF-8 characters in OSX telnet console, fixes #2537 2018-07-14 12:38:53 +02:00
grossmj
ec0be9e22b Allow users to accept different MD5 hashes for preconfigured appliances. Fixes #2526. 2018-07-10 16:02:44 +08:00
grossmj
0e6fa597ec Do not try to update drawing if it is being deleted. Ref #2483. 2018-07-10 15:39:35 +08:00
grossmj
f81450c65a Merge remote-tracking branch 'origin/2.1' into 2.1 2018-07-10 11:54:26 +08:00
grossmj
38cbe70aaa Catch exception when loading invalid appliance file. 2018-07-10 11:54:11 +08:00
ziajka
9fd07b6379 Merge pull request #2528 from GNS3/local-web-ui
Main menu actions to WebUI and Light Web Interface
2018-06-26 12:05:17 +02:00
ziajka
c84b262303 Main menu actions to WebUI and Light Web Interface 2018-06-26 12:04:02 +02:00
Jeremy Grossmann
0150515338 Enable TraceNG module 2018-06-18 23:54:13 +07:00
ziajka
47d335f4c9 Release v2.1.8 2018-06-14 15:16:54 +02:00
grossmj
ffc08361ce Add Solar-Putty command line. Ref #2519. 2018-06-14 17:04:44 +08:00
grossmj
ab90f5f458 Fix merging issue from 2.1 to 2.2 for DockerVMConfigurationPage. Fixes #2516. 2018-06-14 12:03:07 +08:00
grossmj
a0d6a43b51 Fix issues when locking/unlocking items. Ref #2513. 2018-06-13 17:31:27 +08:00
grossmj
20d4f73f56 Add error information when cannot access/read IOS/IOU config file. Ref #2501 2018-06-13 16:27:43 +08:00
grossmj
5204184029 Fallback when using process name to bring console to front. 2018-06-12 17:55:09 +08:00
grossmj
9915beeb8e Use process name to bring console to front. Fixes #2514. 2018-06-12 17:45:54 +08:00
ziajka
1ea383fce2 Development on v2.1.8dev1 2018-06-12 11:15:26 +02:00
ziajka
2744e669b4 Release v2.1.7 2018-06-12 11:12:49 +02:00
grossmj
8fd9ec5319 Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/node.py
2018-06-12 15:22:46 +08:00
grossmj
a5f3164feb Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/node.py
2018-06-10 21:24:57 +07:00
grossmj
3d949df14c Fix tests for default note font/color. 2018-06-10 17:53:00 +07:00
grossmj
6a5764fda9 Console support for clouds (to connect to external devices or services). Fixes #2500. 2018-06-10 17:45:34 +07:00
grossmj
f312d57165 Fix LabelItem tests. 2018-06-09 19:19:16 +07:00
grossmj
973793e6b6 Separate appliance font from note font. Fixes #2477. 2018-06-09 19:13:36 +07:00
grossmj
2a7ce661da Do not include spaces in link description (%d replacement) for packet analyzer command. Ref #2485. 2018-06-09 18:25:09 +07:00
grossmj
a85f99185a Fix error when trying to open project. Fixes #2508 2018-06-09 18:20:03 +07:00
grossmj
d511d0f5f8 Launch packet capture analyzer command without creating pipe. 2018-06-09 18:08:42 +07:00
Jeremy Grossmann
b92a589762 Merge pull request #2510 from GNS3/pyup-update-pytest-3.4.2-to-3.6.1
Update pytest to 3.6.1
2018-06-07 21:56:13 +07:00
grossmj
6ab2d63bdc Do not try to update link if it is being deleted. Fixes #2483. 2018-06-06 21:00:08 +07:00
grossmj
0de6bfe7e1 Fix can't add SVG image to project. Fixes #2502 2018-06-06 18:26:37 +07:00
pyup-bot
b024eb63e9 Update pytest from 3.4.2 to 3.6.1 2018-06-05 19:44:09 +02:00
grossmj
f144103bca Remove unwanted trailing characters and other white spaces when reading .md5sum files. Fixes #2498. 2018-06-04 23:59:53 +07:00
grossmj
7c1af696b9 Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/modules/docker/pages/docker_vm_preferences_page.py
#	gns3/modules/docker/ui/docker_vm_configuration_page.ui
#	gns3/modules/docker/ui/docker_vm_configuration_page_ui.py
#	gns3/modules/qemu/pages/qemu_vm_configuration_page.py
#	gns3/modules/virtualbox/pages/virtualbox_vm_configuration_page.py
#	gns3/modules/vmware/pages/vmware_vm_configuration_page.py
#	gns3/version.py
2018-06-04 22:45:24 +07:00
grossmj
c0b26aff48 Update interface sequence number check. Fixes #2491. 2018-06-04 22:31:19 +07:00
ziajka
9601e4e6f2 Logo should not have context menu, Fixes: #2507 2018-05-24 13:21:13 +02:00
ziajka
88708c2a8d Update logo position only when changes, Fixes: #2506 2018-05-24 13:15:32 +02:00
ziajka
8eff12194d Development on v2.1.7dev1 2018-05-22 14:14:46 +02:00
ziajka
b0520b2bd4 Release v2.1.6 2018-05-22 14:11:59 +02:00
ziajka
17d2c023bf Fix redraw logo on Windows 2018-05-22 13:16:48 +02:00
ziajka
ce9fdea0a0 Merge pull request #2492 from GNS3/extra-hosts
Extra hosts for Docker, global variables for project and supplier logo support, Fixes: #2482
2018-05-15 09:23:42 +02:00
ziajka
24d7dacb4e Variables fix on ProjectWelcomeDialog 2018-05-10 10:46:57 +02:00
ziajka
bb36765407 Remove project_created_signal 2018-05-09 15:24:41 +02:00
ziajka
250db92ce0 Ask for global variables when project is loaded 2018-05-09 11:54:13 +02:00
ziajka
d59ec39505 Add/Edit global variables of project 2018-05-08 18:31:26 +02:00
ziajka
5e9ae04dc1 Rename tabs at Edit Project 2018-05-08 17:05:25 +02:00
ziajka
ddb0fccda3 Global variables tab on Edit project 2018-05-08 17:03:04 +02:00
ziajka
9b22a52f14 Support of supplier logo and url 2018-05-08 16:22:01 +02:00
grossmj
948878bfdd Add missing crowdfunder name in About dialog. 2018-05-08 21:52:37 +08:00
ziajka
7340abbaa9 Project variables and supplier 2018-05-08 13:00:32 +02:00
grossmj
1c1ea50adc Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/node.py
#	gns3/version.py
2018-04-28 19:44:49 +07:00
grossmj
4ea0528bf2 No timeout when duplicating a project. 2018-04-28 17:09:08 +07:00
grossmj
49005e6add No timeout when restoring snapshot. 2018-04-28 16:41:54 +07:00
ziajka
5484c039b5 Fix tests 2018-04-27 14:47:09 +02:00
ziajka
daaf71b6d2 Add advanced settings for docker and param, Ref. #2482 2018-04-27 14:28:14 +02:00
grossmj
450f0e006b Merge remote-tracking branch 'origin/2.1' into 2.1 2018-04-23 15:40:18 +07:00
grossmj
a6a967fbde Replace "not supported" by "none" in topology summary view. 2018-04-23 15:39:58 +07:00
ziajka
1a6293709e Development on v2.1.6 2018-04-18 11:41:43 +02:00
ziajka
2ed53225e0 Release v2.1.5 2018-04-18 11:28:52 +02:00
grossmj
b8798fbda5 Disable TraceNG for version 2.1.5 2018-04-18 17:19:44 +08:00
grossmj
c2ac68be49 Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/modules/traceng/traceng_node.py
2018-04-18 17:10:50 +08:00
grossmj
368de32faa Fix Qemu binary list locks when a version is deleted. Fixes #2474. 2018-04-18 15:44:33 +08:00
grossmj
98d01cbfa0 Fix invalid answer from the PyPi server. Fixes #2473. 2018-04-18 15:10:31 +08:00
grossmj
ad62bb7832 Fix wrong wizard page name. 2018-04-16 17:16:20 +08:00
grossmj
637061663a Add default destination setting for traceng + some checks. Ref #2450. 2018-04-16 15:03:02 +08:00
grossmj
c137198985 Grid size support for projects. Fixes #2469. 2018-04-13 16:56:37 +08:00
grossmj
946efb61de Remove 'include INSTALL' from MANIFEST. Fixes #2470. 2018-04-13 14:17:03 +08:00
grossmj
cb74a8e12f Cleanup more code. 2018-04-11 16:56:18 +07:00
grossmj
c42fecaea3 Cleanup node code. 2018-04-10 20:48:00 +07:00
grossmj
088b020ac0 More spring cleaning. 2018-04-09 16:50:33 +07:00
grossmj
af507e7668 Some spring cleanup. 2018-04-08 16:44:12 +07:00
grossmj
204ff1f8fd Streamline appliance wizard. Fixes #2224. 2018-04-08 16:42:07 +07:00
grossmj
8c7e8e412a Fix "Node list view not updated when renaming or deleting appliance template". Fixes #2356. 2018-04-06 16:57:07 +07:00
grossmj
030169dc10 Automatically resize the Custom adapters configuration dialog. Fixes #2467. 2018-04-06 16:24:50 +07:00
grossmj
e877adca35 Change size of custom adapters configuration dialog. Ref #2467. 2018-04-06 14:42:30 +07:00
grossmj
18dc8fab14 Fix tests. 2018-04-06 14:35:39 +07:00
grossmj
60018612b1 Improve node tooltips. Fixes #2462. 2018-04-06 13:25:23 +07:00
Jeremy Grossmann
0410c446fc Merge pull request #2466 from dhalperi/fix-typo
Fix a minor typo in the setup wizard
2018-04-05 13:19:38 +07:00
Daniel Halperin
18486e4772 Fix a minor typo in the setup wizard
Eveything -> Everything
2018-04-04 22:53:09 -07:00
grossmj
3c9787effb Do not activate "console auto start" by default. Ref #1910. 2018-04-05 11:08:40 +07:00
grossmj
664da8ee3d Remove unused code. 2018-04-04 21:52:29 +07:00
grossmj
b4da9b7bae Support for console auto start. Fixes #1910 2018-04-04 21:32:08 +07:00
grossmj
5ef612815b Bump version to 2.2.0dev2 2018-04-03 12:51:09 +07:00
grossmj
06d7ed783f Add custom_adapters setting support for appliance files. Ref #2361. 2018-04-02 23:13:45 +07:00
grossmj
cc5b55a7ce Fix tests. 2018-04-02 22:59:47 +07:00
grossmj
c8e1602a26 Possibility to customize port names and adapter types for Qemu, VirtualBox, VMware and Docker. Fixes #2361.
MAC addresses can customized for Qemu as well.
2018-04-02 22:27:12 +07:00
grossmj
43688cb9bd Allow to have the projects with the same name in different locations. Fixes #2380. 2018-03-30 23:18:07 +07:00
grossmj
eb34715178 Save state feature for VirtualBox and VMware. New "On close" setting to
select the action to execute when closing/stopping a Qemu/VirtualBox/VMware VM.
2018-03-30 21:18:44 +07:00
grossmj
b7d78b92fc Support for suspend to disk / resume (Qemu). Ref #725. 2018-03-30 19:27:46 +07:00
grossmj
ab7930d3d9 Fix bug with 'none' console type for Ethernet switch.
Fix some tests related to traceng.
2018-03-30 13:00:52 +07:00
grossmj
c684e63be2 Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/ui/resources_rc.py
2018-03-30 12:16:31 +07:00
grossmj
4c610acfa4 Fix traceng tests. 2018-03-30 12:10:57 +07:00
grossmj
37f74824f1 Merge branch 'traceng' into 2.1 2018-03-29 15:19:29 +07:00
grossmj
5ccf8c414d Sync 2018-03-29 15:19:18 +07:00
grossmj
913f0d5e4a Check for valid IP address and prevent to run on non-Windows platforms. 2018-03-29 13:26:43 +07:00
grossmj
061bac0cc6 Support for source and destination for traceNG. 2018-03-27 16:58:49 +07:00
grossmj
20ff8a19f6 Allow to resize a Qemu VM disk (extend only). Ref #2382. 2018-03-26 18:05:20 +07:00
grossmj
53ba302515 Allow to select the default NAT interface in preferences for local server. 2018-03-26 14:23:01 +07:00
grossmj
4b43dfb77c Fix missing lock and unlock icons in resources. 2018-03-25 18:50:52 +07:00
grossmj
f8555f4008 Consistent icon styles for contextual menu. Fixes #1272. 2018-03-25 18:40:10 +07:00
grossmj
75c3092724 Spice with agent support for Qemu VMs. Fixes #2355. 2018-03-25 14:35:25 +07:00
grossmj
89e274d040 Fix zoom-in zoom-out step values. Ref #2457. 2018-03-25 13:05:50 +07:00
grossmj
f9619d79ae Support for console type "none" for all VMs. Fixes #2452. 2018-03-24 18:12:06 +07:00
grossmj
7fd9f39c36 Allow to copy Dynamips, IOU, Qemu and Docker templates in preferences. Fixes #2451. 2018-03-23 21:11:20 +07:00
grossmj
bb732bc202 Support for none console type (Qemu & Docker only) 2018-03-23 15:41:56 +07:00
grossmj
481e6c3450 Fix some issues with hardware acceleration support for Qemu. 2018-03-22 15:45:41 +07:00
grossmj
7ad663cc2a Support Qemu with HAXM acceleration. 2018-03-21 16:41:47 +07:00
ziajka
ec59cd87bd Back to development on v2.1.5dev1 2018-03-15 08:46:06 +01:00
grossmj
d4a0b21206 Some spring cleaning. 2018-03-15 14:17:40 +07:00
ziajka
05d9ee8499 Re-release v2.1.4 due to travis issue 2018-03-14 15:28:15 +01:00
grossmj
3e0242ada7 Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/version.py
2018-03-14 18:39:08 +07:00
grossmj
a72ece5c18 Custom icons and small fixes for TraceNG integration. 2018-03-14 16:56:39 +07:00
grossmj
63baa2eff0 Base support for TraceNG. 2018-03-12 17:57:13 +07:00
ziajka
b91fd4a0c2 Development on v2.1.5dev1 2018-03-12 09:25:41 +01:00
ziajka
718217e332 Release v2.1.4 2018-03-12 09:17:16 +01:00
grossmj
ba71e560f9 Merge remote-tracking branch 'origin/2.2' into 2.2 2018-03-12 13:34:31 +07:00
grossmj
1989ec3a40 Merge branch '2.1' into 2.2 2018-03-12 13:30:46 +07:00
ziajka
c202c5e4be Move connect to update settings into one place 2018-03-09 13:31:55 +01:00
ziajka
71830dd69f Merge pull request #2449 from GNS3/update-nodes
Update node on server on any change, Fixes: #2429
2018-03-09 12:55:48 +01:00
ziajka
37a7fdfa68 Update node on server on any change, Fixes: #2429 2018-03-09 12:54:29 +01:00
ziajka
cb074be0a1 Merge pull request #2446 from GNS3/pyup-update-pytest-3.4.1-to-3.4.2
Update pytest to 3.4.2
2018-03-06 09:24:50 +01:00
pyup-bot
08784158c1 Update pytest from 3.4.1 to 3.4.2 2018-03-06 04:56:24 +01:00
grossmj
0efe006cad Mark IOU layer 1 keepalive messages feature as non-functional. Fixes #2431. 2018-03-05 16:44:42 +07:00
ziajka
4a663a5910 Fix typo 2018-02-27 16:08:33 +01:00
ziajka
a559bd4ae4 Images refresh when added via settings, Fixes:#2423 2018-02-27 16:07:06 +01:00
ziajka
2539abd445 Merge pull request #2436 from GNS3/pyup-update-pytest-timeout-1.2.0-to-1.2.1
Update pytest-timeout to 1.2.1
2018-02-22 09:23:16 +01:00
ziajka
e76f1ca5cc Merge pull request #2437 from GNS3/pyqt-update
PyQt 5.10 with AV builds, Ref. #2434
2018-02-21 16:18:36 +01:00
pyup-bot
bc338b6232 Update pytest-timeout from 1.2.0 to 1.2.1 2018-02-21 16:15:38 +01:00
ziajka
ddb581623a Use PyQt 5.10 and change AV build to use MSVS2017 2018-02-21 16:11:53 +01:00
ziajka
486faf6718 Merge branch '2.2' into pyqt-update 2018-02-21 12:59:52 +01:00
ziajka
a081dcddb8 Merge branch '2.1' into 2.2 2018-02-21 12:04:56 +01:00
ziajka
c4160ec942 Test with PyQt5.9 2018-02-21 12:04:02 +01:00
ziajka
f38d9ef525 Back to PyQt5.8 2018-02-21 12:02:48 +01:00
ziajka
6639108354 Merge pull request #2435 from GNS3/pyup-update-pytest-3.1.0-to-3.4.1
Update pytest to 3.4.1
2018-02-21 10:05:37 +01:00
ziajka
a63a097341 Merge branch '2.2' into pyup-update-pytest-3.1.0-to-3.4.1 2018-02-21 10:05:27 +01:00
ziajka
94bad69198 Merge pull request #2412 from GNS3/pyup-update-pytest-pythonpath-0.7.1-to-0.7.2
Update pytest-pythonpath to 0.7.2
2018-02-21 10:04:19 +01:00
ziajka
e9057e75a0 PyQt5.10 support, Ref. #2434 2018-02-21 10:01:20 +01:00
pyup-bot
b80b86d365 Update pytest from 3.1.0 to 3.4.1 2018-02-21 03:02:35 +01:00
ziajka
5ebb3011d3 Merge pull request #2433 from GNS3/show-if-labels-on-new-project
Show labels on the new project, Fixes: #2308
2018-02-19 13:05:22 +01:00
ziajka
81300fd40e Adjust tests 2018-02-19 12:55:52 +01:00
ziajka
d4dda2a285 Emit project_loaded_signal after project creation 2018-02-19 12:54:36 +01:00
ziajka
5a4342d4b8 Add option Show interface labels on new project, Ref. #2308 2018-02-16 14:32:07 +01:00
ziajka
94fc5e6c4f Improve finding pyuic3.exe on Windows 2018-02-16 14:30:49 +01:00
grossmj
a3e81fbf2e Use debug for error downloading file messages. Fixes #2398. 2018-02-07 16:12:50 +08:00
grossmj
514eb97eac Merge remote-tracking branch 'origin/2.1' into 2.1 2018-02-06 15:38:38 +08:00
grossmj
7637039cb2 Refresh buttons in the cloud node to query the server for available interfaces. Fixes #2416. 2018-02-06 15:36:27 +08:00
pyup-bot
e46c92e92f Update pytest-pythonpath from 0.7.1 to 0.7.2 2018-02-03 04:13:02 +01:00
Jeremy Grossmann
ac989b191b Merge pull request #2410 from GNS3/new-appliance-symbol-from-controller
Appliance import looks for symbols on server, Fixes. #2405
2018-02-02 10:24:32 +01:00
Dominik Ziajka
c971cef31b Handle Certifacte Error, Ref. gns3-server#1262 2018-02-02 10:02:18 +01:00
Dominik Ziajka
c1af2df780 Backward compatibility for tests, Ref. #2405? 2018-02-02 08:47:56 +01:00
grossmj
eaaa141be9 Use UTF-8 for IOURC file migration. 2018-02-02 15:41:42 +08:00
ziajka
226169cdc6 Look for symbols on controller, Ref. #2405 2018-02-01 17:42:02 +01:00
grossmj
42a4c89f20 Display an error message if Telnet console program cannot be executed. 2018-01-29 18:59:28 +07:00
Jeremy Grossmann
38ade919df Merge pull request #2404 from GNS3/allow-different-md5
Allow to accept a different md5 hash than the one in the appliance file.
2018-01-26 16:13:10 +07:00
grossmj
6458f88d1c Remove leftover debug message. 2018-01-26 15:26:27 +07:00
ziajka
1e936da469 Allow to accept a different md5 hash than the one in the appliance file. Ref. server#1246 2018-01-25 11:55:33 +01:00
ziajka
f90ec81fca Critical information during upload file with different md5, Ref. #1246 2018-01-24 11:43:32 +01:00
grossmj
141578a1e1 Restore locked item state. 2018-01-23 17:39:35 +07:00
grossmj
e1d2bcca20 Merge branch '2.1' into 2.2
# Conflicts:
#	gns3/ui/main_window_ui.py
#	gns3/ui/resources_rc.py
#	gns3/version.py
#	resources/resources.qrc
2018-01-23 12:44:02 +07:00
grossmj
a5435280d7 Bump to version 2.2.0dev1 & refresh resources/ui files. 2018-01-23 11:36:58 +07:00
grossmj
1482b0e804 Back to development on v2.1.4dev1 2018-01-21 15:57:41 +07:00
grossmj
8ebe3435c4 Re-release v2.1.3 to fix idna packaging issue. 2018-01-21 15:16:25 +07:00
ziajka
a1cd34d7c4 Back to development on v2.1.4dev1 2018-01-19 08:18:19 +01:00
ziajka
1e4a44135c Re-release v2.1.3 2018-01-19 08:11:46 +01:00
ziajka
a407f1ec90 Update to python3.6 in tests - running xvfb 2018-01-19 08:05:07 +01:00
ziajka
faab113384 Update to python3.6 in tests 2018-01-19 08:01:11 +01:00
ziajka
c158b7fc46 Use Ubuntu 17.10 for TCI tests 2018-01-19 07:55:13 +01:00
ziajka
16de9e830f Use Ubuntu 16.04 for TCI tests 2018-01-19 07:44:37 +01:00
ziajka
25c625c0bb Development on v2.1.4dev1 2018-01-19 07:17:22 +01:00
ziajka
bf42d1a355 Release v2.1.3 2018-01-19 07:15:24 +01:00
grossmj
1c0f3493ee Fix more client/server version tests. 2018-01-18 16:14:09 +08:00
grossmj
c3c1f87c5e Change messages when there are different client and server versions. Fixes #2391. 2018-01-18 15:58:21 +08:00
grossmj
6b80914385 Bump version number to 2.1.3dev1 2018-01-18 15:32:06 +08:00
grossmj
a114d9ace7 Fix "Transport selection via DSN is deprecated" message. Sync is configured with HTTPTransport. 2018-01-15 16:56:16 +07:00
grossmj
4dca4d057a Refresh CPU/RAM info every 1 second. Ref #2262. 2018-01-15 14:42:01 +07:00
grossmj
17af21e29a Only check for AVG on Windows 2018-01-14 13:40:31 +07:00
grossmj
7fbce0266d Improve the search for VBoxManage. 2018-01-11 16:33:15 +07:00
grossmj
d5cdbdbf90 Allow telnet console to node with name containing double quotes. Fixes #2371. 2018-01-10 22:16:35 +07:00
ziajka
e5a790f4b2 Development v2.1.3dev1 2018-01-08 14:21:28 +01:00
ziajka
f3769df0d6 Add to CHANGELOG changes 2018-01-08 14:09:21 +01:00
ziajka
a21db74941 Release v2.1.2 2018-01-08 14:08:12 +01:00
grossmj
d1e1f6dfb6 Update VMware promotion in setup wizard. 2018-01-08 18:41:40 +07:00
grossmj
cc45c9631a Confirm exit. Fixes #2359. 2018-01-08 18:00:59 +07:00
ziajka
d16a52e389 Development on v2.1.2dev1 2017-12-22 13:28:39 +01:00
ziajka
ee2bea7cdd Release v2.1.1 2017-12-22 13:26:55 +01:00
grossmj
7cbc25cbbf Bump version to 2.1.1dev2 2017-12-20 11:20:07 +01:00
ziajka
7237cf1b88 Merge pull request #2373 from GNS3/fix-2363
Fix dragging appliance into topology from nodes window, fixes: #2363
2017-12-20 09:54:05 +01:00
ziajka
965923900b Fix dragging appliance into topology from nodes window, fixes: #2363 2017-12-20 09:53:19 +01:00
ziajka
a5a3a4e8cc Merge pull request #2372 from GNS3/fix-2362-2
Fix Appliances in Docked mode, fixes: #2362
2017-12-20 09:29:21 +01:00
ziajka
d898b30d84 Fix Appliances in Docked mode, fixes: #2362 2017-12-20 09:28:33 +01:00
ziajka
e86ced750e Merge pull request #2370 from GNS3/fix-2366
Create local variable in order to debug issue in the next occurrence,…
2017-12-19 14:48:44 +01:00
ziajka
e15b717cb0 Create local variable in order to debug issue in the next occurrence, #2366 2017-12-19 14:46:24 +01:00
ziajka
d8bd33f0e7 Merge pull request #2369 from GNS3/fix-2364
Fix ParseError: not well-formed (invalid token), #2364
2017-12-19 12:52:25 +01:00
ziajka
bc2fbe33ef Fix ParseError: not well-formed (invalid token), #2364 2017-12-19 12:51:20 +01:00
ziajka
b99b26f463 Merge pull request #2368 from GNS3/fix-2365
Fix local variable 'vm' referenced before assignment #2365
2017-12-19 11:41:30 +01:00
ziajka
5b7606793f Fix local variable 'vm' referenced before assignment #2365 2017-12-19 11:40:24 +01:00
ziajka
b8b5e8739e Merge pull request #2367 from GNS3/fix-2362
Fix: 'NodesDockWidget' object has no attribute 'uiNodesView', #2362
2017-12-19 11:38:11 +01:00
ziajka
0126c30887 Fix: 'NodesDockWidget' object has no attribute 'uiNodesView', #2362 2017-12-18 14:30:26 +01:00
grossmj
a89086ff60 Fix test. 2017-12-07 14:31:41 -06:00
grossmj
9ca35c56de Tentative fix for packet capture not working correctly when remote main server is configured. Ref #2111. 2017-12-07 13:59:02 -06:00
grossmj
3ddccf40a8 Log Qt messages with log.debug() instead of log.info(). 2017-12-05 14:24:24 -06:00
grossmj
d51c96f105 Merge remote-tracking branch 'remotes/origin/2.1' into 2.2 2017-11-27 14:21:31 +07:00
grossmj
a47b839cc2 Merge remote-tracking branch 'remotes/origin/2.1' into 2.2
# Conflicts:
#	gns3/ui/main_window_ui.py
#	gns3/ui/resources_rc.py
2017-11-27 14:19:29 +07:00
Jeremy Grossmann
1398ef323a Merge pull request #2345 from GNS3/no-timeout-snapshots
Snapshoting project without timeout but with Cancel button. Ref. #2314
2017-11-24 18:19:26 +07:00
grossmj
d52c4d839d Fix auto idle-pc from preferences. Fixes #2344. 2017-11-23 23:01:01 +07:00
ziajka
6989ee2c8b Snapshot creation, busy all the time as we cannot calculate progress. 2017-11-23 15:06:44 +01:00
ziajka
5c182e95ca Snapshoting project without timeout but with button. Ref. #2314 2017-11-23 14:13:56 +01:00
grossmj
bcb7a8e57b Improve validation for idle-pc. 2017-11-23 10:41:30 +07:00
grossmj
dba75e844e Activate faulthandler. 2017-11-21 13:44:29 +07:00
Jeremy Grossmann
6733739fa5 Merge pull request #2340 from ehlers/fix-osx-telnet
Fix OS X Telnet
2017-11-20 11:44:02 +07:00
Bernhard Ehlers
f5de62aa05 Add PATH to OS X console commands 2017-11-19 15:40:54 +01:00
Bernhard Ehlers
4087d35f6a Use raw triple quotes in large console settings
This eliminates one level of quoting
2017-11-19 15:31:26 +01:00
grossmj
f6a1af46a0 Fix issue in node summary when console is not supported by a node. 2017-11-19 20:30:58 +07:00
Bernhard Ehlers
6cef2fed5a Revert "Add preferred path to use Telnet console in DMG package. Ref #2274." 2017-11-19 12:37:21 +01:00
Bernhard Ehlers
e16c8db311 Revert "Force to use the telnet client embedded in DMG. Ref #2274." 2017-11-19 12:36:33 +01:00
Bernhard Ehlers
61c95a93ca Revert "Add debug when using Telnet path on OSX. Ref #2274." 2017-11-19 12:35:55 +01:00
Bernhard Ehlers
0df36dab30 Revert "Fix bug when replacing Telnet path on OSX. Ref #2274." 2017-11-19 12:33:54 +01:00
Bernhard Ehlers
9a5da633e0 Revert "Fix problem when embedded telnet client path contains a space on macOS. Ref #2328." 2017-11-19 12:32:29 +01:00
Bernhard Ehlers
02fed964f2 Revert "Support Telnet path containing spaces. Ref #2328." 2017-11-19 12:30:15 +01:00
grossmj
84ba56ae74 Remove unused symbols. Fixes #2320. 2017-11-19 14:50:12 +07:00
grossmj
e8c4758cb7 Show console information in Topology Summary Dock. Fixes #2258. 2017-11-19 13:56:54 +07:00
grossmj
d8dc31965f New option: require KVM. If false, Qemu VMs will not be prevented to run without KVM. 2017-11-19 12:39:37 +07:00
grossmj
9af48ba9a3 Implement variable replacement for Qemu VM options. 2017-11-18 17:36:11 +07:00
grossmj
67d8e317e0 Show on what server a node is installed in the servers summary pane. Fixes #2279. 2017-11-18 16:02:31 +07:00
grossmj
64392780c5 Add more info when cannot remove capture file after stopping packet capture in a remote project. Ref #1223. 2017-11-17 18:32:18 +07:00
grossmj
81bb159d45 Do not overwrites the disk images when copied to default directory. Fixes #2326. 2017-11-17 15:42:33 +07:00
ziajka
85352af9bd Update pyup config to use 2.2 branch 2017-11-15 13:21:14 +01:00
grossmj
65cfdf6b33 Revert "Only replace quoted telnet for macOS Telnet commands. Ref #2328." 2017-11-15 10:39:24 +07:00
grossmj
5d45dbebf6 Only replace quoted telnet for macOS Telnet commands. Ref #2328. 2017-11-14 18:42:17 +07:00
grossmj
0a7b6d81d8 Support Telnet path containing spaces. Ref #2328. 2017-11-14 18:34:06 +07:00
grossmj
588dcadd3a Fix problem when embedded telnet client path contains a space on macOS. Ref #2328. 2017-11-14 17:22:44 +07:00
grossmj
f24c93a55f Do not launch console for builtin nodes when using the "Console to all nodes" button. Fixes #2309. 2017-11-12 17:02:00 +08:00
grossmj
26e5c80406 Merge remote-tracking branch 'remotes/origin/master' into 2.1 2017-11-12 14:22:13 +08:00
ziajka
eb502232a2 Development on 2.1.1dev1 2017-11-09 10:50:12 +01:00
ziajka
25e17d718c Release 2.1.0 2017-11-09 07:29:33 +01:00
ziajka
89108070df Development on v2.1.0dev10 2017-11-07 10:19:46 +01:00
ziajka
f4df3ff9c0 Release v2.1.0rc4 2017-11-07 08:48:00 +01:00
Jeremy Grossmann
90f80b9804 Merge pull request #2322 from GNS3/upload-dialogs-progress
Accurate upload progress dialogs for large files
2017-11-06 20:50:59 +08:00
ziajka
3e86044132 Fix tests 2017-11-06 09:31:39 +01:00
ziajka
78d805cebc Disable showProgress during obtaining endpoint 2017-11-06 09:28:07 +01:00
ziajka
289f754108 Accurate upload progress dialogs for large files 2017-11-03 11:38:04 +01:00
Jeremy Grossmann
1a0c1f826b Merge pull request #2313 from GNS3/disable-direct-file-upload
Disable direct file upload on default
2017-10-30 17:18:36 +07:00
ziajka
9837d661a5 Disable direct file upload on default 2017-10-26 13:41:27 +02:00
grossmj
ff60776769 Merge remote-tracking branch 'origin/2.1' into 2.1 2017-10-26 15:47:02 +07:00
grossmj
7ac442631a Add registry version 5 2017-10-26 15:46:39 +07:00
Jeremy Grossmann
8da2ff3a97 Merge pull request #2310 from GNS3/image-upload-manager
Safe approach to send files to computes and dialogs fixes, Fixes: #2307, Ref: #2188
2017-10-26 13:01:14 +07:00
Jeremy Grossmann
a04d9784f2 Merge branch '2.1' into image-upload-manager 2017-10-26 12:24:17 +07:00
ziajka
623aa4a2de Code style and tests 2017-10-25 16:10:24 +02:00
ziajka
ef3c2afab9 Direct file upload enabled on default 2017-10-25 15:22:39 +02:00
ziajka
73e59d92ca Progress Dialog: don't count finished queries done in background 2017-10-25 13:50:35 +02:00
ziajka
8f3d5bf038 Add debug messages to file upload 2017-10-25 10:27:17 +02:00
ziajka
d638c6e0d7 Image Upload Manager for uploading 2017-10-24 17:20:30 +02:00
grossmj
c5688cacf9 Restore timer for refreshing the progress dialog status. 2017-10-24 20:12:01 +07:00
Jeremy Grossmann
b34bcd6369 Merge pull request #2305 from GNS3/race-nodes-view
Fix race condition on NodesDockWidget, fixes: #2304
2017-10-23 16:10:40 +07:00
ziajka
5e0fc3675f Fix race condition on NodesDockWidget, fixes: #2304 2017-10-23 11:08:33 +02:00
grossmj
e75a21e2ed Do not write an error message when importing non existing config from a directory. Fixes #2296. 2017-10-21 12:30:08 +07:00
grossmj
aeee44e597 Fix bug when replacing Telnet path on OSX. Ref #2274. 2017-10-19 16:32:16 +07:00
ziajka
3cebee64ad Back to development on 2.1.0rc3 2017-10-19 09:56:56 +02:00
ziajka
fd1619cfd3 Fix Travis deploy - urlib3 2017-10-19 09:51:05 +02:00
ziajka
4573d2aed8 Fix Travis deploy - urlib3 2017-10-19 09:47:22 +02:00
ziajka
7f29c497cc Fix Travis deploy 2017-10-19 09:21:57 +02:00
ziajka
6da42b5013 Development on 2.1.0dev9 2017-10-19 08:58:15 +02:00
ziajka
430366947f Release 2.1.0 rc3 2017-10-19 08:56:10 +02:00
grossmj
3870f8ecdc Add debug when using Telnet path on OSX. Ref #2274. 2017-10-18 18:47:24 +07:00
grossmj
f68626e4cc Force to use the telnet client embedded in DMG. Ref #2274. 2017-10-18 17:26:44 +07:00
Jeremy Grossmann
6b4126b688 Merge pull request #2297 from GNS3/direct-file-upload
Direct file upload, Fixes #2264
2017-10-18 00:36:28 +07:00
ziajka
2ef9890dc1 Upload directly to compute 2017-10-17 12:32:38 +02:00
ziajka
100e3dbf27 Load endpoing and execute post image 2017-10-16 09:44:16 +02:00
Jeremy Grossmann
b85db6e24f Merge pull request #2294 from ddragic/qxcb-log-filter
Filter additional QXcbConnection log messages
2017-10-14 15:38:52 +02:00
Dušan Dragić
05d2077b16 Filter additional QXcbConnection log messages 2017-10-14 13:21:55 +02:00
grossmj
80f3dab152 Rebuild resources file. 2017-10-13 18:21:48 +08:00
grossmj
eef4d6e9fd Merge 2.1 to 2.2 2017-10-13 18:11:27 +08:00
ziajka
357d039434 Preparation to load endpoint before usage 2017-10-13 12:05:31 +02:00
ziajka
0288384c85 Direct file upload settings 2017-10-13 11:22:32 +02:00
grossmj
84347848e9 Do not add missing file extension for screenshot file names on Mac. Fixes #2287. 2017-10-11 04:58:59 +08:00
grossmj
24e5ef885c Add preferred path to use Telnet console in DMG package. Ref #2274. 2017-10-10 23:57:34 +08:00
grossmj
0d8255ecaf Merge remote-tracking branch 'origin/2.1' into 2.1 2017-10-10 22:32:18 +08:00
grossmj
679548e4ad Log Qt messages as info instead of error. Ref #2281. 2017-10-10 22:13:22 +08:00
ziajka
e5384af45d Fix Travis bug with missing twine library. Fixes: #2283 2017-10-06 09:55:43 +02:00
ziajka
ae68d4d84b Development on 2.1.0dev8 2017-10-04 11:40:14 +02:00
ziajka
2ab81816ef Release 2.1.0 rc2 2017-10-04 11:36:51 +02:00
grossmj
4c4241183a Only show "can't get settings from controller" message in debug mode. 2017-10-04 16:24:29 +08:00
grossmj
46406d1e7b Remove explicit Telnet path on OS X. Ref #2274 2017-10-03 04:31:08 +08:00
Jeremy Grossmann
a92573394f Merge pull request #2280 from GNS3/fix-pyqt-version-check
Disable WebSocket notification for lower PyQT version than 5.6. Fixes…
2017-10-02 18:03:31 +02:00
ziajka
6baf628997 Disable WebSocket notification for lower PyQT version than 5.6. Fixes #2272 2017-10-02 10:44:28 +02:00
grossmj
8f9190e094 Increase timeout to 5 minutes when creating and restoring a snapshot. 2017-10-02 05:02:26 +08:00
grossmj
7e14e734b2 Bump version to 2.1.0dev7 2017-10-02 04:04:38 +08:00
grossmj
76fc2f07ce Add more information when a request timeouts. Ref #2277. 2017-10-02 00:46:23 +08:00
grossmj
eee066d5f3 Do not show the progress dialog when moving a node. Ref #2275. 2017-10-02 00:44:58 +08:00
grossmj
9dead47a37 Increase timer before showing a progress dialog from 250ms to 500ms. Ref #2275. 2017-10-01 23:56:28 +08:00
grossmj
a22bd8e9be Use embedded Telnet client on OS X. Ref #2274. 2017-10-01 23:33:12 +08:00
grossmj
a4b897d458 Fix small bug when adding an appliance template and the name already exists. 2017-09-19 16:32:19 +07:00
grossmj
e784f21c0f Use RAW sockets by default on Linux for VMware VM connections. 2017-09-19 12:47:30 +07:00
grossmj
3a000cdc60 Increase timeout to get compute servers from controller. Ref #2269. 2017-09-15 19:40:42 +07:00
grossmj
405f3b3382 Fix "Node doesn't exist" after deletion, but still on the canvas. Fixes #2266. 2017-09-15 17:23:15 +07:00
grossmj
7397f76566 Remove debug test. 2017-09-15 16:24:57 +07:00
grossmj
178cb35d6a Make sure the warning button icon appears in cloud properties dialog on Windows. Fixes #2245. 2017-09-15 16:21:05 +07:00
grossmj
012e5d331d Fix bug when cancelling the importation of a configuration file. Fixes #2260. 2017-09-15 15:52:36 +07:00
ziajka
bd81d36635 Development on v2.1.0dev6 2017-09-13 09:30:56 +02:00
ziajka
234eab57c8 Release 2.1.0rc1 2017-09-13 09:28:55 +02:00
grossmj
e4b19714f4 Fix missing spice console option in appliance template schema. Fixes #2255. 2017-09-13 13:55:30 +07:00
ziajka
7bfba1015b Back to development at 2.1.0dev5 2017-09-05 11:24:08 +02:00
ziajka
498ba2d2b1 Re-release 2.1.0b2 2017-09-05 11:16:02 +02:00
ziajka
f3756b8401 Fix unicode error during appliance tests 2017-09-05 09:56:56 +02:00
ziajka
68f6d37aab Fix link tests 2017-09-05 09:43:33 +02:00
ziajka
6ce35fa5b5 Development on 2.1.0dev5 2017-09-05 08:41:08 +02:00
ziajka
e376753859 Release 2.1.0 beta 2 2017-09-05 08:37:42 +02:00
Jeremy Grossmann
7b03c3eae7 Merge pull request #2250 from GNS3/dont-move-under-layer-0
Disabled possibility of moving items under zero layer (Fixes #2220)
2017-09-01 16:20:04 +07:00
ziajka
902ba42be1 Merge pull request #2252 from GNS3/fix-resources
Fix resources dependencies for cloud configuration page (Fixes: #2251)
2017-09-01 11:06:58 +02:00
ziajka
73fe898eda Fix resources dependencies for cloud configuration page (Fixes: #2251) 2017-09-01 11:05:53 +02:00
ziajka
1ff488d39a Disabled possibility of moving items under zero layer (Fixes #2220) 2017-09-01 10:13:19 +02:00
Jeremy Grossmann
1622a79383 Merge pull request #2248 from GNS3/dialog-warning-fallback
dialog-warning.svg fallback for themed icon (Ref. #2245)
2017-09-01 12:51:38 +07:00
Jeremy Grossmann
1564c63a42 Merge pull request #2247 from GNS3/wide-packet-filters-dialog
Change width of packet filters dialog (Fixes #2244)
2017-09-01 12:50:24 +07:00
ziajka
29f651aaea dialog-warning.svg fallback for themed icon (Ref. #2245) 2017-08-31 11:37:32 +02:00
ziajka
9ee0222339 Change width of packet filters dialog (Fixes #2244) 2017-08-31 09:46:08 +02:00
grossmj
6e1384c985 Fix high CPU usage when using packet filters. Fixes #2240. 2017-08-28 11:40:50 +07:00
Jeremy Grossmann
20190c5816 Merge pull request #2232 from GNS3/toggle-node-menu-item
Toggle Node menu item (Fixes #2227)
2017-08-25 16:32:49 +08:00
ziajka
cab3412ddc Toggle Node menu item (Fixes #2227) 2017-08-22 13:01:50 +02:00
grossmj
78e7b78315 Have the contextual menu use icons from the active style. Ref #1272. 2017-08-15 19:38:23 +08:00
grossmj
b3f8170e01 Individually lock or unlock an item on the scene. Fixes #1228. 2017-08-11 17:35:24 +08:00
grossmj
18321f5347 Improve lock and unlock all items so some actions can still be performed on objects. Fixes #1134. 2017-08-11 15:03:33 +08:00
grossmj
734fcdfe9e Lock or unlock all items button. Fixes #1134. 2017-08-11 14:37:45 +08:00
Jeremy Grossmann
6d74ce4070 Merge pull request #2215 from GNS3/fix-qemu-edit-symbol
Fixes loading symbols for QEMU at Edit Page (#2214)
2017-08-10 21:41:58 +08:00
Jeremy Grossmann
159d21af9a Merge pull request #2217 from GNS3/fixes-lineitem
Fixes multiselection styles change crash on LineItem (#2216)
2017-08-10 21:39:42 +08:00
ziajka
713feff11f Fixes multiselection styles change crash on LineItem (#2216) 2017-08-10 09:38:22 +02:00
ziajka
64c5ca712e Fixes loading symbols for QEMU at Edit Page (#2214) 2017-08-10 09:09:07 +02:00
Jeremy Grossmann
1572a6f67f Merge pull request #2212 from GNS3/2211
Fixes exception when right click on Dynamips router in the device dock
2017-08-08 22:16:53 +08:00
ziajka
fcee5c6916 Fixes exception when right click on Dynamips router in the device dock (#2211) 2017-08-08 13:59:47 +02:00
grossmj
378d454e1e Move console to all devices icon after the separation bar. Ref #1272 2017-08-08 15:56:22 +08:00
grossmj
eb90950be1 Lock icons. Ref #1134. 2017-08-08 15:46:52 +08:00
Jeremy Grossmann
3d21f9a997 Merge pull request #2210 from CapnCheapo/patch-1
Update frame_relay_switch_configuration_page_ui.py
2017-08-08 11:14:19 +08:00
Jeremy Grossmann
d93ad5e9d5 Merge pull request #2209 from CapnCheapo/2.1
Update frame_relay_switch_configuration_page.ui
2017-08-08 11:14:08 +08:00
Stephen C. Moore
13739281da Update frame_relay_switch_configuration_page_ui.py
Fixes #2205
2017-08-07 14:04:44 -05:00
Stephen C. Moore
1f281a807b Update frame_relay_switch_configuration_page.ui
Fixes #2205
2017-08-07 14:03:00 -05:00
ziajka
2ca250d2c2 Development on 2.1.0dev4 2017-08-04 11:36:47 +02:00
ziajka
b82b031168 Release 2.1.0 beta 1 2017-08-04 11:35:21 +02:00
Julien Duponchelle
c48048f013 Info added to the Nat node
Ref #2197
2017-08-02 13:19:24 +02:00
Julien Duponchelle
9aaca9955a Add missing popup information in cloud and docker node
Fix #2197
2017-08-02 12:14:30 +02:00
Julien Duponchelle
a0e6a82ea2 Handle invalid json in websockets
Fix #2192
2017-08-01 16:32:52 +02:00
Julien Duponchelle
9a3e320e95 Avoid invalid bad request error when receiving partial answer
Fix #2194
2017-08-01 16:29:31 +02:00
Julien Duponchelle
c3fce51493 Catch parse error for broken SVG
Fix #2193
2017-08-01 16:14:08 +02:00
Julien Duponchelle
116cf55758 Filter QXcbConnection log messages
It's Qt noise on Linux we can't do nothing to avoid it.

Fix #2191
2017-08-01 16:14:08 +02:00
Julien Duponchelle
269c6bd0cd Catch class 'PyQt5.QtNetwork.QNetworkReply'> returned a result with an error set
Fix #2195
2017-08-01 16:14:08 +02:00
Julien Duponchelle
31aa612a62 Fix KeyError: 'overlay_notifications'
Fix #2196
2017-08-01 16:14:07 +02:00
ziajka
55f396694f Development on 2.1.0dev3 2017-07-31 11:35:40 +02:00
ziajka
b51fd9c92f Release 2.1.0 alpha 2 2017-07-31 11:31:42 +02:00
Julien Duponchelle
5857d3709b Fix permission error when importing a project on a remote server
Fix #2082
2017-07-27 10:20:28 +02:00
Julien Duponchelle
0c00e1309c Fix RecursionError
Fix #2185
2017-07-26 15:46:40 +02:00
Julien Duponchelle
06dbf9f7d8 Fix 'NodesDockWidget' object has no attribute 'loadPath'
Fix #2182
2017-07-26 14:59:13 +02:00
Julien Duponchelle
ef651d9e9a Fix 'MainWindow' object has no attribute '_settings
Fix #2183
2017-07-26 14:55:13 +02:00
Julien Duponchelle
65dd3a23c6 Fix object has no attribute 'warning_signal'
Fix #2184
2017-07-26 14:53:25 +02:00
Julien Duponchelle
85f697d47b Fix timeout issues when using an appliance 2017-07-26 11:08:39 +02:00
Jeremy Grossmann
0988fdca09 Merge pull request #2180 from GNS3/ubridge_dir
Make sure ubridge path is not a directory
2017-07-25 06:40:36 -07:00
Julien Duponchelle
eba3d5751e Make sure ubridge path is not a directory
Ref https://twitter.com/andreppires/status/889594139800719360
2017-07-25 09:03:57 +02:00
Julien Duponchelle
93e140ae05 2.1.0dev2 2017-07-24 16:19:52 +02:00
Julien Duponchelle
b81a531a7b 2.0.1a1 2017-07-24 16:19:16 +02:00
Jeremy Grossmann
089b4108cc Merge pull request #2179 from GNS3/duplicate
Allow to duplicate a node
2017-07-24 01:39:32 -07:00
grossmj
b89f70370a Updating text for duplicating node/project. 2017-07-24 15:38:54 +07:00
Jeremy Grossmann
81b4ded30a Merge pull request #2174 from GNS3/fix_segfault
Try to fix segfault at exit
2017-07-21 08:44:06 -07:00
Julien Duponchelle
b658eea427 Poll local server return code when waiting for server stop 2017-07-21 13:36:03 +02:00
Julien Duponchelle
da225ffdf9 Try to avoid websocket garbage collection 2017-07-21 13:25:02 +02:00
Jeremy Grossmann
b7fb6e6b13 Merge pull request #2171 from GNS3/fix_wifi_off
Fix issues when Wifi is turned off
2017-07-21 02:50:08 -07:00
Julien Duponchelle
078cef064b Allow to duplicate a node
Ref #1065
2017-07-20 18:05:46 +02:00
Julien Duponchelle
bec1c41f75 Handle recent version of Chicken of VNC
Fix #2146
2017-07-20 16:15:35 +02:00
Julien Duponchelle
64f3516153 Catch unknown protocol errors
The server should use a different port automaticaly.

Fix #2120, #2131
2017-07-20 15:31:02 +02:00
Julien Duponchelle
558e8ad8ce Set main window as parent of LocalServer class 2017-07-20 11:51:05 +02:00
Julien Duponchelle
5f7408809e Fix a race condition when we got a project error 2017-07-20 11:09:38 +02:00
Julien Duponchelle
8359da3c76 Increase timeout before quitting GNS3 because server could be slow to stop 2017-07-20 11:07:11 +02:00
Julien Duponchelle
c613e20971 Try to avoid multiple error dialog in case of network issues 2017-07-20 11:03:39 +02:00
Julien Duponchelle
34ab6c2e1b Try to fix segfault at exit
Fix #2166
2017-07-20 10:46:34 +02:00
Julien Duponchelle
5382a8a397 Fix image permissions
Fix #2169
2017-07-20 09:35:05 +02:00
grossmj
507f104ae5 os.geteuid() doesn't exist on Windows. 2017-07-20 12:15:00 +07:00
Jeremy Grossmann
ada2f647a0 Merge pull request #2170 from GNS3/appliances_dir
Add an appliance templates directory
2017-07-19 22:10:03 -07:00
grossmj
347b76d39e Update text for "My custom appliances" 2017-07-20 12:08:58 +07:00
Jeremy Grossmann
3749819016 Merge pull request #2172 from GNS3/suspend_link
Suspend link
2017-07-20 11:15:28 +07:00
grossmj
4d4871d165 Improve link suspend support. 2017-07-20 11:13:17 +07:00
Julien Duponchelle
59f6a22e81 Suspend link
Fix https://github.com/GNS3/gns3-gui/issues/1295
2017-07-19 17:23:19 +02:00
Julien Duponchelle
0982338e2c Fix tests suite for 2.1 GUI 2017-07-19 15:55:07 +02:00
Julien Duponchelle
20efde749c Turn off timeout for node creation
The timeout is an issue if you use a local GNS3 server and
a remote over a slow link.

Fix https://github.com/GNS3/gns3-server/issues/1126
2017-07-19 15:26:51 +02:00
Julien Duponchelle
23c3576256 Fix docker test env 2017-07-19 15:11:31 +02:00
Julien Duponchelle
1dbf30c6cb Fix issues when Wifi is turned off
Fix #2104
2017-07-19 14:50:40 +02:00
Julien Duponchelle
2081689c12 Change text for my appliances 2017-07-19 13:47:02 +02:00
Julien Duponchelle
983c55928e Add an appliance templates directory
* Settings in general preferences to configure it
* When you import a gns3a the file is copy to this directory
* A filter my appliances is available

Fix https://github.com/GNS3/gns3-gui/issues/2133
2017-07-19 11:55:46 +02:00
Julien Duponchelle
de625d6cfc Allow duplicate name in nodes view if emulator are different
Fix #2158
2017-07-19 09:49:03 +02:00
Jeremy Grossmann
523d791cac Merge pull request #2164 from GNS3/tabs-filters
Tabs in packet filters dialog. Fixes #2159
2017-07-17 21:35:43 +07:00
grossmj
270518f294 Add reset button and tab icons to indicate filter status. 2017-07-17 21:33:55 +07:00
Julien Duponchelle
7ab8d679f7 Fix a rare crash 2017-07-17 12:06:27 +02:00
ziajka
f518464eb2 Tabs in packet filters dialog. Fixes #2159 2017-07-17 10:44:15 +02:00
grossmj
321685acb8 Fixes tests. 2017-07-17 13:25:41 +07:00
grossmj
747ca36a5a Fixes uncaught exception in select server. Fixes #2106. 2017-07-17 12:59:17 +07:00
grossmj
485844f8de Fixes missing "style" in label data. Fixes #2110. 2017-07-17 12:40:14 +07:00
grossmj
b7c0a8c368 Fixes bug when no server selected. Fixes #2163. 2017-07-17 12:25:06 +07:00
grossmj
678c42f941 Fixes uBridge check. Fixes #2143. 2017-07-16 16:56:41 +07:00
Jeremy Grossmann
1eaf6c97e0 Merge pull request #2160 from GNS3/run-as-root
Fixes #2048. Inform the user about running as root, disallow change o…
2017-07-16 12:27:28 +07:00
grossmj
f92282f823 Update message to prevent to run as a user. 2017-07-16 12:26:45 +07:00
ziajka
9ef90210d8 Better os handling and message improvement, fixes #2160 2017-07-14 13:11:41 +02:00
Jeremy Grossmann
f280ea4c68 Merge pull request #2161 from GNS3/fix_2157
Fix Wireshark is restarted when updating packet filters
2017-07-12 22:09:42 +07:00
Julien Duponchelle
0067634990 Fix Wireshark is restarted when updating packet filters
Fix #2157
2017-07-12 16:40:52 +02:00
ziajka
4799fc7c93 Fixes #2048. Inform the user about running as root, disallow change of user and disable crash reports in this case 2017-07-12 14:07:49 +02:00
Jeremy Grossmann
829154fb1c Merge pull request #2156 from GNS3/bpf_filter
BPF filter support
2017-07-12 16:23:48 +07:00
grossmj
7e0caba4b0 Small changes for packet filters Ui. 2017-07-12 16:23:29 +07:00
grossmj
3c3890ff21 Fix typo. 2017-07-12 14:12:48 +07:00
Julien Duponchelle
65411d1742 Merge branch 'master' into 2.1 2017-07-11 17:59:12 +02:00
Julien Duponchelle
146a6a5af2 BPF filter support
Fix #765
2017-07-11 17:28:38 +02:00
grossmj
fc72140402 Bring to front complete (Windows only). Fixes #847. 2017-07-11 21:22:22 +07:00
grossmj
1b13d83e38 New icon for bring to front. Ref #847. 2017-07-11 20:02:43 +07:00
grossmj
e3f073d74b Generic best effort bring console to front for all nodes. Ref #847. 2017-07-11 16:31:18 +07:00
grossmj
8f6e84f8a9 Support bring to front for VMware VMs. Ref #847. 2017-07-11 15:31:42 +07:00
grossmj
c594c3d8a7 Fixes bring to front for VirtualBox VMs. Ref #847. 2017-07-11 14:30:26 +07:00
Jeremy Grossmann
a550527e6d Merge pull request #2153 from GNS3/idlepc_apicall
Idlepc apicall
2017-07-10 11:33:26 +07:00
Jeremy Grossmann
0ce377f321 Merge pull request #2145 from GNS3/preserve-file-permissions
Preserve permissions while copying files. Fixes #2125.
2017-07-08 21:56:50 +07:00
grossmj
ab37a6237c Fix small bug when not capturing/filtering on a link. 2017-07-08 21:47:30 +07:00
Jeremy Grossmann
ecc57133c6 Merge pull request #2139 from GNS3/filters
Filter packet interface
2017-07-08 18:51:50 +07:00
grossmj
fc6c2c0304 Filter icons and bug fixes for topology summary view. 2017-07-08 18:46:31 +07:00
Julien Duponchelle
92c731a9c9 Merge pull request #2151 from GNS3/improve-tpl-error
Fixes #2122. Warning dialog on Win and Mac when user has not choice.
2017-07-07 16:58:42 +02:00
Julien Duponchelle
bc433e5281 Merge pull request #2152 from GNS3/bugfix-2121
New appliance - case without versions in file. Fixes #2121
2017-07-07 16:58:04 +02:00
Julien Duponchelle
21eb0b0f03 Call the new api for getting IDLE PC values
Ref #2103
2017-07-07 16:41:17 +02:00
ziajka
3f70d0238f New appliance - case without versions in file. Fixes #2121 2017-07-07 12:59:41 +02:00
ziajka
8fd3f67378 Fixes #2122. Warning dialog on Win and Mac when user has not choice. 2017-07-07 11:54:27 +02:00
Julien Duponchelle
4e9cb90468 Remove dead code 2017-07-07 11:10:40 +02:00
Julien Duponchelle
d38e62fa38 Renable auth
Fix #2148
2017-07-07 11:09:39 +02:00
Julien Duponchelle
4b7df545aa Test if auth is enabled
Ref #2148
2017-07-07 10:20:22 +02:00
grossmj
6af5f5f3fb Stop all packet filters from the topology summary view. 2017-07-07 14:12:36 +07:00
Julien Duponchelle
b9318dfe6a Merge pull request #2140 from GNS3/bugfix-2137
Fixes #2137
2017-07-07 09:01:32 +02:00
Julien Duponchelle
6ffc2c807b Merge pull request #2144 from GNS3/bugfix-2129
Bugfix 2129 - ValueError: stat: path too long for Windows
2017-07-07 09:00:51 +02:00
grossmj
d177ea44bd Change the way the help is displayed for filter dialog. 2017-07-06 20:40:55 +07:00
ziajka
39f3b22817 Removed initial checks in tests due to Windows 2017-07-06 14:53:44 +02:00
ziajka
d157295550 Preserve permissions while copying files. Fixes #2125. 2017-07-06 14:47:08 +02:00
ziajka
d7b9465850 Typo. Fixes #2129 2017-07-06 13:30:59 +02:00
ziajka
0766dac62b Handle ValueError on Windows during checking path with SVG data. Fixes #2129 2017-07-06 13:29:34 +02:00
ziajka
4f81dde2fd Checking if new project dialog is already open, #2140 2017-07-06 11:47:58 +02:00
Julien Duponchelle
4a716000ff Do not crash if zoom is None 2017-07-06 09:58:13 +02:00
Jeremy Grossmann
d8b0e9234e Merge pull request #2134 from GNS3/use_websocket_for_notification
Use websocket for notifications
2017-07-05 15:42:50 +07:00
Julien Duponchelle
bf0af2a929 Cleanup double include of QtWebsockets 2017-07-05 10:33:48 +02:00
Jeremy Grossmann
a05e47a4d2 Merge pull request #2135 from GNS3/bugfix-725
Bugfix 725 (IPV6)
2017-07-05 14:47:56 +07:00
grossmj
996c5c927c Fix call to protocol() and add :: IPv6 address. 2017-07-05 14:47:07 +07:00
Julien Duponchelle
dab7569575 Fix compatibility with Qt version before 5.6 2017-07-04 18:24:50 +02:00
Julien Duponchelle
2a0e8a3b4f Merge pull request #2138 from GNS3/bugfix-1095
Bugfix 1095
2017-07-04 15:20:42 +02:00
ziajka
872e7199e4 Fixes #2137 2017-07-04 15:14:15 +02:00
Julien Duponchelle
67b57a8d78 Filter packet interface
Ref #765
2017-07-04 15:05:41 +02:00
ziajka
ba3c1e6969 Added protocol filtering on available server bind addresses 2017-07-04 14:00:33 +02:00
ziajka
1879172505 Persistent zoom level - refactor 2017-07-04 09:33:25 +02:00
ziajka
22fe51fe5a Show interface labels - persistance 2017-07-04 08:36:37 +02:00
ziajka
b5ac40896f Snap to grid persistance 2017-07-04 08:27:54 +02:00
ziajka
2d6b53245b Show the grid - save the state 2017-07-04 08:14:19 +02:00
ziajka
eee377c4fc Show layers persistent state 2017-07-03 15:09:12 +02:00
ziajka
c34b82c255 State of the zoom settings 2017-07-03 10:40:23 +02:00
grossmj
c750ce8d80 Add QtWebsockets dependency. 2017-06-30 17:49:30 +08:00
ziajka
2d2e682540 Add warning when user tries to run vncviewer on IPv6 host 2017-06-30 10:19:07 +02:00
Julien Duponchelle
e06cf5b9a1 Use websocket for notifications
Fix #2127
2017-06-29 10:58:06 +02:00
ziajka
d25258c47f IPV6 support for spice client 2017-06-28 15:50:25 +02:00
grossmj
1e40a36a48 Delete duplicated code. 2017-06-28 17:20:25 +08:00
Jeremy Grossmann
d5059d22fc Merge pull request #2119 from GNS3/drop_trusty
Drop support for Qt before 5.5 (ubuntu trusty)
2017-06-24 13:54:29 +02:00
grossmj
bae61bdcaa Allow IOU 64-bit images. 2017-06-23 12:00:33 +02:00
Julien Duponchelle
eb44226ee4 Drop support for Qt before 5.5 (ubuntu trusty)
Fix #2080
2017-06-22 14:32:53 +02:00
Jeremy Grossmann
8ee251cbb2 Merge pull request #2087 from GNS3/ethernet_switch_console
Console for ethernet switch
2017-06-21 23:53:07 +02:00
Julien Duponchelle
a84e081a75 Merge pull request #2116 from GNS3/spice-feature
Spice support
2017-06-21 11:56:15 +02:00
ziajka
d92db4e99d Spice support
* SPICE console type for QEMU settings pages
* SPICE settings tab at General Preferences
* Executing SPICE console type
* Preconfigured SPICE client for Windows
* SPICE commands for Win and Mac
2017-06-21 11:21:33 +02:00
Julien Duponchelle
873e04ed9d Merge branch 'master' into 2.1 2017-06-21 10:39:10 +02:00
Julien Duponchelle
c0c41b99eb Merge pull request #2114 from GNS3/revert-2112-spice-feature
Revert "Spice feature"
2017-06-21 10:31:21 +02:00
Julien Duponchelle
12b694047a Revert "Spice feature" 2017-06-21 10:30:49 +02:00
Julien Duponchelle
59651a3fe5 Merge pull request #2112 from GNS3/spice-feature
Spice feature
2017-06-21 10:29:58 +02:00
ziajka
02ad5d2f3a SPICE commands for Win and Mac 2017-06-20 12:37:44 +02:00
ziajka
a31b98f781 Preconfigured SPICE client for Windows 2017-06-20 10:18:25 +02:00
ziajka
e9a674c4e9 Executing SPICE console type 2017-06-16 14:16:54 +02:00
ziajka
4b383e2b06 SPICE settings tab at General Preferences 2017-06-16 11:11:40 +02:00
ziajka
6d2ca353a3 SPICE console type for QEMU settings pages 2017-06-16 10:57:00 +02:00
ziajka
d8b5caf679 Ignore env directory inside the project 2017-06-16 10:27:24 +02:00
Bernhard Ehlers
d61088e3a7 Sync appliance.json with gns3-registry repository
Fix #2107

Signed-off-by: Julien Duponchelle <julien@gns3.net>
2017-06-15 15:39:08 +02:00
Julien Duponchelle
a3f0569663 2.0.4dev1 2017-06-13 10:34:20 +02:00
Julien Duponchelle
31e82bb410 2.0.3 2017-06-13 10:33:13 +02:00
Julien Duponchelle
cab3baf2c6 Cleanup 2017-06-12 16:04:11 +02:00
grossmj
55b80cc9cb Merge remote-tracking branch 'origin/2.1' into 2.1 2017-06-09 22:37:29 +02:00
grossmj
aec6c37016 Do not enable authentication by default. 2017-06-09 22:37:10 +02:00
Julien Duponchelle
574da9c80a Display error when we can't export files
Fix #2097, #2098
2017-06-09 15:06:33 +02:00
Julien Duponchelle
117f6ec3b1 Merge branch '2.1' into ethernet_switch_console 2017-06-09 14:30:39 +02:00
Julien Duponchelle
574d6b3792 Merge branch 'master' into 2.1 2017-06-09 14:16:17 +02:00
Julien Duponchelle
8321883199 Fix auth header not sent is some conditions
Fix #2099
2017-06-09 14:12:38 +02:00
Julien Duponchelle
9608614aa9 Merge branch 'master' into 2.1 2017-06-09 11:39:24 +02:00
Julien Duponchelle
98e4aefa65 If we have auth issue at server startup continue to get better error 2017-06-09 11:37:54 +02:00
Julien Duponchelle
67d816baa3 Do not override IOU configuration file when you change the image
Fix #2091
2017-06-08 16:20:04 +02:00
Julien Duponchelle
13a8bd4500 Fix some PNG loading issues on Windows
Fix #2085
2017-06-08 14:59:47 +02:00
Julien Duponchelle
802b80b764 Handle label with missing elements
Fix #2096
2017-06-07 19:02:38 +02:00
Julien Duponchelle
fe5f80382a Merge branch '2.1' into ethernet_switch_console 2017-06-07 18:30:42 +02:00
Julien Duponchelle
a4ed59200d Merge branch 'master' into 2.1 2017-06-07 18:28:54 +02:00
Julien Duponchelle
59292ff6cb Support floating value for font size
Fix #2092
2017-06-07 16:34:05 +02:00
Julien Duponchelle
7810d19f4d Handle partial json in a response
Fix #2093
2017-06-07 14:49:31 +02:00
ziajka
0788ce569f Extended 'Thanks to' section - tab selection on first element 2017-06-06 16:14:53 +02:00
ziajka
4b0379892d Extended section 2017-06-06 15:55:15 +02:00
Julien Duponchelle
3ca05c7427 Console for ethernet switch
Ref https://github.com/GNS3/gns3-server/issues/454
2017-05-31 13:23:37 +02:00
Julien Duponchelle
6a16bcedc0 Reduce debug noise 2017-05-31 12:05:48 +02:00
Julien Duponchelle
8f8994e7df 2.0.3dev1 2017-05-30 09:10:54 +02:00
Julien Duponchelle
4e172fc7e3 2.0.2 2017-05-30 09:02:02 +02:00
Julien Duponchelle
56ebfc7fd0 Drop SSL support
Fix #1022
2017-05-26 15:52:09 +02:00
Julien Duponchelle
8ed8a2c115 Show a default symbol in case of corrupted file
Fix https://github.com/GNS3/gns3-server/issues/1043
2017-05-24 12:26:18 +02:00
Julien Duponchelle
073665a75d When another gui is already running exit instead of proper close to avoid any issue
Fix #2059
2017-05-23 18:00:02 +02:00
Julien Duponchelle
4ccc67aa46 Fix duplicate on remote server use wrong location
Fix https://github.com/GNS3/gns3-server/issues/1040
2017-05-23 17:25:29 +02:00
Julien Duponchelle
6e2632e91f Display the location of settings when we disallow opening due to old release 2017-05-23 15:54:35 +02:00
Julien Duponchelle
38ddcde902 Improve search for dynamips in development on OSX 2017-05-23 14:24:54 +02:00
Julien Duponchelle
436563afcb Merge pull request #2079 from GNS3/pyup-update-pytest-3.0.7-to-3.1.0
Update pytest to 3.1.0
2017-05-23 08:29:55 +02:00
pyup-bot
7eaab3e38b Update pytest from 3.0.7 to 3.1.0 2017-05-23 02:25:48 +02:00
Julien Duponchelle
0927a2a8c9 Fix error display when loading a .png custom symbol 2017-05-22 13:38:04 +02:00
Julien Duponchelle
87e6159ff6 Fix a crash in the progress dialog
Fix #2064
2017-05-22 09:56:31 +02:00
Julien Duponchelle
effdcf5e24 Fix a race condition when exporting a closed project
Fix #2078
2017-05-22 09:54:17 +02:00
Julien Duponchelle
021cdd2e65 Fix RuntimeError: wrapped C/C++ object of type NodeItem has been deleted
Fix #2070
2017-05-19 17:39:34 +02:00
Julien Duponchelle
9b559d43be Fix duplicate node in node view
Fix #2004
2017-05-19 17:22:29 +02:00
grossmj
ad7d06ef21 Fixes typo. 2017-05-19 00:06:50 +02:00
Julien Duponchelle
b88bf71be9 Clean IOU code
Ref https://github.com/GNS3/gns3-gui/issues/2065
2017-05-18 17:13:11 +02:00
Julien Duponchelle
3b019edc82 Avoid an error when downloading symbols not available 2017-05-16 11:22:06 +02:00
grossmj
f3504809ed Bring VirtualBox and VMware VM window to front on Windows. Ref #847. 2017-05-16 11:14:53 +02:00
Julien Duponchelle
23735f35ad Rename linked_base to linked_clone
Ref https://github.com/GNS3/gns3-server/issues/1034
2017-05-16 10:28:11 +02:00
Julien Duponchelle
3adc46fbe2 Merge branch '2.0' into 2.1 2017-05-16 09:31:28 +02:00
Julien Duponchelle
363c4a9966 2.0.2dev1 2017-05-16 09:15:18 +02:00
Julien Duponchelle
8a303e4563 Merge branch '2.0' into 2.1 2017-05-16 08:38:43 +02:00
Julien Duponchelle
842ad8ae26 Merge branch '2.0' into 2.1 2017-05-15 16:07:32 +02:00
Julien Duponchelle
466c349642 Remove log noise 2017-04-28 12:51:26 +02:00
Julien Duponchelle
1356fd9c69 Reduce log info noise 2017-04-27 15:46:39 +02:00
Julien Duponchelle
2d1c9444c5 Delete noise 2017-04-27 15:13:56 +02:00
Julien Duponchelle
22d7815d8e Fix can't drag VPCS to topology
Fix #2001
2017-04-27 14:28:58 +02:00
Julien Duponchelle
53487d5937 Merge branch '2.0' into 2.1 2017-04-27 10:56:41 +02:00
Julien Duponchelle
ab729d8f67 Close the program if you close the profile select dialog
Fix #1922
2017-04-26 17:05:16 +02:00
Julien Duponchelle
eb5c10de3d Support node uuid is telnet console parameter
Fix #1918
2017-04-26 16:19:59 +02:00
Julien Duponchelle
1b6d534b8e Merge branch '2.0' into 2.1 2017-04-20 10:30:58 +02:00
Julien Duponchelle
a9b5b9eda2 Merge pull request #1906 from GNS3/appliances_api
Move appliances management to the server
2017-04-12 14:36:10 +02:00
Julien Duponchelle
ce12eb86e8 Merge branch '2.0' into 2.1 2017-03-30 10:07:55 +02:00
Julien Duponchelle
d4ffbd9f97 Fix an error on Windows when loading SVG files
Fix #1913
2017-03-09 10:10:06 +01:00
Julien Duponchelle
4c01a465ac Merge branch '2.0' into 2.1 2017-03-08 18:14:32 +01:00
Julien Duponchelle
012bc1e406 Display the appliances in the application
Ref #1045
2017-03-07 18:10:15 +01:00
Julien Duponchelle
05ba772715 Remove log noise 2017-03-07 10:30:59 +01:00
Jeremy Grossmann
9ea57f511b Merge pull request #1830 from GNS3/applicance_in_nodes
Display the appliances in the application
2017-03-06 19:49:48 -07:00
grossmj
5aaa2d7280 Some tweaks for appliance wizard. 2017-03-07 08:40:56 +08:00
Julien Duponchelle
497eb19369 Fix a resize notifications dialog for the first notification 2017-02-28 17:42:23 +01:00
Julien Duponchelle
70049aa877 Merge branch '2.1' into applicance_in_nodes 2017-02-28 15:59:52 +01:00
Julien Duponchelle
ece7930cb1 Merge branch '2.0' into 2.1 2017-02-28 15:59:29 +01:00
Julien Duponchelle
c7df589857 Fix noisy dialog and an error with right click 2017-02-28 15:37:15 +01:00
Julien Duponchelle
8bcc92f319 Merge branch '2.1' into applicance_in_nodes 2017-02-28 15:03:28 +01:00
Julien Duponchelle
dedde63b60 Merge branch '2.0' into 2.1 2017-02-28 14:10:07 +01:00
Jeremy Grossmann
a81d1443f9 Merge pull request #1835 from GNS3/base_config_server_side
Manage base configuration on server
2017-02-19 22:59:27 -08:00
Jeremy Grossmann
e69089f4cf Merge pull request #1865 from GNS3/status_bar_error
Display a count of errors at the bottom of the screen
2017-02-18 05:53:27 -08:00
Julien Duponchelle
0c052542b3 Display a count of errors at the bottom of the screen
You can test it by typing in the console:
log error test
log warning test

Click on the count hide / show the console and reset the counters.

Also now status bar is a dedicated class we can easyly extend it.

Ref #1864
2017-02-17 16:24:58 +01:00
Julien Duponchelle
00e402f28c Merge pull request #1814 from GNS3/show_error
Display an overlay popup with log messages
2017-02-17 11:20:37 +01:00
Julien Duponchelle
0742b282a3 Change some log level to avoid notifications noises 2017-02-17 11:09:18 +01:00
Jeremy Grossmann
2b588aa0bf Merge pull request #1820 from GNS3/line
Allow drawing lines
2017-02-17 02:08:36 -08:00
Julien Duponchelle
92fb8418ab Add a checkbox to display or not notification in app 2017-02-17 11:08:31 +01:00
Julien Duponchelle
9c7dbc864e Display an overlay popup with log messages
Ref #1334
2017-02-17 11:08:31 +01:00
Julien Duponchelle
25aebaa46c Better support for Vertical Line 2017-02-17 11:01:57 +01:00
Jeremy Grossmann
0e30b3cf5f Merge pull request #1842 from GNS3/qemu_more_adapters
Allow up to 275 adapters for qemu
2017-02-17 01:41:24 -08:00
Julien Duponchelle
755667c4d5 Spawn line at correct position 2017-02-17 10:37:27 +01:00
grossmj
16dbdf70d9 Add line icons 2017-02-17 17:17:05 +08:00
Jeremy Grossmann
806c7479ee Merge pull request #1831 from GNS3/scale_percent
Display zoom percentage when changing scale.
2017-02-17 00:25:04 -08:00
Jeremy Grossmann
cc8b84725a Update graphics_view.py 2017-02-17 16:24:46 +08:00
Julien Duponchelle
e01701614e Remember last appliance filter 2017-02-16 17:21:42 +01:00
Julien Duponchelle
efaffac801 Merge branch '2.0' into 2.1 2017-02-16 16:28:21 +01:00
Julien Duponchelle
c58366e9cb When an appliance template is added we hide it 2017-02-10 15:55:48 +01:00
Julien Duponchelle
068ebcdea0 "/appliances" => "/appliances/templates" 2017-02-10 15:55:47 +01:00
Julien Duponchelle
51f2b4bfa8 Display the appliances in the application
Ref #1045
2017-02-10 15:55:47 +01:00
Julien Duponchelle
168e4ab86e Merge branch '2.0' into 2.1 2017-02-10 15:55:17 +01:00
Julien Duponchelle
7ae18ff82a Allow up to 275 adapters for qemu
See https://github.com/GNS3/gns3-server/pull/895 for server part
2017-02-07 17:37:10 +01:00
Julien Duponchelle
c694173f9d Merge branch '2.0' into 2.1 2017-02-07 17:36:45 +01:00
Julien Duponchelle
b58b92c9f0 Merge branch '2.0' into 2.1 2017-02-07 15:03:44 +01:00
Julien Duponchelle
3ddb2e70d4 Manage base configuration on server
Fix #786
2017-02-03 13:18:04 +01:00
Julien Duponchelle
05966a9119 Display zoom percentage when changing scale.
It's like other messages display during 2 seconds.

Fix #1263
2017-02-01 14:51:58 +01:00
Julien Duponchelle
8ea24e9920 Remove unused variables 2017-02-01 14:32:05 +01:00
Julien Duponchelle
47f34fd5af Merge branch '2.0' into 2.1 2017-01-31 17:00:40 +01:00
Julien Duponchelle
89321a6cad Allow drawing lines
Ref #997
2017-01-27 10:15:05 +01:00
Julien Duponchelle
6690ba7108 2.1.0dev1 2017-01-24 10:38:13 +01:00
656 changed files with 142551 additions and 147593 deletions

View File

@@ -0,0 +1,34 @@
---
name: GNS3 bug report
about: Create a report to help us fix a bug
title: 'Short description of the bug'
labels: Bug
assignees: ''
---
**Before you start**
Please open an issue only if you suspect there is a bug or any problem with GNS3. Go to https://gns3.com/community for any other questions or for requesting help with GNS3.
You may also post this issue directly on the GNS3 server repository if you know the bug comes from the server: https://github.com/GNS3/gns3-server/issues/new
**Describe the bug**
Please provide a clear and detailed description of what the bug is.
**GNS3 version and operating system (please complete the following information):**
- OS: [e.g. Windows, Linux or macOS]
- GNS3 version [e.g. 2.1.14]
- Any use of the GNS3 VM or remote server (ESXi, bare metal etc.)
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Screenshots or videos**
If applicable, add screenshots (e.g. of the topology and/or error message) or links to videos to help explain the problem. This will help us a lot to quickly find the bug and fix it.
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,10 @@
---
name: GNS3 development
about: Any question or discussion regarding GNS3 development
title: ''
labels: ''
assignees: ''
---

View File

@@ -0,0 +1,25 @@
---
name: GNS3 feature request
about: Suggest an idea for GNS3
title: 'Short description of the feature request'
labels: Enhancement
assignees: ''
---
**Before you start**
Please check if a similar feature request has already been submitted.
You may also post this issue directly on the GNS3 server repository if you know the feature request only applies to the server: https://github.com/GNS3/gns3-server/issues/new
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen. If applicable, please provide screenshots
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

3
.gitignore vendored
View File

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

View File

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

View File

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

522
CHANGELOG
View File

@@ -1,5 +1,527 @@
# Change Log
## 2.2.0rc5 09/09/2019
* Adjust size for setup dialog and remove question about running the wizard again. Ref #2846
## 2.2.0rc4 30/08/2019
* Fix issue when asking to run the setup wizard again. Ref #2846
* Remove warning about VirtualBox not supporting nested virtualization. Ref https://github.com/GNS3/gns3-server/issues/1610
* Ask user if they want to see the wizard again. Ref #2846
## 2.2.0rc3 12/08/2019
* Revert to jsonschema 2.6.0 due to packaging problem.
## 2.2.0rc2 10/08/2019
* Bump jsonschema to version 3.0.2
* Fix "Unable to change Remote Main Server IP". Fixes #2823
* Fix "AttributeError: 'QGraphicsTextItem' object has no attribute 'locked'". Fixes #2814
* Fix a minor typo in the setup wizard
## 2.2.0b4 11/07/2019
* Fix issue preventing to open the QFileDialog in the correct directory.
* Remove unused edit readme action. Fixes #2816
* Remove deprecated Qemu parameter to run legacy ASA VMs. Fixes #2827
* Upload images on remote controller. Fixes #2828
* Preferences dialog: send API request only if connected to controller
* Fix AttributeError: 'QGraphicsTextItem' object has no attribute 'locked'. Fixes #2814
* Fix KeyError: 'chassis' when converting old IOS templates. Fixes #2813
## 2.2.0b3 15/06/2019
* Fix template migration issues from GUI to controller. Fixes https://github.com/GNS3/gns3-gui/issues/2803
* %guest-cid% variable implementation for Qemu VMs. Fixes https://github.com/GNS3/gns3-gui/issues/2804
* Increase timeout from 2 to 5 seconds for synchronous check. Ref #2805
## 2.2.0b2 29/05/2019
* Fix KeyError: 'endpoint' issue. Fixes #2802
* Fix wrong aligment of symbols in saved/exported projects. Fixes #2800
* Replace urllib.request by Qt implementation for local server synchronous check. Fixes #2793
* Support snapshots for portable projects. Fixes https://github.com/GNS3/gns3-gui/issues/2792
* Fix event notification problem for projects and how snapshots are restored.
* Do not close the nodes dock widget when creating project.
* Fix no scan for images on remote controller. Fixes #2799
* Use QNetworkAccessManager to download custom appliance symbols.
* Experimental auto upgrade should not be available for "frozen" app. Fixes #2797
* Don't allow link labels to be moved for locked nodes. Fixes #2794
* 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
## 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.2.0b1 21/05/2019
* Change behavior when an IOU license is verified. Fixes https://github.com/GNS3/gns3-server/issues/1555
* Fix cannot load new profile. Fixes #2784
* Fix remote packet capture when controller is also remote. Fixes #2785
* Set console type to "none" by default for Ethernet switches and add a warning if trying to use "telnet". Fixes https://github.com/GNS3/gns3-gui/issues/2776
* Add tooltip for symbol theme support in general preferences. Fixes #2770
* Support for persistent docker volumes
## 2.1.17 17/05/2019
* No changes.
## 2.2.0a5 15/04/2019
* Revert "Drop old Qemu support (Windows and macOS) and legacy ASA support." Ref https://github.com/GNS3/gns3-server/issues/1579
* Do not make NPF or NPCAP service mandatory to start the local server on Windows.
* Do not try to upload a local image that is already installed on the local server.
* Back to the major.minor version for config files. Ref https://github.com/GNS3/gns3-gui/issues/2756
* Some adjustments with compute WebSocket handling. Ref https://github.com/GNS3/gns3-server/issues/1564
* Fix AttributeError: 'GraphicsView' object has no attribute '_import_config_dir'. Fixes #2768
* Do not try to lock a SvgIconItem. Fixes #2766
* Prevent locked nodes to be deleted. Fixes https://github.com/GNS3/gns3-gui/issues/2764
* Add PuTTY 0.71 and mark GNS3 PuTTY as deprecated. Fixes #2758
* Fix bug with IOS platform detection. Fixes #2760
## 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.2.0a4 05/04/2019
* Use the full version number for path to config files. Ref https://github.com/GNS3/gns3-gui/issues/2756
* Fix error message when shutting down GUI without a started server.
* Fix remote packet capture and make sure packet capture is stopped when deleting an NIO. Fixes https://github.com/GNS3/gns3-gui/issues/2753
* Store config files in version specific location
* Update pytest from 4.3.1 to 4.4.0
* Fix error messages on closing GNS3 application. Fixes https://github.com/GNS3/gns3-gui/issues/2750
* Fix bug when list of files for an appliance is not displayed.
* Update 'local' to 'bundled' in server & gui, Fixes: #1561
## 2.2.0a3 25/03/2019
* Fix bug when changing symbol. Fixes #2740
* Fix issue when images are not uploaded from appliance wizard. Ref https://github.com/GNS3/gns3-gui/issues/2738
## 2.2.0a2 14/03/2019
* Try to handle stacked widget layout differently. Ref #2605
* Early support for symbol themes.
* Download custom appliance symbols from GitHub Fix symbol cache issue. Ref https://github.com/GNS3/gns3-gui/issues/2671 Fix temporary directory for symbols was not deleted Fix temporary appliance file was not deleted
* New export project wizard.
* Update paths for binaries moved to the MacOS directory in GNS3.app
* Prevent to change layer position for locked items. Ref #2679
* Display available appliances in a hierarchical folder structure. Fixes #2702
* Handle locking/unlocking items independently from the layer position.
* Better description to why an appliance cannot be installed.
* Force jsonschema dependency to 2.6.0
* Fix broken idle-pc support. Fixes #1515
## 2.2.0a1 29/01/2019
* Fix default NAT interface not restored on Windows. Fixes #2681
* Merge and improvements to the setup wizard. Fixes #2676.
* Adjust the setup wizard (VMware image size, layouts).
* Refactor appliance wizard.
* Natural sorting support for custom adapters tree widget.
* Add the word "template" to configuration dialog titles.
* Reorder node contextual menu.
* Option to limit the size of node symbols (activated by default). Ref #2674.
* Resize SVG node symbol only when height is above 80px. Ref #2674 Work on str instead of binary when resizing SVG symbol.
* Automatically resize SVG symbols that are too big. Ref #2674.
* Bigger new template wizard.
* Fix DeprecationWarning: invalid escape sequence. Fixes https://github.com/GNS3/gns3-gui/issues/2670
* Use theme icons in other contextual menus. Fixes #2669
* Change some text regarding appliance installation.
* Handle errors when creating template from appliance.
* Template creation from an appliance.
* Basic support to create new template from appliance.
* Fix race condition when trying to automatically open a console and the project is already running. Fixes #1493.
* Fix issue with IOS c7200 templates and usage variable.
* Add usage instructions to node tooltip. Ref #2662.
* Smaller node info dialog.
* New node information dialog to display general, usage and command line information. Ref https://github.com/GNS3/gns3-gui/issues/2662 https://github.com/GNS3/gns3-gui/issues/2656
* Support "usage" field for Dynamips, IOU, VirtualBox and VMware. Fixes https://github.com/GNS3/gns3-gui/issues/2657
* Add "new template" entry to File menu. Fixes #2658
* Fix bug with filter in add template. Fixes #2651.
* Fix missing method '_newApplianceActionSlot'. Fixes #2643.
* Use "template" to name what we use to create new nodes.
* Use project instead of topology where appropriate.
* Make sure nothing is named "compute server".
* Use "node" instead of "appliance" for grid support.
* Support for differing grid sizes for appliances and drawings. Requires corresponding commit on gns3-server.
* New projects can be created with show grid/snap to grid.
* Disallow changing layer of a locked object. Ref #2513.
* Cosmetic changes regarding appliances.
* Fix issue when duplicating an appliance on GUI side.
* Fix issue to access configuration pages for Ethernet switch and hub appliances.
* Fix small bugs when using the new appliance management API.
* Fix bug with custom adapters and categories for Docker VM. Fixes https://github.com/GNS3/gns3-gui/issues/2613
* Fix bug with categories with Docker appliances.
* Schema validation for appliance API. Ref #1427.
* Remove generic controller settings API endpoint.
* Fix conflict between the two websocket streams (project & controller).
* Fix platform.linux_distribution() is deprecated. Fixes https://github.com/GNS3/gns3-gui/issues/2578
* Allow multiple appliances to be installed. Ref #2490
* Add more information about appliance templates.
* New appliance wizard to install an appliance from different sources. Ref #2490
* Redesign appliance handling part 1. Ref #2490 - Removed appliance templates from device dock - Use new controller notification stream - Fixed device update and remove from device dock
* Fix "Network session error" issues. Fixes #2560.
* Set default layer for newly created nodes to 1 and 2 for all other drawings. Ref #2513.
* Deactivate TraceNG module
* Main menu actions to WebUI and Light Web Interface
* Enable TraceNG module
* Add Solar-Putty command line. Ref #2519.
* Fix issues when locking/unlocking items. Ref #2513.
* Fix tests for default note font/color.
* Console support for clouds (to connect to external devices or services). Fixes #2500.
* Fix LabelItem tests.
* Separate appliance font from note font. Fixes #2477.
* Do not include spaces in link description (%d replacement) for packet analyzer command. Ref #2485.
* Fix error when trying to open project. Fixes #2508
* Launch packet capture analyzer command without creating pipe.
* Streamline appliance wizard. Fixes #2224.
* Fix "Node list view not updated when renaming or deleting appliance template". Fixes #2356.
* Automatically resize the Custom adapters configuration dialog. Fixes #2467.
* Change size of custom adapters configuration dialog. Ref #2467.
* Improve node tooltips. Fixes #2462.
* Do not activate "console auto start" by default. Ref #1910.
* Support for console auto start. Fixes #1910
* Add custom_adapters setting support for appliance files. Ref #2361.
* Possibility to customize port names and adapter types for Qemu, VirtualBox, VMware and Docker. Fixes #2361. MAC addresses can customized for Qemu as well.
* Allow to have the projects with the same name in different locations. Fixes #2380.
* Save state feature for VirtualBox and VMware. New "On close" setting to select the action to execute when closing/stopping a Qemu/VirtualBox/VMware VM.
* Support for suspend to disk / resume (Qemu). Ref #725.
* Fix bug with 'none' console type for Ethernet switch. Fix some tests related to traceng.
* Allow to resize a Qemu VM disk (extend only). Ref #2382.
* Allow to select the default NAT interface in preferences for local server.
* Fix missing lock and unlock icons in resources.
* Consistent icon styles for contextual menu. Fixes #1272.
* Spice with agent support for Qemu VMs. Fixes #2355.
* Fix zoom-in zoom-out step values. Ref #2457.
* Support for console type "none" for all VMs. Fixes #2452.
* Allow to copy Dynamips, IOU, Qemu and Docker templates in preferences. Fixes #2451.
* Support for none console type (Qemu & Docker only)
* Support Qemu with HAXM acceleration.
* Use PyQt 5.10 and change AV build to use MSVS2017
* PyQt5.10 support, Ref. #2434
* Allow to accept a different md5 hash than the one in the appliance file. Ref. server#1246
* Critical information during upload file with different md5, Ref. #1246
* Restore locked item state.
* Bump to version 2.2.0dev1 & refresh resources/ui files.
* Have the contextual menu use icons from the active style. Ref #1272.
* Individually lock or unlock an item on the scene. Fixes #1228.
* Improve lock and unlock all items so some actions can still be performed on objects. Fixes #1134.
* Lock or unlock all items button. Fixes #1134.
* Move console to all devices icon after the separation bar. Ref #1272
* Lock icons. Ref #1134.
## 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
* Fix RecursionError
* Fix 'NodesDockWidget' object has no attribute 'loadPath'
* Fix 'MainWindow' object has no attribute '_settings
* Fix object has no attribute 'warning_signal'
* Fix timeout issues when using an appliance
* Make sure ubridge path is not a directory
## 2.1.0a1 24/07/2017
* Packet filtering
* Suspend a link
* Duplicate a node
* Move config to central server
* Appliance templates on server
## 2.0.3 13/06/2017
* Display error when we can't export files
* Fix auth header not sent is some conditions
* If we have auth issue at server startup continue to get better error
* Do not override IOU configuration file when you change the image
* Fix some PNG loading issues on Windows
* Handle label with missing elements
* Support floating value for font size
* Handle partial json in a response
* Add Dominik as a new team member
## 2.0.2 30/05/2017
* Show a default symbol in case of corrupted file
* When another gui is already running exit instead of proper close to avoid any issue
* Fix duplicate on remote server use wrong location
* Display the location of settings when we disallow opening due to old release
* Improve search for dynamips in development on OSX
* Fix error display when loading a .png custom symbol
* Fix a crash in the progress dialog
* Fix a race condition when exporting a closed project
* Fix RuntimeError: wrapped C/C++ object of type NodeItem has been deleted
## 2.0.1 16/05/2017
* Improve inline help. Fixes #1999. Add a warning about wifi interfaces in the cloud. Fixes #1902.

View File

@@ -1,12 +1,12 @@
# Run tests inside a container
FROM ubuntu:vivid
FROM ubuntu:18.04
MAINTAINER GNS3 Team
#ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y --force-yes python3.4 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3.4-dev xvfb
RUN apt-get install -y --force-yes python3.6 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.6-dev xvfb
RUN apt-get clean
@@ -19,4 +19,4 @@ ADD . /src
WORKDIR /src
CMD xvfb-run python3.4 -m pytest -vv
CMD xvfb-run python3.6 -m pytest -vv

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
version: '{build}-{branch}'
image: Visual Studio 2015
image: Visual Studio 2017
platform: x64

View File

@@ -1,6 +1,6 @@
-rrequirements.txt
pep8==1.7.0
pytest==3.0.7
pytest-pythonpath==0.7.1 # useful for running tests outside tox
pytest-timeout==1.2.0
pytest==4.4.1
pytest-pythonpath==0.7.3 # useful for running tests outside tox
pytest-timeout==1.3.3

93
gns3/appliance_manager.py Normal file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .qt import QtCore
from .controller import Controller
from .local_config import LocalConfig
from .settings import GENERAL_SETTINGS
import logging
log = logging.getLogger(__name__)
class ApplianceManager(QtCore.QObject):
"""
Manager for appliances.
"""
appliances_changed_signal = QtCore.Signal()
def __init__(self):
super().__init__()
self._appliances = []
self._controller = Controller.instance()
self._controller.connected_signal.connect(self.refresh)
self._controller.disconnected_signal.connect(self._controllerDisconnectedSlot)
def refresh(self, update=False):
"""
Gets the appliances from the controller.
"""
if self._controller.connected():
settings = LocalConfig.instance().loadSectionSettings("MainWindow", GENERAL_SETTINGS)
symbol_theme = settings["symbol_theme"]
if update is True:
self._controller.get("/appliances?update=yes&symbol_theme={}".format(symbol_theme), self._listAppliancesCallback, progressText="Downloading appliances from online registry...")
else:
self._controller.get("/appliances?symbol_theme={}".format(symbol_theme), self._listAppliancesCallback)
def _controllerDisconnectedSlot(self):
"""
Called when the controller has been disconnected.
"""
self._appliances = []
self.appliances_changed_signal.emit()
def appliances(self):
"""
Returns the appliances.
:returns: array of appliances
"""
return self._appliances
def _listAppliancesCallback(self, result, error=False, **kwargs):
"""
Callback to get the appliances.
"""
if error is True:
log.error("Error while getting appliances list: {}".format(result.get("message", "unknown")))
return
self._appliances = result
self.appliances_changed_signal.emit()
@staticmethod
def instance():
"""
Singleton to return only on instance of ApplianceManager.
:returns: instance of ApplianceManager
"""
if not hasattr(ApplianceManager, '_instance') or ApplianceManager._instance is None:
ApplianceManager._instance = ApplianceManager()
return ApplianceManager._instance

View File

@@ -22,13 +22,11 @@ Base class for node classes.
from .qt import QtCore
from .ports.port import Port
import logging
log = logging.getLogger(__name__)
class BaseNode(QtCore.QObject):
"""
BaseNode implementation.
@@ -49,7 +47,6 @@ class BaseNode(QtCore.QObject):
server_error_signal = QtCore.Signal(int, str)
_instance_count = 1
_allocated_names = set()
# node statuses
stopped = 0
@@ -57,14 +54,15 @@ class BaseNode(QtCore.QObject):
suspended = 2
# node categories
routers = 0
switches = 1
end_devices = 2
security_devices = 3
routers = "router"
switches = "switch"
end_devices = "guest"
security_devices = "firewall"
def __init__(self, module, compute, project):
super().__init__()
# create an unique ID
self._id = BaseNode._instance_count
BaseNode._instance_count += 1
@@ -81,19 +79,48 @@ class BaseNode(QtCore.QObject):
def links(self):
"""
Links connected to the node
Links connected to this node
"""
return self._links
def addLink(self, link):
"""
Add a link connected to this node
:param link: link object
"""
self._links.add(link)
def deleteLink(self, link):
"""
Delete a link connected to this node
:param link: link object
"""
try:
self._links.remove(link)
except KeyError:
pass
def state(self):
"""
Returns a human readable status of this node.
:returns: string
"""
status = self.status()
if status == self.started:
return "started"
elif status == self.stopped:
return "stopped"
elif status == self.suspended:
return "suspended"
return "unknown"
@classmethod
def reset(cls):
"""
@@ -176,19 +203,16 @@ class BaseNode(QtCore.QObject):
# set ports as started
port.setStatus(Port.started)
self.started_signal.emit()
log.info("{} has started".format(self.name()))
elif status == self.stopped:
for port in self._ports:
# set ports as stopped
port.setStatus(Port.stopped)
self.stopped_signal.emit()
log.info("{} has stopped".format(self.name()))
elif status == self.suspended:
for port in self._ports:
# set ports as suspended
port.setStatus(Port.suspended)
self.suspended_signal.emit()
log.info("{} has suspended".format(self.name()))
def initialized(self):
"""
@@ -227,59 +251,6 @@ class BaseNode(QtCore.QObject):
return self._ports
@staticmethod
def defaultCategories():
"""
Returns the default categories.
:returns: dict
"""
categories = {"Routers": BaseNode.routers,
"Switches": BaseNode.switches,
"End devices": BaseNode.end_devices,
"Security devices": BaseNode.security_devices}
return categories
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this node.
Must be overloaded.
:returns: symbol path (or resource).
"""
raise NotImplementedError()
@staticmethod
def symbolName():
"""
Returns the symbol name (for the nodes view).
:returns: name (string)
"""
raise NotImplementedError()
@staticmethod
def categories(self):
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node category (integer)
"""
raise NotImplementedError()
def __str__(self):
"""
Must be overloaded.
"""
raise NotImplementedError()
def controllerHttpPost(self, path, callback, body={}, context={}, **kwargs):
"""
POST on current server / project
@@ -326,3 +297,46 @@ class BaseNode(QtCore.QObject):
"""
self._project.delete(path, callback, context=context, **kwargs)
@staticmethod
def defaultCategories():
"""
Returns the default categories.
:returns: dict
"""
categories = {"Routers": BaseNode.routers,
"Switches": BaseNode.switches,
"End devices": BaseNode.end_devices,
"Security devices": BaseNode.security_devices}
return categories
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this node.
Must be overloaded.
:returns: symbol path (or resource).
"""
raise NotImplementedError()
@staticmethod
def categories(self):
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node category (integer)
"""
raise NotImplementedError()
def __str__(self):
"""
Must be overloaded.
"""
raise NotImplementedError()

View File

@@ -20,10 +20,11 @@ import uuid
class Compute:
"""
A compute node on the remote server
An instance of a compute.
"""
def __init__(self, compute_id=None):
if compute_id is None:
compute_id = str(uuid.uuid4())
self._compute_id = compute_id
@@ -36,85 +37,211 @@ class Compute:
self._password = None
self._cpu_usage_percent = None
self._memory_usage_percent = None
self._capabilities = {
"node_types": []
}
self._capabilities = {"node_types": []}
self._last_error = None
def id(self):
"""
Returns the compute ID.
:returns: compute identifier
"""
return self._compute_id
def name(self):
"""
Returns the compute name.
:returns: compute name
"""
return self._name
def setName(self, name):
"""
Sets the compute name.
:param name: compute name
"""
self._name = name
def connected(self):
"""
Returns whether or not there is a connection to the compute.
:returns: boolean
"""
return self._connected
def setConnected(self, value):
"""
Sets whether or not there is a connection to the compute.
:param value: boolean
"""
self._connected = value
def port(self):
return self._port
def setPort(self, port):
self._port = port
def user(self):
return self._user
def setUser(self, user):
self._user = user
def setPassword(self, password):
self._password = password
def protocol(self):
return self._protocol
def setProtocol(self, protocol):
self._protocol = protocol
def host(self):
"""
Returns the compute host.
:returns: host (string)
"""
return self._host
def setHost(self, host):
"""
Sets the compute host.
:param host: host (string)
"""
self._host = host
def setCpuUsagePercent(self, usage):
self._cpu_usage_percent = usage
def port(self):
"""
Returns the compute port number.
:returns: port number (integer)
"""
return self._port
def setPort(self, port):
"""
Sets the compute port number.
:param port: port number (integer)
"""
self._port = port
def user(self):
"""
Returns the compute user for HTTP authentication.
:returns: user (string)
"""
return self._user
def setUser(self, user):
"""
Sets the compute user for HTTP authentication.
:param user: user (string)
"""
self._user = user
def setPassword(self, password):
"""
Returns the compute password for HTTP authentication.
:returns: password (string)
"""
self._password = password
def protocol(self):
"""
Returns the compute protocol.
:returns: protocol (string)
"""
return self._protocol
def setProtocol(self, protocol):
"""
Sets the compute protocol.
:param protocol: protocol (string)
"""
self._protocol = protocol
def cpuUsagePercent(self):
"""
Returns the compute CPU usage.
:returns: CPU usage (integer)
"""
return self._cpu_usage_percent
def setCpuUsagePercent(self, usage):
"""
Sets the compute CPU usage.
:param usage: CPU usage (integer)
"""
self._cpu_usage_percent = usage
def setMemoryUsagePercent(self, usage):
"""
Returns the compute memory usage.
:returns: memory usage (integer)
"""
self._memory_usage_percent = usage
def memoryUsagePercent(self):
"""
Sets the compute memory usage.
:param usage: memory usage (integer)
"""
return self._memory_usage_percent
def capabilities(self):
"""
Returns the compute capabilities
:returns: capabilities (dictionary)
"""
return self._capabilities
def setCapabilities(self, val):
self._capabilities = val
def setCapabilities(self, value):
"""
Sets the compute capabilities
:param value: capabilities (dictionary)
"""
self._capabilities = value
def setLastError(self, last_error):
self._last_error = last_error
def lastError(self):
return self._last_error
def __str__(self):
return self._compute_id
def __json__(self):
return {
"host": self._host,
"port": self._port,
"protocol": self._protocol,
"user": self._user,
"password": self._password,
"name": self._name,
"compute_id": self._compute_id
}
return {"host": self._host,
"port": self._port,
"protocol": self._protocol,
"user": self._user,
"password": self._password,
"name": self._name,
"compute_id": self._compute_id}
def __eq__(self, v):
if isinstance(v, Compute):
return self.__json__() == v.__json__()
return False

View File

@@ -16,7 +16,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .qt import QtCore
from .compute import Compute
from .controller import Controller
@@ -31,11 +30,16 @@ log = logging.getLogger(__name__)
class ComputeManager(QtCore.QObject):
"""
Manager for computes.
"""
created_signal = QtCore.Signal(str)
updated_signal = QtCore.Signal(str)
deleted_signal = QtCore.Signal(str)
def __init__(self):
super().__init__()
self._computes = {}
self._controller = Controller.instance()
@@ -43,7 +47,7 @@ class ComputeManager(QtCore.QObject):
self._controller.disconnected_signal.connect(self._controllerDisconnectedSlot)
self._controllerConnectedSlot()
# If we receive fresh data from the notification feed no need to refresh via an API call
# No need to refresh via an API call if we received fresh data from the notification feed
self._last_computes_refresh = datetime.datetime.now().timestamp()
self._timer = QtCore.QTimer()
@@ -53,24 +57,40 @@ class ComputeManager(QtCore.QObject):
self._timer.start()
def _refreshComputesSlot(self):
"""
Called when computes are refreshed.
"""
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):
"""
Called when connected to a compute.
"""
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):
"""
Called when disconnected from a compute.
"""
for compute_id in list(self._computes):
del self._computes[compute_id]
self.deleted_signal.emit(compute_id)
def _listComputesCallback(self, result, error=False, **kwargs):
"""
Callback to list computes.
"""
self._refreshingComputes = False
if error is True:
log.error("Error while getting compute list: {}".format(result["message"]))
@@ -81,9 +101,9 @@ class ComputeManager(QtCore.QObject):
def computeDataReceivedCallback(self, compute):
"""
Called when we received data from a compute
node.
Called when we received data from a compute node.
"""
self._last_computes_refresh = datetime.datetime.now().timestamp()
new_node = False
@@ -101,6 +121,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)
@@ -109,8 +130,9 @@ class ComputeManager(QtCore.QObject):
def computeIsTheRemoteGNS3VM(self, compute):
"""
:returns: Boolean True if the remote server is the remote GNS3 VM
:returns: boolean True if the remote server is the remote GNS3 VM
"""
if compute.id() != "local" and compute.id() != "vm":
if self.vmCompute() and "GNS3 VM ({})".format(compute.name()) == self.vmCompute().name():
return True
@@ -120,6 +142,7 @@ class ComputeManager(QtCore.QObject):
"""
:returns: List of computes nodes
"""
computes = []
for compute in self._computes.values():
# We filter the remote GNS3 VM compute from the computes list
@@ -131,6 +154,7 @@ class ComputeManager(QtCore.QObject):
"""
:returns: The GNS3 VM compute node or None
"""
try:
return self._computes["vm"]
except KeyError:
@@ -140,6 +164,7 @@ class ComputeManager(QtCore.QObject):
"""
:returns: The local compute node or None
"""
try:
return self._computes["local"]
except KeyError:
@@ -151,6 +176,7 @@ class ComputeManager(QtCore.QObject):
With a remote controller it could be different of our local platform
"""
c = self.localCompute()
if c is None:
return sys.platform
@@ -160,31 +186,45 @@ class ComputeManager(QtCore.QObject):
"""
:returns: List of non local and non VM computes
"""
return [c for c in self._computes.values() if c.id() != "local" and c.id() != "vm"]
def getCompute(self, compute_id):
"""
Gets a compute by ID
:param compute_id: compute identifier
:returns: compute
"""
if compute_id.startswith("http:") or compute_id.startswith("https:"):
u = urllib.parse.urlsplit(compute_id)
for compute in self._computes.values():
if "{}:{}".format(compute.host(), compute.port()) == u.netloc:
return compute
raise KeyError("Compute {} is missing.".format(compute_id))
raise KeyError("Compute ID {} is missing.".format(compute_id))
if compute_id not in self._computes:
self._computes[compute_id] = Compute(compute_id)
self.created_signal.emit(compute_id)
return self._computes[compute_id]
def deleteCompute(self, compute_id):
"""
Deletes a compute by ID
:param compute_id: compute identifier
"""
if compute_id in self._computes:
compute = self._computes[compute_id]
del self._computes[compute_id]
self._controller.delete("/computes/" + compute_id, None)
self.deleted_signal.emit(compute_id)
self._controller.delete("/computes/{compute_id}".format(compute_id=compute_id), None)
self.deleted_signal.emit(compute_id)
def updateList(self, computes):
"""
Sync an array of compute server with remote
Sync an array of compute with remote
"""
for compute_id in copy.copy(self._computes):
# Delete compute on controller not in the new computes
if compute_id in ["local", "vm"]:

View File

@@ -19,17 +19,16 @@
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__)
class ComputeItem(QtWidgets.QTreeWidgetItem):
"""
Custom item for the QTreeWidget instance
(topology summary view).
@@ -62,25 +61,47 @@ 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):
"""
Compute summary view implementation.
@@ -90,9 +111,7 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
def __init__(self, parent):
super().__init__(parent)
self._computes = {}
ComputeManager.instance().created_signal.connect(self._computeAddedSlot)
ComputeManager.instance().updated_signal.connect(self._computeUpdatedSlot)
ComputeManager.instance().deleted_signal.connect(self._computeRemovedSlot)

View File

@@ -1,26 +0,0 @@
!
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname %h
!
ip cef
no ip domain-lookup
no ip icmp rate-limit unreachable
ip tcp synwait 5
no cdp log mismatch duplex
!
line con 0
exec-timeout 0 0
logging synchronous
privilege level 15
no login
line aux 0
exec-timeout 0 0
logging synchronous
privilege level 15
no login
!
!
end

View File

@@ -1,181 +0,0 @@
!
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
no service dhcp
!
hostname %h
!
ip cef
no ip routing
no ip domain-lookup
no ip icmp rate-limit unreachable
ip tcp synwait 5
no cdp log mismatch duplex
vtp file nvram:vlan.dat
!
!
interface FastEthernet0/0
description *** Unused for Layer2 EtherSwitch ***
no ip address
shutdown
!
interface FastEthernet0/1
description *** Unused for Layer2 EtherSwitch ***
no ip address
shutdown
!
interface FastEthernet1/0
no shutdown
duplex full
speed 100
!
interface FastEthernet1/1
no shutdown
duplex full
speed 100
!
interface FastEthernet1/2
no shutdown
duplex full
speed 100
!
interface FastEthernet1/3
no shutdown
duplex full
speed 100
!
interface FastEthernet1/4
no shutdown
duplex full
speed 100
!
interface FastEthernet1/5
no shutdown
duplex full
speed 100
!
interface FastEthernet1/6
no shutdown
duplex full
speed 100
!
interface FastEthernet1/7
no shutdown
duplex full
speed 100
!
interface FastEthernet1/8
no shutdown
duplex full
speed 100
!
interface FastEthernet1/9
no shutdown
duplex full
speed 100
!
interface FastEthernet1/10
no shutdown
duplex full
speed 100
!
interface FastEthernet1/11
no shutdown
duplex full
speed 100
!
interface FastEthernet1/12
no shutdown
duplex full
speed 100
!
interface FastEthernet1/13
no shutdown
duplex full
speed 100
!
interface FastEthernet1/14
no shutdown
duplex full
speed 100
!
interface FastEthernet1/15
no shutdown
duplex full
speed 100
!
interface Vlan1
no ip address
shutdown
!
!
line con 0
exec-timeout 0 0
logging synchronous
privilege level 15
no login
line aux 0
exec-timeout 0 0
logging synchronous
privilege level 15
no login
!
!
banner exec $
***************************************************************
This is a normal Router with a SW module inside (NM-16ESW)
It has been preconfigured with hard coded speed and duplex
To create vlans use the command "vlan database" from exec mode
After creating all desired vlans use "exit" to apply the config
To view existing vlans use the command "show vlan-switch brief"
Warning: You are using an old IOS image for this router.
Please update the IOS to enable the "macro" command!
***************************************************************
$
!
!Warning: If the IOS is old and doesn't support macro, it will stop the configuration loading from this point!
!
macro name add_vlan
end
vlan database
vlan $v
exit
@
macro name del_vlan
end
vlan database
no vlan $v
exit
@
!
!
banner exec $
***************************************************************
This is a normal Router with a Switch module inside (NM-16ESW)
It has been pre-configured with hard-coded speed and duplex
To create vlans use the command "vlan database" in exec mode
After creating all desired vlans use "exit" to apply the config
To view existing vlans use the command "show vlan-switch brief"
Alias(exec) : vl - "show vlan-switch brief" command
Alias(configure): va X - macro to add vlan X
Alias(configure): vd X - macro to delete vlan X
***************************************************************
$
!
alias configure va macro global trace add_vlan $v
alias configure vd macro global trace del_vlan $v
alias exec vl show vlan-switch brief
!
!
end

View File

@@ -1,132 +0,0 @@
!
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname %h
!
!
!
logging discriminator EXCESS severity drops 6 msg-body drops EXCESSCOLL
logging buffered 50000
logging console discriminator EXCESS
!
no ip icmp rate-limit unreachable
!
ip cef
no ip domain-lookup
!
!
!
!
!
!
ip tcp synwait-time 5
!
!
!
!
!
!
interface Ethernet0/0
no ip address
no shutdown
duplex auto
!
interface Ethernet0/1
no ip address
no shutdown
duplex auto
!
interface Ethernet0/2
no ip address
no shutdown
duplex auto
!
interface Ethernet0/3
no ip address
no shutdown
duplex auto
!
interface Ethernet1/0
no ip address
no shutdown
duplex auto
!
interface Ethernet1/1
no ip address
no shutdown
duplex auto
!
interface Ethernet1/2
no ip address
no shutdown
duplex auto
!
interface Ethernet1/3
no ip address
no shutdown
duplex auto
!
interface Ethernet2/0
no ip address
no shutdown
duplex auto
!
interface Ethernet2/1
no ip address
no shutdown
duplex auto
!
interface Ethernet2/2
no ip address
no shutdown
duplex auto
!
interface Ethernet2/3
no ip address
no shutdown
duplex auto
!
interface Ethernet3/0
no ip address
no shutdown
duplex auto
!
interface Ethernet3/1
no ip address
no shutdown
duplex auto
!
interface Ethernet3/2
no ip address
no shutdown
duplex auto
!
interface Ethernet3/3
no ip address
no shutdown
duplex auto
!
interface Vlan1
no ip address
shutdown
!
!
!
!
!
!
!
!
!
line con 0
exec-timeout 0 0
privilege level 15
logging synchronous
line aux 0
exec-timeout 0 0
privilege level 15
logging synchronous
!
end

View File

@@ -1,108 +0,0 @@
!
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname %h
!
!
!
no ip icmp rate-limit unreachable
!
!
!
!
ip cef
no ip domain-lookup
!
!
ip tcp synwait-time 5
!
!
!
!
interface Ethernet0/0
no ip address
shutdown
!
interface Ethernet0/1
no ip address
shutdown
!
interface Ethernet0/2
no ip address
shutdown
!
interface Ethernet0/3
no ip address
shutdown
!
interface Ethernet1/0
no ip address
shutdown
!
interface Ethernet1/1
no ip address
shutdown
!
interface Ethernet1/2
no ip address
shutdown
!
interface Ethernet1/3
no ip address
shutdown
!
interface Serial2/0
no ip address
shutdown
serial restart-delay 0
!
interface Serial2/1
no ip address
shutdown
serial restart-delay 0
!
interface Serial2/2
no ip address
shutdown
serial restart-delay 0
!
interface Serial2/3
no ip address
shutdown
serial restart-delay 0
!
interface Serial3/0
no ip address
shutdown
serial restart-delay 0
!
interface Serial3/1
no ip address
shutdown
serial restart-delay 0
!
interface Serial3/2
no ip address
shutdown
serial restart-delay 0
!
interface Serial3/3
no ip address
shutdown
serial restart-delay 0
!
!
no cdp log mismatch duplex
!
line con 0
exec-timeout 0 0
privilege level 15
logging synchronous
line aux 0
exec-timeout 0 0
privilege level 15
logging synchronous
!
end

View File

@@ -1 +0,0 @@
set pcname %h

View File

@@ -21,15 +21,16 @@ Handles commands typed in the GNS3 console.
import sys
import cmd
import logging
import struct
import sip
import json
from .qt import sip
from .node import Node
from .qt import QtCore
from .version import __version__
import logging
log = logging.getLogger(__name__)
class ConsoleCmd(cmd.Cmd):
@@ -177,6 +178,24 @@ class ConsoleCmd(cmd.Cmd):
print("Cannot console to {}".format(device))
break
def do_log(self, args):
"""
Log a message
log level message
"""
args = args.split()
if len(args) == 0:
return
level = args.pop(0)
if level == "info":
log.info(" ".join(args))
elif level == "warning":
log.warning(" ".join(args))
else:
log.error(" ".join(args))
def _start_console(self, node):
"""
Starts a console application for a specific node.

View File

@@ -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:
@@ -110,6 +110,9 @@ class ConsoleView(PyCutExt, ConsoleCmd):
self._topology = Topology.instance()
def _writeMessageSlot(self, message, level):
"""
Write a message in the console.
"""
if level == "error":
self.write(message, error=True)
elif level == "warning":

View File

@@ -18,11 +18,14 @@
import os
import hashlib
import tempfile
import json
import pathlib
from .qt import QtCore, QtGui, QtWidgets, qpartial, qslot
from .qt import QtCore, QtNetwork, QtGui, QtWidgets, QtWebSockets, qpartial, qslot
from .symbol import Symbol
from .local_server_config import LocalServerConfig
from .settings import LOCAL_SERVER_SETTINGS
from gns3.utils import parse_version
import logging
log = logging.getLogger(__name__)
@@ -30,35 +33,44 @@ log = logging.getLogger(__name__)
class Controller(QtCore.QObject):
"""
An instance of the GNS3 server controller
An instance of the server controller.
"""
connected_signal = QtCore.Signal()
disconnected_signal = QtCore.Signal()
connection_failed_signal = QtCore.Signal()
project_list_updated_signal = QtCore.Signal()
def __init__(self, parent=None):
def __init__(self):
super().__init__()
self._connected = False
self._connecting = False
self._cache_directory = tempfile.mkdtemp()
self._notification_stream = None
self._version = None
self._cache_directory = tempfile.TemporaryDirectory(suffix="-gns3")
self._http_client = None
# If it's the first error we display an alert box to the user
self._first_error = True
self._error_dialog = None
self._display_error = True
self._projects = []
self._websocket = QtWebSockets.QWebSocket()
# If we do multiple call in order to download the same symbol we queue them
self._static_asset_download_queue = {}
def host(self):
return self._http_client.host()
def version(self):
return self._version
def isRemote(self):
"""
:returns Boolean: True if the controller is remote
"""
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
return not settings["auto_start"]
@@ -66,24 +78,28 @@ class Controller(QtCore.QObject):
"""
:returns: True if connection is in progress
"""
return self._connecting
def connected(self):
"""
Is the controller connected
"""
return self._connected
def httpClient(self):
"""
:returns: HTTP client for connected to the controller
:returns: HTTP client to connect to the controller
"""
return self._http_client
def setHttpClient(self, http_client):
"""
:param http_client: Instance of HTTP client to communicate with the server
"""
self._http_client = http_client
if self._http_client:
if self.isRemote():
@@ -92,10 +108,18 @@ 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
"""
self._display_error = val
self._first_error = True
@@ -103,6 +127,7 @@ class Controller(QtCore.QObject):
"""
Connection process as started
"""
self._connected = False
self._connecting = True
self.get('/version', self._versionGetSlot)
@@ -112,11 +137,13 @@ class Controller(QtCore.QObject):
self._connected = False
self.disconnected_signal.emit()
self._connectingToServer()
self.stopListenNotifications()
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:
self._connecting = False
@@ -128,7 +155,7 @@ class Controller(QtCore.QObject):
self._error_dialog.setText("Error when connecting to the GNS3 server:\n{}".format(result["message"]))
self._error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
self._error_dialog.show()
# Try to connect again in x seconds
# Try to connect again in 5 seconds
QtCore.QTimer.singleShot(5000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))
self._first_error = False
else:
@@ -136,32 +163,43 @@ 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:
self._connected = True
self._connecting = False
self.connected_signal.emit()
self.refreshProjectList()
self._startListenNotifications()
def post(self, *args, **kwargs):
return self.createHTTPQuery("POST", *args, **kwargs)
def get(self, *args, **kwargs):
return self.createHTTPQuery("GET", *args, **kwargs)
def put(self, *args, **kwargs):
return self.createHTTPQuery("PUT", *args, **kwargs)
def delete(self, *args, **kwargs):
return self.createHTTPQuery("DELETE", *args, **kwargs)
def getCompute(self, path, compute_id, *args, **kwargs):
"""
API get on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/{}{}".format(compute_id, path)
return self.get(path, *args, **kwargs)
def post(self, *args, **kwargs):
return self.createHTTPQuery("POST", *args, **kwargs)
def postCompute(self, path, compute_id, *args, **kwargs):
"""
API post on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/{}{}".format(compute_id, path)
return self.post(path, *args, **kwargs)
@@ -170,9 +208,10 @@ class Controller(QtCore.QObject):
"""
Support for remote server <= 1.5
This fix should be not require after the 2.1
when all the appliance template will be managed
on server
when all the templates will be managed on server
"""
#FIXME: remove this?
if compute_id.startswith("http:") or compute_id.startswith("https:"):
from .compute_manager import ComputeManager
try:
@@ -181,22 +220,32 @@ class Controller(QtCore.QObject):
return compute_id
return compute_id
def put(self, *args, **kwargs):
return self.createHTTPQuery("PUT", *args, **kwargs)
def getEndpoint(self, path, compute_id, *args, **kwargs):
"""
API post on a specific compute
"""
def delete(self, *args, **kwargs):
return self.createHTTPQuery("DELETE", *args, **kwargs)
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/endpoint/{}{}".format(compute_id, path)
return self.get(path, *args, **kwargs)
def putCompute(self, path, compute_id, *args, **kwargs):
"""
API put on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/{}{}".format(compute_id, path)
return self.put(path, *args, **kwargs)
def createHTTPQuery(self, method, path, *args, **kwargs):
"""
Forward the query to the HTTP client or controller depending of the path
"""
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)
@staticmethod
def instance():
"""
@@ -208,37 +257,41 @@ class Controller(QtCore.QObject):
Controller._instance = Controller()
return Controller._instance
def getStatic(self, url, callback):
def getStatic(self, url, callback, fallback=None):
"""
Get a URL from the /static on controller and cache it on disk
:param url: URL without the protocol and host part
:param callback: Callback to call when file is ready
:param fallback: Fallback url in case of error
"""
if not self._http_client:
return
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:
self._static_asset_download_queue[path].append(callback)
self._static_asset_download_queue[path].append((callback, fallback, ))
else:
self._static_asset_download_queue[path] = [callback]
self._static_asset_download_queue[path] = [(callback, fallback, )]
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, url, path))
def _getStaticCallback(self, url, path, result, error=False, raw_body=None, **kwargs):
if path not in self._static_asset_download_queue:
return
if error:
log.error("Error while downloading file: {}".format(url))
if path in self._static_asset_download_queue:
del self._static_asset_download_queue[path]
fallback_used = False
for callback, fallback in self._static_asset_download_queue[path]:
if fallback:
self.getStatic(fallback, callback)
fallback_used = True
if fallback_used:
log.debug("Error while downloading file: {}".format(url))
del self._static_asset_download_queue[path]
return
try:
with open(path, "wb+") as f:
@@ -247,24 +300,82 @@ class Controller(QtCore.QObject):
log.error("Can't write to {}: {}".format(path, str(e)))
return
log.debug("File stored {} for {}".format(path, url))
for callback in self._static_asset_download_queue[path]:
for callback, fallback in self._static_asset_download_queue[path]:
callback(path)
del self._static_asset_download_queue[path]
def getSymbolIcon(self, symbol_id, callback):
def getStaticCachedPath(self, url):
"""
Returns static cached (hashed) path
:param url:
"""
m = hashlib.md5()
m.update(url.encode())
if ".svg" in url:
extension = ".svg"
else:
extension = ".png"
path = os.path.join(self._cache_directory.name, m.hexdigest() + extension)
return path
def clearStaticCache(self):
"""
Clear the cache directory.
"""
for filename in os.listdir(self._cache_directory.name):
if filename.endswith(".svg") or filename.endswith(".png"):
try:
os.remove(os.path.join(self._cache_directory.name, filename))
except OSError as e:
log.debug("Error deleting cached symbol '{}':{}".format(filename, e))
continue
def getSymbolIcon(self, symbol_id, callback, fallback=None):
"""
Get a QIcon for a symbol from the controller
:param url: URL without the protocol and host part
:param symbol_id: Symbol id
:param callback: Callback to call when file is ready
:param fallback: Fallback symbol if not found
"""
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback))
if symbol_id is None:
self.getStatic(Symbol(fallback).url(), qpartial(self._getIconCallback, callback))
else:
if fallback:
fallback = Symbol(fallback).url()
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback), fallback=fallback)
def _getIconCallback(self, callback, path):
pixmap = QtGui.QPixmap(path)
if pixmap.isNull():
log.debug("Invalid symbol {}".format(path))
path = ":/icons/cancel.svg"
icon = QtGui.QIcon()
icon.addFile(path)
callback(icon)
def uploadSymbol(self, symbol_id, path):
self.post("/symbols/" + symbol_id + "/raw",
qpartial(self._finishSymbolUpload, path),
body=pathlib.Path(path), progressText="Uploading {}".format(symbol_id), timeout=None)
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
if error:
log.error("Error while uploading symbol: {}: {}".format(path, result.get("message", "unknown")))
return
# Refresh the templates list
from .template_manager import TemplateManager
TemplateManager.instance().templates_changed_signal.emit()
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))
@@ -290,3 +401,78 @@ class Controller(QtCore.QObject):
def projects(self):
return self._projects
def _startListenNotifications(self):
if not self.connected():
return
# Due to bug in Qt on some version we need a dedicated network manager
self._notification_network_manager = QtNetwork.QNetworkAccessManager()
self._notification_stream = None
# Qt websocket before Qt 5.6 doesn't support auth
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.6.0") or parse_version(QtCore.PYQT_VERSION_STR) < parse_version("5.6.0"):
self._notification_stream = Controller.instance().createHTTPQuery("GET", "/notifications", self._endListenNotificationCallback,
downloadProgressCallback=self._event_received,
networkManager=self._notification_network_manager,
timeout=None,
showProgress=False,
ignoreErrors=True)
else:
self._notification_stream = self._http_client.connectWebSocket(self._websocket, "/notifications/ws")
self._notification_stream.textMessageReceived.connect(self._websocket_event_received)
self._notification_stream.error.connect(self._websocket_error)
def stopListenNotifications(self):
if self._notification_stream:
log.debug("Stop listening for notifications from controller")
stream = self._notification_stream
self._notification_stream = None
stream.abort()
self._notification_network_manager = None
def _endListenNotificationCallback(self, result, error=False, **kwargs):
"""
If notification stream disconnect we reconnect to it
"""
if self._notification_stream:
self._notification_stream = None
self._startListenNotifications()
@qslot
def _websocket_error(self, error):
if self._notification_stream:
log.error("Websocket notification stream error: {}".format(self._notification_stream.errorString()))
self._notification_stream = None
self._startListenNotifications()
@qslot
def _websocket_event_received(self, event):
try:
self._event_received(json.loads(event))
except ValueError as e:
log.error("Invalid event received: {}".format(e))
def _event_received(self, result, *args, **kwargs):
# Log only relevant events
if result["action"] not in ("ping", "compute.updated"):
log.debug("Event received from controller stream: {}".format(result))
if result["action"] == "template.created" or result["action"] == "template.updated":
from gns3.template_manager import TemplateManager
TemplateManager.instance().templateDataReceivedCallback(result["event"])
elif result["action"] == "template.deleted":
from gns3.template_manager import TemplateManager
TemplateManager.instance().deleteTemplateCallback(result["event"])
elif result["action"] == "compute.created" or result["action"] == "compute.updated":
from .compute_manager import ComputeManager
ComputeManager.instance().computeDataReceivedCallback(result["event"])
elif result["action"] == "log.error":
log.error(result["event"]["message"])
elif result["action"] == "log.warning":
log.warning(result["event"]["message"])
elif result["action"] == "log.info":
log.info(result["event"]["message"], extra={"show": True})
elif result["action"] == "ping":
pass

View File

@@ -20,6 +20,7 @@ import psutil
import os
import platform
import struct
import distro
try:
import raven
@@ -41,7 +42,7 @@ if __version_info__[3] != 0:
import faulthandler
# Display a traceback in case of segfault crash. Usefull when frozen
# Not enabled by default for security reason
log.info("Enable catching segfault")
log.debug("Enable catching segfault")
faulthandler.enable()
@@ -51,7 +52,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "sync+https://b9912fe90df3496e98ccfb44d9b9baf0:ac42a40e66314b48883243402b957290@sentry.io/38506"
DSN = "https://cef6948387d14919929d7e3db6b11835:064728e23bf043f99100cf271bcd5547@sentry.io/38506"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):
@@ -70,11 +71,20 @@ 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"]:
if not RAVEN_AVAILABLE:
return
if os.path.exists(LocalConfig.instance().runAsRootPath()):
log.warning("User has run application as root. Crash reports are disabled.")
sys.exit(1)
return
if os.path.exists(".git"):
log.warning("A .git directory exist crash report is turn off for developers. Instant exit")
sys.exit(1)
@@ -89,27 +99,41 @@ class CrashReport:
"os:release": platform.release(),
"os:win_32": " ".join(platform.win32_ver()),
"os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]),
"os:linux": " ".join(platform.linux_distribution()),
"os:linux": " ".join(distro.linux_distribution()),
"python:version": "{}.{}.{}".format(sys.version_info[0],
sys.version_info[1],
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:
log.error("Can't send crash report to Sentry: {}".format(e))
return
log.info("Crash report sent with event ID: {}".format(client.get_ident(report)))
log.debug("Crash report sent with event ID: {}".format(client.get_ident(report)))
def _add_qt_information(self, context):
try:
from .qt import QtCore
import sip
from .qt import sip
except ImportError:
return context
context["psutil:version"] = psutil.__version__

View File

@@ -16,14 +16,18 @@
# 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
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
from ..template_manager import TemplateManager
from ..template import Template
from ..modules import Qemu
from ..registry.appliance import Appliance, ApplianceError
from ..registry.registry import Registry
from ..registry.config import Config, ConfigException
from ..registry.config import Config
from ..registry.appliance_to_template import ApplianceToTemplate
from ..registry.image import Image
from ..utils import human_filesize
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
@@ -31,103 +35,133 @@ 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
from ..image_manager import ImageManager
import logging
log = logging.getLogger(__name__)
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
images_changed_signal = QtCore.Signal()
versions_changed_signal = QtCore.Signal()
def __init__(self, parent, path):
super().__init__(parent)
self.setupUi(self)
self.images_changed_signal.connect(self._refreshVersions)
self.versions_changed_signal.connect(self._versionRefreshedSlot)
self._refreshing = False
self._server_check = False
self._template_created = False
self._path = path
# Count how many images are curently uploading
# count how many images are being uploaded
self._image_uploading_count = 0
# symbols loaded from controller
self._symbols = []
# connect slots
self.images_changed_signal.connect(self._refreshVersions)
self.versions_changed_signal.connect(self._versionRefreshedSlot)
self.uiRefreshPushButton.clicked.connect(self.images_changed_signal.emit)
self.uiDownloadPushButton.clicked.connect(self._downloadPushButtonClickedSlot)
self.uiImportPushButton.clicked.connect(self._importPushButtonClickedSlot)
self.uiApplianceVersionTreeWidget.currentItemChanged.connect(self._applianceVersionCurrentItemChangedSlot)
self.uiCreateVersionPushButton.clicked.connect(self._createVersionPushButtonClickedSlot)
self.allowCustomFiles.clicked.connect(self._allowCustomFilesChangedSlot)
#FIXME: deactivate the create version feature (confusing and maybe not necessary, TBD)
self.uiCreateVersionPushButton.hide()
# directories where to search for images
images_directories = list()
for emulator in ("QEMU", "IOU", "DYNAMIPS"):
emulator_images_dir = ImageManager.instance().getDirectoryForType(emulator)
if os.path.exists(emulator_images_dir):
images_directories.append(emulator_images_dir)
images_directories.append(os.path.dirname(self._path))
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
if download_directory != "" and download_directory != os.path.dirname(self._path):
images_directories.append(download_directory)
# registry to search for images
self._registry = Registry(images_directories)
self._registry.image_list_changed_signal.connect(self.images_changed_signal.emit)
# appliance object
self._appliance = Appliance(self._registry, self._path)
self.setWindowTitle("Install {} appliance".format(self._appliance["name"]))
self.uiApplianceVersionTreeWidget.currentItemChanged.connect(self._applianceVersionCurrentItemChangedSlot)
self.uiRefreshPushButton.clicked.connect(self.images_changed_signal.emit)
self.uiDownloadPushButton.clicked.connect(self._downloadPushButtonClickedSlot)
self.uiImportPushButton.clicked.connect(self._importPushButtonClickedSlot)
self.uiCreateVersionPushButton.clicked.connect(self._createVersionPushButtonClickedSlot)
# add a custom button to show appliance information
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Appliance info")
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
self.customButtonClicked.connect(self._showApplianceInfoSlot)
# customize the server selection
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
if hasattr(self, "uiVMRadioButton"):
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
if Controller.instance().isRemote():
self.uiLocalRadioButton.setText("Run the appliance on the main server")
self.uiLocalRadioButton.setText("Install the appliance on the main server")
else:
if not path.endswith('.builtin.gns3a'):
destination = None
try:
destination = Config().appliances_dir
except OSError as 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
def initializePage(self, page_id):
"""
Initialize Wizard pages.
Initialize wizard pages.
:param page_id: page identifier
"""
super().initializePage(page_id)
# add symbol
if self._appliance["category"] == "guest":
symbol = ":/symbols/computer.svg"
else:
symbol = ":/symbols/{}.svg".format(self._appliance["category"])
self.page(page_id).setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(symbol))
if "qemu" in self._appliance:
type = "qemu"
elif "iou" in self._appliance:
type = "iou"
elif "docker" in self._appliance:
type = "docker"
elif "dynamips" in self._appliance:
type = "dynamips"
if self.page(page_id) == self.uiServerWizardPage:
if self.page(page_id) == self.uiInfoWizardPage:
self.uiInfoWizardPage.setTitle(self._appliance["product_name"])
self.uiDescriptionLabel.setText(self._appliance["description"])
Controller.instance().getSymbols(self._getSymbolsCallback)
info = (
("Category", "category"),
("Product", "product_name"),
("Vendor", "vendor_name"),
("Status", "status"),
("Maintainer", "maintainer"),
("Architecture", "qemu/arch"),
("KVM", "qemu/kvm")
)
if "qemu" in self._appliance:
emulator_type = "qemu"
elif "iou" in self._appliance:
emulator_type = "iou"
elif "docker" in self._appliance:
emulator_type = "docker"
elif "dynamips" in self._appliance:
emulator_type = "dynamips"
else:
QtWidgets.QMessageBox.warning(self, "Appliance", "Could not determine the emulator type")
self.uiInfoTreeWidget.clear()
for (name, key) in info:
if "/" in key:
key, subkey = key.split("/")
value = self._appliance.get(key, {}).get(subkey, None)
else:
value = self._appliance.get(key, None)
if value is None:
continue
item = QtWidgets.QTreeWidgetItem([name + ":", value])
font = item.font(0)
font.setBold(True)
item.setFont(0, font)
self.uiInfoTreeWidget.addTopLevelItem(item)
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
is_win = ComputeManager.instance().localPlatform().startswith("win")
elif self.page(page_id) == self.uiServerWizardPage:
self.uiRemoteServersComboBox.clear()
if len(ComputeManager.instance().remoteComputes()) == 0:
self.uiRemoteRadioButton.setEnabled(False)
@@ -141,12 +175,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if ComputeManager.instance().localPlatform() is None:
self.uiLocalRadioButton.setEnabled(False)
elif (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
if type == "qemu":
# Qemu has issues on OSX and Windows we disallow usage of the local server
elif is_mac or is_win:
if emulator_type == "qemu":
# disallow usage of the local server because Qemu has issues on OSX and Windows
if not LocalConfig.instance().experimental():
self.uiLocalRadioButton.setEnabled(False)
elif type != "dynamips":
elif emulator_type != "dynamips":
self.uiLocalRadioButton.setEnabled(False)
if ComputeManager.instance().vmCompute():
@@ -158,74 +192,139 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
else:
self.uiRemoteRadioButton.setChecked(False)
if is_mac or is_win:
if not self.uiRemoteRadioButton.isEnabled() and not self.uiVMRadioButton.isEnabled() and not self.uiLocalRadioButton.isEnabled():
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "The GNS3 VM is not available, please configure the GNS3 VM before adding a new appliance.")
elif self.page(page_id) == self.uiFilesWizardPage:
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
if Controller.instance().isRemote() or self._compute_id != "local":
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
else:
self.images_changed_signal.emit()
elif self.page(page_id) == self.uiQemuWizardPage:
if self._appliance['qemu'].get('kvm', 'require') == 'require':
self._server_check = False
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
else:
self._server_check = True
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
elif self.page(page_id) == self.uiSummaryWizardPage:
self.uiSummaryTreeWidget.clear()
for key in self._appliance[type]:
item = QtWidgets.QTreeWidgetItem([key.replace('_', ' ').capitalize() + ":", str(self._appliance[type][key])])
font = item.font(0)
font.setBold(True)
item.setFont(0, font)
self.uiSummaryTreeWidget.addTopLevelItem(item)
self.uiSummaryTreeWidget.resizeColumnToContents(0)
elif self.page(page_id) == self.uiUsageWizardPage:
self.uiUsageTextEdit.setText("The appliance is available in the {} category. \n\n{}".format(
self._appliance["category"].replace("_", " "),
self._appliance.get("usage", ""))
)
elif self.page(page_id) == self.uiCheckServerWizardPage:
self.uiCheckServerLabel.setText("Please wait while checking server capacities...")
if 'qemu' in self._appliance:
if self._appliance['qemu'].get('kvm', 'require') == 'require':
self._server_check = False # If the server as the capacities for running the appliance
self.uiCheckServerLabel.setText("")
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
return
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
self._server_check = True
self.uiUsageTextEdit.setText("The template will be available in the {} category.\n\n{}".format(self._appliance["category"].replace("_", " "), self._appliance.get("usage", "")))
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
"""
Check if server support KVM or not
Check if the server supports KVM or not
"""
if error is None and "kvm" in result and self._appliance["qemu"]["arch"] in result["kvm"]:
self._server_check = True
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
else:
if error:
msg = result["message"]
else:
msg = "The remote server doesn't support KVM. You need a Linux server or the GNS3 VM with VMware and CPU virtualization instructions."
self.uiCheckServerLabel.setText(msg)
QtWidgets.QMessageBox.critical(self, "Qemu", msg)
msg = "The selected server does not support KVM. A Linux server or the GNS3 VM running in VMware is required."
QtWidgets.QMessageBox.critical(self, "KVM support", msg)
self._server_check = False
def _uiServerWizardPage_isComplete(self):
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
def _imageUploadedCallback(self, result, error=False, **kwargs):
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
def _imageUploadedCallback(self, result, error=False, context=None, **kwargs):
if context is None:
context = {}
image_path = context.get("image_path", "unknown")
if error:
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
else:
log.info("Image '{}' has been successfully uploaded".format(image_path))
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
def _showApplianceInfoSlot(self):
"""
Shows appliance information.
"""
info = (("Product", "product_name"),
("Vendor", "vendor_name"),
("Availability", "availability"),
("Status", "status"),
("Maintainer", "maintainer"))
if "qemu" in self._appliance:
qemu_info = (("vCPUs", "qemu/cpus"),
("RAM", "qemu/ram"),
("Adapters", "qemu/adapters"),
("Adapter type", "qemu/adapter_type"),
("Console type", "qemu/console_type"),
("Architecture", "qemu/arch"),
("Console type", "qemu/console_type"),
("KVM", "qemu/kvm"))
info = info + qemu_info
elif "docker" in self._appliance:
docker_info = (("Image", "docker/image"),
("Adapters", "docker/adapters"),
("Console type", "docker/console_type"))
info = info + docker_info
elif "iou" in self._appliance:
iou_info = (("RAM", "iou/ram"),
("NVRAM", "iou/nvram"),
("Ethernet adapters", "iou/ethernet_adapters"),
("Serial adapters", "iou/serial_adapters"))
info = info + iou_info
elif "dynamips" in self._appliance:
dynamips_info = (("Platform", "dynamips/platform"),
("Chassis", "dynamips/chassis"),
("Midplane", "dynamips/midplane"),
("NPE", "dynamips/npe"),
("RAM", "dynamips/ram"),
("NVRAM", "dynamips/nvram"),
("slot0", "dynamips/slot0"),
("slot1", "dynamips/slot1"),
("slot2", "dynamips/slot2"),
("slot3", "dynamips/slot3"),
("slot4", "dynamips/slot4"),
("slot5", "dynamips/slot5"),
("slot6", "dynamips/slot6"),
("wic0", "dynamips/wic0"),
("wic1", "dynamips/wic1"),
("wic2", "dynamips/wic2"))
info = info + dynamips_info
text_info = ""
for (name, key) in info:
if "/" in key:
key, subkey = key.split("/")
value = self._appliance.get(key, {}).get(subkey, None)
else:
value = self._appliance.get(key, None)
if value is None:
continue
text_info += "<span style='font-weight:bold;'>{}</span>: {}<br>".format(name, value)
msgbox = QtWidgets.QMessageBox(self)
msgbox.setWindowTitle("Appliance information")
msgbox.setStyleSheet("QLabel{min-width: 600px;}") # TODO: resize details box QTextEdit{min-height: 500px;}
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
msgbox.setText(text_info)
msgbox.setDetailedText(self._appliance["description"])
msgbox.exec_()
@qslot
def _refreshVersions(self, *args):
"""
Refresh the list of files for different version of the appliance
Refresh the list of files for different versions of the appliance
"""
if self._refreshing:
return
self._refreshing = True
self.uiFilesWizardPage.setSubTitle("The following versions are available for " + self._appliance["product_name"] + ". Check the status of files required to install.")
self.uiFilesWizardPage.setSubTitle("Please select one version of " + self._appliance["product_name"] + " and import the required files. Files are searched in your downloads and GNS3 images directories by default")
worker = WaitForLambdaWorker(lambda: self._refreshDialogWorker())
progress_dialog = ProgressDialog(worker, "Add appliance", "Scanning directories for files...", None, busy=True, parent=self)
progress_dialog.show()
@@ -235,13 +334,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
"""
Called when we finish to scan the disk for new versions
"""
if self._refreshing or self.currentPage() != self.uiFilesWizardPage:
return
self._refreshing = True
self.uiApplianceVersionTreeWidget.clear()
for version in self._appliance["versions"]:
top = QtWidgets.QTreeWidgetItem(self.uiApplianceVersionTreeWidget, ["{} {}".format(self._appliance["product_name"], version["name"])])
top = QtWidgets.QTreeWidgetItem(self.uiApplianceVersionTreeWidget, ["{} version {}".format(self._appliance["product_name"], version["name"])])
size = 0
status = "Ready to install"
for image in version["images"].values():
@@ -249,19 +349,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
status = "Missing files"
size += image.get("filesize", 0)
image_widget = QtWidgets.QTreeWidgetItem(
[
"",
image["filename"],
human_filesize(image.get("filesize", 0)),
image["status"],
image["version"],
image.get("md5sum", "")
])
image_widget = QtWidgets.QTreeWidgetItem([image["filename"],
human_filesize(image.get("filesize", 0)),
image["status"]])
if image["status"] == "Missing":
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
else:
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
image_widget.setToolTip(2, image["path"])
# Associated data stored are col 0: version, col 1: image
image_widget.setData(0, QtCore.Qt.UserRole, version)
@@ -275,28 +370,38 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
expand = True
if status == "Missing files":
top.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
top.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
else:
expand = False
top.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
top.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
top.setData(2, QtCore.Qt.DisplayRole, human_filesize(size))
top.setData(3, QtCore.Qt.DisplayRole, status)
top.setData(2, QtCore.Qt.UserRole, self._appliance)
top.setData(1, QtCore.Qt.DisplayRole, human_filesize(size))
top.setData(2, QtCore.Qt.DisplayRole, status)
top.setData(0, QtCore.Qt.UserRole, version)
top.setData(2, QtCore.Qt.UserRole, self._appliance)
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
# self.uiApplianceVersionTreeWidget.setCurrentItem(top)
if expand:
top.setExpanded(True)
if len(self._appliance["versions"]) > 0:
self.uiApplianceVersionTreeWidget.resizeColumnToContents(0)
self.uiApplianceVersionTreeWidget.resizeColumnToContents(1)
for column in range(self.uiApplianceVersionTreeWidget.columnCount()):
self.uiApplianceVersionTreeWidget.resizeColumnToContents(column)
self._refreshing = False
def _getSymbolsCallback(self, result, error=False, **kwargs):
"""
Callback to retrieve the appliance symbols.
"""
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 images on the disk
"""
# Docker do not have versions
@@ -305,11 +410,20 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
for version in self._appliance["versions"]:
for image in version["images"].values():
img = self._registry.search_image_file(self._appliance.emulator(), image["filename"], image.get("md5sum"), image.get("filesize"))
img = self._registry.search_image_file(self._appliance.emulator(),
image["filename"],
image.get("md5sum"),
image.get("filesize"),
strict_md5_check=not self.allowCustomFiles.isChecked())
if img:
image["status"] = "Found"
if img.location == "local":
image["status"] = "Found locally"
else:
compute = ComputeManager.instance().getCompute(self._compute_id)
image["status"] = "Found on {}".format(compute.name())
image["md5sum"] = img.md5sum
image["filesize"] = img.filesize
image["path"] = img.path
else:
image["status"] = "Missing"
self._refreshing = False
@@ -320,9 +434,9 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
"""
Called when user select a different item in the list of appliance files
"""
self.uiDownloadPushButton.hide()
self.uiImportPushButton.hide()
self.uiExplainDownloadLabel.hide()
if current is None or sip.isdeleted(current):
return
@@ -336,9 +450,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
@qslot
def _downloadPushButtonClickedSlot(self, *args):
"""
Called when user want to download an appliance images.
He should have selected the file before.
Called when user wants to download an appliance image.
The file should be selected first.
"""
if self._refreshing:
return False
@@ -352,7 +467,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if "direct_download_url" in data:
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["direct_download_url"]))
if "compression" in data:
QtWidgets.QMessageBox.warning(self, "Add appliance", "The file is compressed with {} you need to uncompress it before using it.".format(data["compression"]))
QtWidgets.QMessageBox.warning(self, "Add appliance", "The file is compressed with '{}', it must be uncompressed first".format(data["compression"]))
else:
QtWidgets.QMessageBox.warning(self, "Add appliance", "Download will redirect you where the required file can be downloaded, you may have to be registered with the vendor in order to download the file.")
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["download_url"]))
@@ -375,9 +490,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
@qslot
def _importPushButtonClickedSlot(self, *args):
"""
Called when user want to import an appliance images.
He should have selected the file before.
Called when user wants to import an appliance images.
The file should be selected first.
"""
if self._refreshing:
return False
@@ -393,12 +509,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):
"""
@@ -426,19 +547,16 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
def _install(self, version):
"""
Install the appliance to GNS3
Install the appliance in GNS3
:params version: Version name
:params version: appliance version name
"""
try:
config = Config()
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
return False
if version is None:
appliance_configuration = self._appliance.copy()
if "docker" not in appliance_configuration:
# only Docker do not have version
return False
else:
try:
appliance_configuration = self._appliance.search_images_for_version(version)
@@ -446,50 +564,83 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
return False
while len(appliance_configuration["name"]) == 0 or not config.is_name_available(appliance_configuration["name"]):
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "The name \"{}\" is already used by another appliance".format(appliance_configuration["name"]))
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add appliance", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
template_manager = TemplateManager().instance()
while len(appliance_configuration["name"]) == 0 or not template_manager.is_name_available(appliance_configuration["name"]):
QtWidgets.QMessageBox.warning(self.parent(), "Add template", "The name \"{}\" is already used by another template".format(appliance_configuration["name"]))
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add template", "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])
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
progress_dialog.show()
if not progress_dialog.exec_():
return False
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, self._symbols, parent=self)
TemplateManager.instance().createTemplate(Template(new_template), callback=self._templateCreatedCallback)
return False
worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
progress_dialog.show()
if progress_dialog.exec_():
QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
return True
#worker = WaitForLambdaWorker(lambda: self._create_template(appliance_configuration, self._compute_id), allowed_exceptions=[ConfigException, OSError])
#progress_dialog = ProgressDialog(worker, "Add template", "Installing a new template...", None, busy=True, parent=self)
#progress_dialog.show()
#if progress_dialog.exec_():
# QtWidgets.QMessageBox.information(self.parent(), "Add template", "{} template has been installed!".format(appliance_configuration["name"]))
# return True
#return False
def _uploadImages(self, version):
# worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
# progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
# progress_dialog.show()
# if progress_dialog.exec_():
# QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
# return True
def _templateCreatedCallback(self, result, error=False, **kwargs):
if error is True:
QtWidgets.QMessageBox.critical(self.parent(), "Add template", "The template cannot be created: {}".format(result.get("message", "unknown")))
return
QtWidgets.QMessageBox.information(self.parent(), "Add template", "The appliance has been installed and a template named '{}' has been successfully created!".format(result["name"]))
self._template_created = True
self.done(True)
def _uploadImages(self, name, version):
"""
Upload an image to the compute
Upload an image the compute.
"""
appliance_configuration = self._appliance.search_images_for_version(version)
try:
appliance_configuration = self._appliance.search_images_for_version(version)
except ApplianceError as e:
QtWidgets.QMessageBox.critical(self, "Appliance","Cannot install {} version {}: {}".format(name, version, e))
return
for image in appliance_configuration["images"]:
if image["location"] == "local":
if not Controller.instance().isRemote() and self._compute_id == "local" and image["path"].startswith(ImageManager.instance().getDirectory()):
log.debug("{} is already on the local server".format(image["path"]))
return
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
image.upload(self._compute_id, self._applianceImageUploadedCallback)
image_upload_manager = ImageUploadManager(image, Controller.instance(), self._compute_id, self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manager.upload()
self._image_uploading_count += 1
def _applianceImageUploadedCallback(self, result, error=False, **kwargs):
self._image_uploading_count -= 1
def _applianceImageUploadedCallback(self, result, error=False, context=None, **kwargs):
if context is None:
context = {}
image_path = context.get("image_path", "unknown")
if error:
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
else:
log.info("Image '{}' has been successfully uploaded".format(image_path))
self._image_uploading_count -= 1
def nextId(self):
if self.currentPage() == self.uiServerWizardPage:
if "docker" in self._appliance:
return super().nextId() + 3
# skip Qemu binary selection and files pages if this is a Docker appliance
return super().nextId() + 2
elif "qemu" not in self._appliance:
return super().nextId() + 1
elif self.currentPage() == self.uiFilesWizardPage:
if "qemu" not in self._appliance:
# skip the Qemu binary selection page if not a Qemu appliance
return super().nextId() + 1
return super().nextId()
@@ -499,6 +650,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
"""
if self.currentPage() == self.uiFilesWizardPage:
# validate the files page
if self._refreshing:
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
@@ -508,20 +661,26 @@ 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)
if reply == QtWidgets.QMessageBox.No:
return False
self._uploadImages(version["name"])
self._uploadImages(appliance["name"], version["name"])
elif self.currentPage() == self.uiUsageWizardPage:
if self._image_uploading_count > 0:
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for image uploading")
return False
# validate the usage page
if self._template_created:
return True
if self._image_uploading_count > 0:
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for appliance files to be uploaded")
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
if current:
version = current.data(0, QtCore.Qt.UserRole)
@@ -530,9 +689,11 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
return self._install(None)
elif self.currentPage() == self.uiServerWizardPage:
# validate the server page
if self.uiRemoteRadioButton.isChecked():
if len(ComputeManager.instance().remoteComputes()) == 0:
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote servers configured in your preferences")
return False
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
@@ -541,20 +702,20 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if ComputeManager.instance().localPlatform():
if (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
if "qemu" in self._appliance:
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and MacOSX is not supported by the GNS3 team. Are you sur to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and macOS is not supported by the GNS3 team. Do you want to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return False
self._compute_id = "local"
elif self.currentPage() == self.uiQemuWizardPage:
# validate the Qemu
if self._server_check is False:
QtWidgets.QMessageBox.critical(self, "Checking for KVM support", "Please wait for the server to reply...")
return False
if self.uiQemuListComboBox.currentIndex() == -1:
QtWidgets.QMessageBox.critical(self, "Qemu binary", "No compatible Qemu binary selected")
return False
elif self.currentPage() == self.uiCheckServerWizardPage:
return self._server_check
return True
@qslot
@@ -564,6 +725,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
:param checked: either the button is checked or not
"""
if checked:
self.uiRemoteServersGroupBox.setEnabled(False)
self.uiRemoteServersGroupBox.hide()
@@ -590,3 +752,21 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if checked:
self.uiRemoteServersGroupBox.setEnabled(False)
self.uiRemoteServersGroupBox.hide()
@qslot
def _allowCustomFilesChangedSlot(self, checked):
"""
Slot for when user want to upload images which don't match md5
:param checked: if allows or doesn't allow custom files
:return:
"""
if checked:
reply = QtWidgets.QMessageBox.question(self, "Custom files",
"This option allows files with different MD5 checksums. This feature is only for advanced users and can lead "
"to unexpected problems. Do you want to proceed?",
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
self.allowCustomFiles.setChecked(False)
return False

View File

@@ -23,6 +23,7 @@ from gns3.local_config import LocalConfig
from gns3.ui.console_command_dialog_ui import Ui_uiConsoleCommandDialog
from gns3.settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, \
PRECONFIGURED_VNC_CONSOLE_COMMANDS, \
PRECONFIGURED_SPICE_CONSOLE_COMMANDS, \
CUSTOM_CONSOLE_COMMANDS_SETTINGS
@@ -38,7 +39,7 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
def __init__(self, parent, console_type="telnet", current=None):
"""
:params console_type: telnet, serial or vnc
:params console_type: telnet, serial, vnc or spice
:params current: Current console command
"""
super().__init__(parent)
@@ -62,6 +63,9 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
elif self._console_type == "vnc":
self._consoles = copy.copy(PRECONFIGURED_VNC_CONSOLE_COMMANDS)
self._consoles.update(self._settings[self._console_type])
elif self._console_type.startswith("spice"):
self._consoles = copy.copy(PRECONFIGURED_SPICE_CONSOLE_COMMANDS)
self._consoles.update(self._settings[self._console_type])
self.uiCommandComboBox.clear()
self.uiCommandComboBox.addItem("Custom", "")

View File

@@ -0,0 +1,200 @@
# -*- 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/>.
"""
Custom adapters configuration.
"""
import textwrap
import re
from ..qt import QtCore, QtWidgets
from ..ui.custom_adapters_configuration_dialog_ui import Ui_CustomAdaptersConfigurationDialog
class NoEditDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
QtWidgets.QStyledItemDelegate.__init__(self, parent=parent)
def createEditor(self, parent, option, index):
return None
class TreeWidgetItem(QtWidgets.QTreeWidgetItem):
def __lt__(self, other):
column = self.treeWidget().sortColumn()
key1 = self.text(column)
key2 = other.text(column)
return self.natural_sort_key(key1) < self.natural_sort_key(key2)
@staticmethod
def natural_sort_key(key):
regex = r'(\d*\.\d+|\d+)'
parts = re.split(regex, key)
return tuple((e if i % 2 == 0 else float(e)) for i, e in enumerate(parts))
class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConfigurationDialog):
"""
Custom adapters configuration dialog.
:param parent: parent widget
"""
def __init__(self, ports, custom_adapters, default_adapter_type=None, adapter_types=None, base_mac_address=None, parent=None):
super().__init__(parent)
self.setupUi(self)
self._ports = ports
self._default_adapter_type = default_adapter_type
self._adapter_types = adapter_types
self._custom_adapters = custom_adapters
self._base_mac_address = base_mac_address
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(self._resetSlot)
if self._default_adapter_type and self._adapter_types:
self.uiAdaptersTreeWidget.setColumnCount(3)
self.uiAdaptersTreeWidget.headerItem().setText(2, "Adapter type")
if self._base_mac_address:
self.uiAdaptersTreeWidget.setColumnCount(4)
self.uiAdaptersTreeWidget.headerItem().setText(3, "MAC address")
self._populateWidgets()
# resize to fit the tree widget
width = 0
for column in range(self.uiAdaptersTreeWidget.columnCount()):
width += 20 + self.uiAdaptersTreeWidget.columnWidth(column)
self.resize(QtCore.QSize(width, self.height()))
def _getCustomAdapterSettings(self, adapter_number):
for custom_adapter in self._custom_adapters:
if custom_adapter["adapter_number"] == adapter_number:
return custom_adapter
return {}
def _MacToInteger(self, mac_address):
"""
Convert a macaddress with the format 00:0c:29:11:b0:0a to a int
:param mac_address: The mac address
:returns: Integer
"""
return int(mac_address.replace(":", ""), 16)
def _IntegerToMac(self, integer):
"""
Convert an integer to a mac address
"""
return ":".join(textwrap.wrap("%012x" % (integer), width=2))
def _populateWidgets(self):
adapter_number = 0
for port_name in self._ports:
item = TreeWidgetItem(self.uiAdaptersTreeWidget)
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
item.setText(0, "Adapter {}".format(adapter_number))
item.setData(0, QtCore.Qt.UserRole, adapter_number)
item.setData(1, QtCore.Qt.UserRole, port_name)
custom_adapter = self._getCustomAdapterSettings(adapter_number)
item.setText(1, custom_adapter.get("port_name", port_name))
if self._default_adapter_type and self._adapter_types:
combobox = QtWidgets.QComboBox(self)
if type(self._adapter_types) == list:
for adapter_type in self._adapter_types:
combobox.addItem("{}".format(adapter_type))
else:
index = 0
for adapter_type, adapter_description in self._adapter_types.items():
combobox.addItem("{}".format(adapter_type))
combobox.setItemData(index, adapter_description, QtCore.Qt.ToolTipRole)
index += 1
adapter_type_index = combobox.findText(custom_adapter.get("adapter_type", self._default_adapter_type))
combobox.setCurrentIndex(adapter_type_index)
self.uiAdaptersTreeWidget.setItemWidget(item, 2, combobox)
if self._base_mac_address:
self.uiAdaptersTreeWidget.addTopLevelItem(item)
line_edit = QtWidgets.QLineEdit(self)
line_edit.setInputMask("HH:HH:HH:HH:HH:HH;_")
mac_address = self._IntegerToMac(self._MacToInteger(self._base_mac_address) + adapter_number)
line_edit.setText(custom_adapter.get("mac_address", mac_address))
self.uiAdaptersTreeWidget.setItemWidget(item, 3, line_edit)
adapter_number += 1
self.uiAdaptersTreeWidget.setItemDelegateForColumn(0, NoEditDelegate(self))
self.uiAdaptersTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiAdaptersTreeWidget.setSortingEnabled(True)
for column in range(self.uiAdaptersTreeWidget.columnCount()):
self.uiAdaptersTreeWidget.resizeColumnToContents(column)
def _resetSlot(self):
self.uiAdaptersTreeWidget.clear()
self._custom_adapters.clear()
self._populateWidgets()
def _updateCustomAdapters(self):
self._custom_adapters.clear()
for row in range(self.uiAdaptersTreeWidget.topLevelItemCount()):
custom_adapter_settings = {}
item = self.uiAdaptersTreeWidget.topLevelItem(row)
port_name = item.text(1)
adapter_number = item.data(0, QtCore.Qt.UserRole)
custom_adapter_settings["adapter_number"] = adapter_number
original_port_name = item.data(1, QtCore.Qt.UserRole)
if original_port_name != port_name:
custom_adapter_settings["port_name"] = port_name
if self._default_adapter_type and self._adapter_types:
adapter_type = self.uiAdaptersTreeWidget.itemWidget(item, 2).currentText()
if self._default_adapter_type != adapter_type:
custom_adapter_settings["adapter_type"] = adapter_type
if self._base_mac_address:
mac_address = self.uiAdaptersTreeWidget.itemWidget(item, 3).text()
if mac_address and mac_address != ":::::":
if not re.search(r"""^([0-9a-fA-F]{2}[:]){5}[0-9a-fA-F]{2}$""", mac_address):
QtWidgets.QMessageBox.critical(self, "MAC address", "Invalid MAC address (format required: hh:hh:hh:hh:hh:hh)")
return
default_mac_address = self._IntegerToMac(self._MacToInteger(self._base_mac_address) + adapter_number)
if mac_address != default_mac_address:
custom_adapter_settings["mac_address"] = mac_address
if len(custom_adapter_settings) > 1:
# only save if there is more than the adapter_number key
self._custom_adapters.append(custom_adapter_settings.copy())
def done(self, result):
"""
Called when the dialog is closed.
:param result: boolean (accepted or rejected)
"""
if result:
self._updateCustomAdapters()
super().done(result)

View File

@@ -95,13 +95,14 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
def checkAVGInstalled(self):
"""Checking if AVG software is not installed"""
for proc in psutil.process_iter():
try:
psinfo = proc.as_dict(["exe"])
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
except psutil.NoSuchProcess:
pass
if sys.platform.startswith("win32"):
for proc in psutil.process_iter():
try:
psinfo = proc.as_dict(["exe"])
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
except psutil.NoSuchProcess:
pass
return (0, None)
def checkFreeRam(self):
@@ -136,22 +137,15 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
if not os.path.exists(path):
return (2, "Ubridge path {path} doesn't exists".format(path=path))
request_setuid = False
if sys.platform.startswith("linux"):
try:
if "security.capability" in os.listxattr(path):
caps = os.getxattr(path, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
return (2, "Ubridge requires CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
else:
# capabilities not supported
request_setuid = True
except AttributeError:
if "security.capability" not in os.listxattr(path) or not struct.unpack("<IIIII", os.getxattr(path, "security.capability"))[1] & 1 << 13:
return (2, "Ubridge requires CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
except (OSError, AttributeError) as e:
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
return (1, "Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)".format(path=path))
return (1, "Could not determine if CAP_NET_RAW capability is set for uBridge: {}".format(e))
if sys.platform.startswith("darwin") or request_setuid:
if sys.platform.startswith("darwin"):
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
return (2, "Ubridge should be setuid. Run sudo chown root:admin {path} and sudo chmod 4750 {path}".format(path=path))
return (0, None)

View File

@@ -41,9 +41,6 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
self.uiServerHostLineEdit.setText(self._compute.host())
self.uiServerPortSpinBox.setValue(self._compute.port())
index = self.uiServerProtocolComboBox.findText(self._compute.protocol().upper())
self.uiServerProtocolComboBox.setCurrentIndex(index)
if self._compute.user():
self.uiEnableAuthenticationCheckBox.setChecked(True)
self.uiServerUserLineEdit.setText(self._compute.user())
@@ -81,7 +78,7 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
host = self.uiServerHostLineEdit.text().strip()
name = self.uiServerNameLineEdit.text().strip()
protocol = self.uiServerProtocolComboBox.currentText().lower()
protocol = "http"
port = self.uiServerPortSpinBox.value()
user = self.uiServerUserLineEdit.text().strip()
password = self.uiServerPasswordLineEdit.text().strip()

View File

@@ -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,69 @@ 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.uiNodeGridSizeSpinBox.setValue(self._project.nodeGridSize())
self.uiDrawingGridSizeSpinBox.setValue(self._project.drawingGridSize())
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 +114,8 @@ 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.setNodeGridSize(self.uiNodeGridSizeSpinBox.value())
self._project.setDrawingGridSize(self.uiDrawingGridSizeSpinBox.value())
self._project.setVariables(self._cleanVariables())
self._project.update()
super().done(result)

View File

@@ -62,7 +62,7 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
self._exportDebugCallback({}, error=True)
def _exportDebugCallback(self, result, error=False, **kwargs):
log.info("Export debug information to %s", self._path)
log.debug("Export debug information to %s", self._path)
try:
with ZipFile(self._path, 'w') as zip:

View File

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

View File

@@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from ..qt import QtGui, QtWidgets, qslot
from ..ui.filter_dialog_ui import Ui_FilterDialog
import logging
log = logging.getLogger(__name__)
class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
"""
Filter dialog.
"""
def __init__(self, parent, link):
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
self._filter_items = {}
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(self._resetSlot)
def _listAvailableFiltersCallback(self, result, error=False, *args, **kwargs):
if error:
log.warning("Error while listing information about the link: {}".format(result["message"]))
return
self._filters = result
self._initialized = True
self._updateUiSlot()
@qslot
def _updateUiSlot(self, *args):
# Empty the main layout
while True:
item = self.uiVerticalLayout.takeAt(0)
if item is None:
break
elif item.widget():
item.widget().deleteLater()
if len(self._filters) == 0:
QtWidgets.QMessageBox.critical(self, "Link", "No filter available for this link. Try with a different node type.")
self.reject()
self._tabWidget = QtWidgets.QTabWidget(self)
for i, filter in enumerate(self._filters):
tab = QtWidgets.QWidget()
self._tabWidget.addTab(tab, filter['name'])
self._tabWidget.setTabToolTip(i, filter['description'])
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_red.svg'))
vlayout = QtWidgets.QVBoxLayout()
gridLayout = QtWidgets.QGridLayout()
line = 0
filter["spinBoxes"] = []
filter["textEdits"] = []
nb_spin = 0
for param in filter["parameters"]:
label = QtWidgets.QLabel()
label.setText(param["name"] + ":")
gridLayout.addWidget(label, line, 0, 1, 1)
if param["type"] == "int":
spinBox = QtWidgets.QSpinBox()
filter["spinBoxes"].append(spinBox)
spinBox.setMinimum(param["minimum"])
spinBox.setMaximum(param["maximum"])
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(spinBox.sizePolicy().hasHeightForWidth())
spinBox.setSizePolicy(sizePolicy)
try:
value = self._link.filters()[filter["type"]][nb_spin]
spinBox.setValue(value)
if value != 0:
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_green.svg'))
except(KeyError, IndexError):
pass
nb_spin += 1
gridLayout.addWidget(spinBox, line, 1, 1, 1)
unit = QtWidgets.QLabel()
unit.setText(param["unit"])
gridLayout.addWidget(unit, line, 2, 1, 1)
elif param["type"] == "text":
textEdit = QtWidgets.QTextEdit()
textEdit.setAcceptRichText(False)
filter["textEdits"].append(textEdit)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
textEdit.setMinimumWidth(300)
textEdit.setSizePolicy(sizePolicy)
try:
text = self._link.filters()[filter["type"]][0]
textEdit.setPlainText(text)
if text:
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_green.svg'))
except(KeyError, IndexError):
pass
gridLayout.addWidget(textEdit, line, 1, 1, 1)
line += 1
spacerItem = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
gridLayout.addItem(spacerItem, line, 0, 1, 1)
vlayout.addLayout(gridLayout)
tab.setLayout(vlayout)
self.uiVerticalLayout.addWidget(self._tabWidget)
@qslot
def _applyPreferencesSlot(self, *args):
new_filters = {}
for filter in self._filters:
new_filters[filter["type"]] = []
for spinBox in filter["spinBoxes"]:
new_filters[filter["type"]].append(spinBox.value())
for spinBox in filter["textEdits"]:
new_filters[filter["type"]].append(spinBox.toPlainText())
self._link.setFilters(new_filters)
self._link.update()
@qslot
def _helpSlot(self, *args):
help_text = "Filters are applied to packets in both direction.\n\n"
filter_nb = 0
for filter in self._filters:
help_text += "{}: {}".format(filter["name"], filter["description"])
filter_nb += 1
if len(self._filters) != filter_nb:
help_text += "\n\n"
QtWidgets.QMessageBox.information(self, "Help for filters", help_text)
@qslot
def _resetSlot(self, *args):
filters = {}
self._link.setFilters(filters)
self._link.update()
def done(self, result):
"""
Called when the dialog is closed.
:param result: boolean (accepted or rejected)
"""
if result and self._initialized:
self._applyPreferencesSlot()
super().done(result)

View File

@@ -40,7 +40,8 @@ class IdlePCDialog(QtWidgets.QDialog, Ui_IdlePCDialog):
self._idlepcs = idlepcs
for value in self._idlepcs:
match = re.search(r"^(0x[0-9a-f]+)\s+\[(\d+)\]$", value)
# validate idle-pc format, e.g. 0x60c09aa0
match = re.search(r"^(0x[0-9a-f]{8})\s+\[(\d+)\]$", value)
if match:
idlepc = match.group(1)
count = int(match.group(2))
@@ -61,7 +62,7 @@ Select each value that appears in the list and click Apply, and note the CPU usa
"""
QtWidgets.QMessageBox.information(self, "Hints for Idle-PC", help_text)
def _applySlot(self):
def _applySlot(self, update_template=False):
"""
Applies an Idle-PC value.
"""
@@ -77,8 +78,9 @@ Select each value that appears in the list and click Apply, and note the CPU usa
if hasattr(node, "idlepc") and node.settings()["image"] == ios_image:
node.setIdlepc(idlepc)
# apply the idle-pc to templates with the same IOS image
self._router.module().updateImageIdlepc(ios_image, idlepc)
if update_template:
# apply the idle-pc to templates with the same IOS image
self._router.module().updateImageIdlepc(ios_image, idlepc)
def done(self, result):
"""
@@ -88,5 +90,5 @@ Select each value that appears in the list and click Apply, and note the CPU usa
"""
if result:
self._applySlot()
self._applySlot(update_template=True)
super().done(result)

View File

@@ -1,121 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3.qt import QtWidgets, QtCore
from gns3.ui.new_appliance_dialog_ui import Ui_NewApplianceDialog
from gns3.dialogs.preferences_dialog import PreferencesDialog
import logging
log = logging.getLogger(__name__)
class NewApplianceDialog(QtWidgets.QDialog, Ui_NewApplianceDialog):
"""
This dialog allow user to create a new appliance by opening
the correct creation dialog
"""
def __init__(self, parent):
super().__init__(parent)
self.setupUi(self)
self.uiImportApplianceTemplatePushButton.clicked.connect(self._importApplianceTemplatePushButtonClickedSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self._okButtonClickedSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpButtonClickedSlot)
def _importApplianceTemplatePushButtonClickedSlot(self):
self.accept()
from gns3.main_window import MainWindow
MainWindow.instance().openApplianceActionSlot()
def _okButtonClickedSlot(self):
self.accept()
dialog = PreferencesDialog(self.parent())
if self.uiAddIOSRouterRadioButton.isChecked():
self._setPreferencesPane(dialog, "Dynamips").uiNewIOSRouterPushButton.clicked.emit(False)
elif self.uiAddIOUDeviceRadioButton.isChecked():
self._setPreferencesPane(dialog, "IOS on UNIX").uiNewIOUDevicePushButton.clicked.emit(False)
elif self.uiAddQemuVMRadioButton.isChecked():
self._setPreferencesPane(dialog, "QEMU").uiNewQemuVMPushButton.clicked.emit(False)
elif self.uiAddVirtualBoxVMRadioButton.isChecked():
self._setPreferencesPane(dialog, "VirtualBox").uiNewVirtualBoxVMPushButton.clicked.emit(False)
elif self.uiAddVMwareVMRadioButton.isChecked():
self._setPreferencesPane(dialog, "VMware").uiNewVMwareVMPushButton.clicked.emit(False)
elif self.uiAddDockerVMRadioButton.isChecked():
self._setPreferencesPane(dialog, "Docker").uiNewDockerVMPushButton.clicked.emit(False)
elif self.uiAddVPCSRadioButton.isChecked():
self._setPreferencesPane(dialog, "VPCS").uiNewVPCSPushButton.clicked.emit(False)
elif self.uiAddCloudRadioButton.isChecked():
self._setPreferencesPane(dialog, "Cloud nodes").uiNewCloudNodePushButton.clicked.emit(False)
elif self.uiAddEthernetHubRadioButton.isChecked():
self._setPreferencesPane(dialog, "Ethernet hubs").uiNewEthernetHubPushButton.clicked.emit(False)
elif self.uiAddEthernetSwitchRadioButton.isChecked():
self._setPreferencesPane(dialog, "Ethernet switches").uiNewEthernetSwitchPushButton.clicked.emit(False)
else:
return
dialog.exec_()
def _helpButtonClickedSlot(self):
help_text = """<html><p>This dialog helps you to add an appliance template in GNS3. In all cases you must provide your own images.</p>
<p>You can download appliance template files (.gns3appliance) from <a href="https://gns3.com/marketplace/appliances">the GNS3 website</a></p>
<p>A template file provides community tested settings to run a specific appliance in GNS3.</p></html>
"""
QtWidgets.QMessageBox.information(self, "Help for adding a new appliance template", help_text)
def _setPreferencesPane(self, dialog, name):
"""
Finds the first child of the QTreeWidgetItem name.
:param dialog: PreferencesDialog instance
:param name: QTreeWidgetItem name
:returns: current QWidget
"""
panes = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFixedString)
if len(panes) > 0:
child_pane = panes[0].child(0)
dialog.uiTreeWidget.setCurrentItem(child_pane)
else:
i = 0
root = dialog.uiTreeWidget.invisibleRootItem()
while i < root.childCount():
root_item = root.child(i)
x = 0
while x < root_item.childCount():
item = root_item.child(x)
x += 1
if item.text(0) == name:
dialog.uiTreeWidget.setCurrentItem(item)
i += 1
dialog.addModifiedPage(dialog.uiStackedWidget.currentWidget())
return dialog.uiStackedWidget.currentWidget()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
main = QtWidgets.QMainWindow()
dialog = NewApplianceDialog(main)
dialog._setPreferencesPane(PreferencesDialog(main), "Ethernet hubs").uiNewEthernetHubPushButton.clicked.emit(False)
dialog.show()
exit_code = app.exec_()

View File

@@ -0,0 +1,288 @@
# -*- 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 sys
import tempfile
import json
import sip
import os
from gns3.qt import QtCore, QtWidgets, qpartial
from gns3.controller import Controller
from gns3.appliance_manager import ApplianceManager
from ..ui.new_template_wizard_ui import Ui_NewTemplateWizard
import logging
log = logging.getLogger(__name__)
class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
"""
New template wizard.
"""
def __init__(self, parent):
super().__init__(parent)
self.setupUi(self)
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
# add a custom button to show appliance information
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Update from online registry")
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
self.customButtonClicked.connect(self._downloadAppliancesSlot)
self.button(QtWidgets.QWizard.CustomButton1).hide()
self.uiFilterLineEdit.textChanged.connect(self._filterTextChangedSlot)
ApplianceManager.instance().appliances_changed_signal.connect(self._appliancesChangedSlot)
def _downloadAppliancesSlot(self):
"""
Request server to update appliances from online registry.
"""
ApplianceManager.instance().refresh(update=True)
Controller.instance().clearStaticCache()
def _appliancesChangedSlot(self):
"""
Called when the appliances have been updated.
"""
self._get_appliances_from_server()
QtWidgets.QMessageBox.information(self, "Appliances", "Appliances are up-to-date!")
def _filterTextChangedSlot(self, text):
self._get_appliances_from_server(appliance_filter=text)
def _setItemIcon(self, item, icon):
if item is None or sip.isdeleted(item):
return
item.setIcon(0, icon)
def _get_tooltip_text(self, appliance):
"""
Gets the appliance information to be displayed in the tooltip.
"""
info = (("Product", "product_name"),
("Vendor", "vendor_name"),
("Availability", "availability"),
("Status", "status"),
("Maintainer", "maintainer"))
if "qemu" in appliance:
qemu_info = (("vCPUs", "qemu/cpus"),
("RAM", "qemu/ram"),
("Adapters", "qemu/adapters"),
("Adapter type", "qemu/adapter_type"),
("Console type", "qemu/console_type"),
("Architecture", "qemu/arch"),
("Console type", "qemu/console_type"),
("KVM", "qemu/kvm"))
info = info + qemu_info
elif "docker" in appliance:
docker_info = (("Image", "docker/image"),
("Adapters", "docker/adapters"),
("Console type", "docker/console_type"))
info = info + docker_info
elif "iou" in appliance:
iou_info = (("RAM", "iou/ram"),
("NVRAM", "iou/nvram"),
("Ethernet adapters", "iou/ethernet_adapters"),
("Serial adapters", "iou/serial_adapters"))
info = info + iou_info
elif "dynamips" in appliance:
dynamips_info = (("Platform", "dynamips/platform"),
("Chassis", "dynamips/chassis"),
("Midplane", "dynamips/midplane"),
("NPE", "dynamips/npe"),
("RAM", "dynamips/ram"),
("NVRAM", "dynamips/nvram"),
("slot0", "dynamips/slot0"),
("slot1", "dynamips/slot1"),
("slot2", "dynamips/slot2"),
("slot3", "dynamips/slot3"),
("slot4", "dynamips/slot4"),
("slot5", "dynamips/slot5"),
("slot6", "dynamips/slot6"),
("wic0", "dynamips/wic0"),
("wic1", "dynamips/wic1"),
("wic2", "dynamips/wic2"))
info = info + dynamips_info
text_info = ""
for (name, key) in info:
if "/" in key:
key, subkey = key.split("/")
value = appliance.get(key, {}).get(subkey, None)
else:
value = appliance.get(key, None)
if value is None:
continue
text_info += "<span style='font-weight:bold;'>{}</span>: {}<br>".format(name, value)
return text_info
def _get_appliances_from_server(self, appliance_filter=None):
"""
Gets the appliances from the server and display them.
"""
self.uiAppliancesTreeWidget.clear()
parent_routers = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_routers.setText(0, "Routers")
parent_routers.setFlags(parent_routers.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_switches = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_switches.setText(0, "Switches")
parent_switches.setFlags(parent_switches.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_guests = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_guests.setText(0, "Guests")
parent_guests.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_firewalls = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_firewalls.setText(0, "Firewalls")
parent_firewalls.setFlags(parent_firewalls.flags() & ~QtCore.Qt.ItemIsSelectable)
self.uiAppliancesTreeWidget.expandAll()
for appliance in ApplianceManager.instance().appliances():
if appliance_filter is None:
appliance_filter = self.uiFilterLineEdit.text().strip()
if appliance_filter and appliance_filter.lower() not in appliance["name"].lower():
continue
if appliance["category"] == "router":
item = QtWidgets.QTreeWidgetItem(parent_routers)
elif appliance["category"].endswith("switch"):
item = QtWidgets.QTreeWidgetItem(parent_switches)
elif appliance["category"] == "firewall":
item = QtWidgets.QTreeWidgetItem(parent_firewalls)
elif appliance["category"] == "guest":
item = QtWidgets.QTreeWidgetItem(parent_guests)
if appliance["builtin"]:
appliance_name = appliance["name"]
else:
appliance_name = "{} (custom)".format(appliance["name"])
item.setText(0, appliance_name)
#item.setText(1, appliance["category"].capitalize().replace("_", " "))
if "qemu" in appliance:
item.setText(1, "Qemu")
elif "iou" in appliance:
item.setText(1, "IOU")
elif "dynamips" in appliance:
item.setText(1, "Dynamips")
elif "docker" in appliance:
item.setText(1, "Docker")
else:
item.setText(1, "N/A")
item.setText(2, appliance["vendor_name"])
item.setData(0, QtCore.Qt.UserRole, appliance)
#item.setSizeHint(0, QtCore.QSize(32, 32))
item.setToolTip(0, self._get_tooltip_text(appliance))
Controller.instance().getSymbolIcon(appliance.get("symbol"), qpartial(self._setItemIcon, item),
fallback=":/symbols/" + appliance["category"] + ".svg")
self.uiAppliancesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiAppliancesTreeWidget.resizeColumnToContents(0)
if not appliance_filter:
self.uiAppliancesTreeWidget.collapseAll()
def initializePage(self, page_id):
"""
Initialize Wizard pages.
:param page_id: page identifier
"""
super().initializePage(page_id)
if self.page(page_id) == self.uiApplianceFromServerWizardPage:
self.button(QtWidgets.QWizard.CustomButton1).show()
self.setButtonText(QtWidgets.QWizard.FinishButton, "&Install")
self._get_appliances_from_server()
else:
self.button(QtWidgets.QWizard.CustomButton1).hide()
def cleanupPage(self, page_id):
"""
Restore button default settings on the first page.
"""
self.button(QtWidgets.QWizard.CustomButton1).hide()
self.setButtonText(QtWidgets.QWizard.FinishButton, "&Finish")
super().cleanupPage(page_id)
def validateCurrentPage(self):
"""
Validates if an appliance can be installed.
"""
if self.currentPage() == self.uiSelectTemplateSourceWizardPage and not Controller.instance().connected():
QtWidgets.QMessageBox.critical(self, "New template", "There is no connection to the server")
return False
elif self.currentPage() == self.uiApplianceFromServerWizardPage:
if not self.uiAppliancesTreeWidget.selectedItems():
QtWidgets.QMessageBox.critical(self, "New template", "Please select an appliance to install!")
return False
return True
def nextId(self):
"""
Wizard rules!
"""
current_id = self.currentId()
if self.page(current_id) == self.uiSelectTemplateSourceWizardPage and \
(self.uiImportApplianceFromFileRadioButton.isChecked() or self.uiCreateTemplateManuallyRadioButton.isChecked()):
self.done(True)
return super().nextId()
def done(self, result):
"""
This dialog is closed.
"""
super().done(result)
if result:
#ApplianceManager.instance().appliances_changed_signal.disconnect(self._appliancesChangedSlot)
from gns3.main_window import MainWindow
if self.currentPage() == self.uiApplianceFromServerWizardPage:
items = self.uiAppliancesTreeWidget.selectedItems()
for item in items:
f = tempfile.NamedTemporaryFile(mode="w+", suffix=".builtin.gns3a", delete=False)
json.dump(item.data(0, QtCore.Qt.UserRole), f)
f.close()
MainWindow.instance().loadPath(f.name)
try:
os.remove(f.name)
except OSError:
pass
elif self.uiCreateTemplateManuallyRadioButton.isChecked():
MainWindow.instance().preferencesActionSlot()
elif self.uiImportApplianceFromFileRadioButton.isChecked():
from gns3.main_window import MainWindow
MainWindow.instance().openApplianceActionSlot()

View File

@@ -0,0 +1,56 @@
# -*- 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/>.
"""
Dialog to show node information.
"""
from ..qt import QtWidgets
from ..ui.node_info_dialog_ui import Ui_NodeInfoDialog
class NodeInfoDialog(QtWidgets.QDialog, Ui_NodeInfoDialog):
"""
Node information dialog.
:param parent: parent widget
"""
def __init__(self, node, parent):
super().__init__(parent)
self.setupUi(self)
general_info = node.info()
usage_info = node.usage()
command_line_info = node.commandLine()
self.setWindowTitle(node.name())
# General tab
self.uiGeneralTextBrowser.setPlainText(general_info)
# Usage tab
if not usage_info:
usage_info = "No usage information has been provided for this node."
self.uiUsageTextBrowser.setPlainText(usage_info)
# Command line tab
if command_line_info is None:
command_line_info = "Command line information is not supported for this type of node."
elif len(command_line_info) == 0:
command_line_info = "Please start the node in order to get the command line information."
self.uiCommandLineTextBrowser.setPlainText(command_line_info)

View File

@@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Display error to the user in an overlay popup
"""
import time
from gns3.qt import QtWidgets, QtCore, qslot
import logging
log = logging.getLogger(__name__)
MAX_ELEMENTS = 3
DISPLAY_DURATION = {
"CRITICAL": 120,
"ERROR": 120,
"WARNING": 20,
"INFO": 5
}
class NotifDialogHandler(logging.StreamHandler):
def __init__(self, dialog):
super().__init__()
self._dialog = dialog
self.setLevel(logging.INFO)
self._dialog.show()
def emit(self, record):
self._dialog.addNotif(record.levelname, record.getMessage())
class NotifDialog(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self._notifs = []
self.setWindowFlags(QtCore.Qt.FramelessWindowHint |
QtCore.Qt.WindowDoesNotAcceptFocus |
QtCore.Qt.SubWindow)
# QtCore.Qt.Tool)
# QtCore.Qt.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) # | QtCore.Qt.WA_TranslucentBackground)
self._layout = QtWidgets.QVBoxLayout()
self._timer = QtCore.QTimer()
self._timer.setInterval(1000)
self._timer.timeout.connect(self._refreshSlot)
self._timer.start()
for i in range(0, MAX_ELEMENTS):
l = QtWidgets.QLabel()
l.setAlignment(QtCore.Qt.AlignTop)
l.setWordWrap(True)
l.hide()
self._layout.addWidget(l)
self.setLayout(self._layout)
@qslot
def addNotif(self, level, message):
if not self.parent().settings().get("overlay_notifications", True):
return
# This unicode char prevent the wordwrap at /
message = message.replace("/", "\u2060/\u2060")
if len(self._notifs) == MAX_ELEMENTS:
self._notifs.pop(0)
self._notifs.append((level, message, time.time()))
self.update()
@qslot
def _refreshSlot(self):
"""
Hide the notifs after some delay
"""
notifs = []
for (i, (level, message, when)) in enumerate(self._notifs):
if when + DISPLAY_DURATION[level] > time.time():
notifs.append((level, message, when))
if notifs != self._notifs:
self._notifs = notifs
self.update()
elif len(notifs) > 0:
self.resize()
def update(self):
if len(self._notifs) == 0:
self.hide()
else:
for (i, (level, message, when)) in enumerate(self._notifs):
w = self._layout.itemAt(i).widget()
w.setText(message)
if level == "ERROR" or level == "CRITICAL":
w.setStyleSheet("""
color: black;
padding-left: 12px;
background-color: rgb(247, 205, 198);
border-left: 10px solid red;
""")
elif level == "WARNING":
w.setStyleSheet("""
color: black;
padding-left: 12px;
background-color: #f4f2b5;
border-left: 10px solid orange;
""")
elif level == "INFO":
w.setStyleSheet("""
color: black;
padding-left: 12px;
background-color: #cfffc9;
border-left: 10px solid green;
""")
w.show()
for i in range(i + 1, MAX_ELEMENTS):
w = self._layout.itemAt(i).widget()
w.hide()
self.resize()
self.show()
def resize(self):
x = self.parent().width() - self.width() - 10
y = 10
self.setGeometry(x, y, self.sizeHint().width(), self.sizeHint().height())
@qslot
def mousePressEvent(self, event):
self._notifs.clear()
self.update()
if __name__ == '__main__':
"""
A demo main for testing the features
"""
import sys
app = QtWidgets.QApplication(sys.argv)
logging.basicConfig(level=logging.INFO)
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
l1 = QtWidgets.QLabel()
l1.setText("Hello World")
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(l1)
self.setLayout(vbox)
self.setStyleSheet("background-color:blue;")
self._dialog = NotifDialog(self)
log.addHandler(NotifDialogHandler(self._dialog))
log.info("test")
def moveEvent(self, event):
log.error("An error")
log.info("An info with an url http://test")
log.warning("A warning with a long long long longlong longlong longlong longlong longlong longlong longlong long message")
self._dialog.update()
def resizeEvent(self, event):
self._dialog.update()
main = MainWindow()
main.setMinimumWidth(600)
main.setMinimumHeight(600)
main.show()
exit_code = app.exec_()

View File

@@ -184,10 +184,17 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
self.uiTitleLabel.setText("{} preferences".format(name))
index = self.uiStackedWidget.indexOf(preferences_page)
widget = self.uiStackedWidget.widget(index)
# self.uiStackedWidget.setMinimumSize(widget.size())
self.uiStackedWidget.resize(widget.size())
#self.uiStackedWidget.setMinimumSize(widget.size()) # FIXME: this seems to not work on Windows and OSX
#self.uiStackedWidget.resize(widget.size())
self.uiStackedWidget.setCurrentIndex(index)
for index in range(0, self.uiStackedWidget.count()):
page = self.uiStackedWidget.widget(index)
if self.uiStackedWidget.currentIndex() == index:
page.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
else:
page.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
def _applyPreferences(self):
"""
Saves all the preferences.
@@ -226,11 +233,5 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
Saves the preferences and closes this dialog.
"""
# close the nodes dock to refresh the node list
from ..main_window import MainWindow
main_window = MainWindow.instance()
main_window.uiNodesDockWidget.setVisible(False)
main_window.uiNodesDockWidget.setWindowTitle("")
if self._applyPreferences():
QtWidgets.QDialog.accept(self)

View File

@@ -22,6 +22,7 @@ import shutil
from gns3.qt import QtWidgets
from gns3.local_config import LocalConfig
from gns3.ui.profile_select_dialog_ui import Ui_ProfileSelectDialog
from gns3.version import __version_info__
import logging
log = logging.getLogger(__name__)
@@ -39,8 +40,8 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
self._main.hide()
parent = self._main
super().__init__(parent)
self.setupUi(self)
self.setupUi(self)
self.uiNewPushButton.clicked.connect(self._newPushButtonSlot)
self.uiDeletePushButton.clicked.connect(self._deletePushButtonSlot)
@@ -48,12 +49,13 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
screen = QtWidgets.QApplication.desktop().screenGeometry()
self.move(screen.center() - self.rect().center())
version = "{}.{}".format(__version_info__[0], __version_info__[1])
if sys.platform.startswith("win"):
appdata = os.path.expandvars("%APPDATA%")
path = os.path.join(appdata, "GNS3")
path = os.path.join(appdata, "GNS3", version)
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3")
path = os.path.join(home, ".config", "GNS3", version)
self.profiles_path = os.path.join(path, "profiles")
self.uiShowAtStartupCheckBox.setChecked(LocalConfig.instance().multiProfiles())
@@ -65,9 +67,9 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
try:
if os.path.exists(self.profiles_path):
for profil in sorted(os.listdir(self.profiles_path)):
if not profil.startswith("."):
self.uiProfileSelectComboBox.addItem(profil)
for profile in sorted(os.listdir(self.profiles_path)):
if not profile.startswith("."):
self.uiProfileSelectComboBox.addItem(profile)
except OSError:
pass
@@ -79,7 +81,7 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
super().accept()
def _newPushButtonSlot(self):
profile, ok = QtWidgets.QInputDialog.getText(self.parent(), "New profile", "Profile name:")
profile, ok = QtWidgets.QInputDialog.getText(self, "New profile", "Profile name:")
if ok:
self.uiProfileSelectComboBox.addItem(profile)
self.uiProfileSelectComboBox.setCurrentText(profile)
@@ -88,13 +90,13 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
def _deletePushButtonSlot(self):
profile = self.uiProfileSelectComboBox.currentText()
if profile == "default":
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", "You can't delete the default profile")
QtWidgets.QMessageBox.critical(self, "Delete profile", "The default profile cannot be deleted")
else:
try:
shutil.rmtree(os.path.join(self.profiles_path, profile))
self._refresh()
except (OSError, PermissionError) as e:
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", str(e))
QtWidgets.QMessageBox.critical(self, "Cannot delete profile", str(e))
if __name__ == '__main__':

View File

@@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from ..qt import QtCore, QtGui, QtWidgets, qslot
from ..qt import QtCore, QtGui, QtWidgets, qslot, sip_is_deleted
from ..ui.project_dialog_ui import Ui_ProjectDialog
from ..controller import Controller
from ..topology import Topology
@@ -91,6 +91,8 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
projects_to_delete = set()
for project in self.uiProjectsTreeWidget.selectedItems():
if sip_is_deleted(project):
continue
project_id = project.data(0, QtCore.Qt.UserRole)
project_name = project.data(1, QtCore.Qt.UserRole)
@@ -106,6 +108,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
Controller.instance().deleteProject(project_id)
def _duplicateProjectSlot(self):
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
QtWidgets.QMessageBox.critical(self, "Duplicate project", "No project selected")
return
@@ -132,11 +135,23 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
new_project_name)
name = name.strip()
if reply and len(name) > 0:
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id), self._duplicateCallback, body={"name": name})
if Controller.instance().isRemote():
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
self._duplicateCallback,
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},
progressText="Duplicating project '{}'...".format(name),
timeout=None)
def _duplicateCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while duplicate project: {}".format(result["message"]))
log.error("Error while duplicating project: {}".format(result["message"]))
return
Controller.instance().refreshProjectList()
@@ -260,17 +275,17 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
for existing_project in Controller.instance().projects():
if self._project_settings["project_name"] == existing_project["name"] \
or ("project_files_dir" in self._project_settings and self._project_settings["project_files_dir"] == existing_project["path"]):
and ("project_files_dir" in self._project_settings and self._project_settings["project_files_dir"] == existing_project["path"]):
if existing_project["status"] == "opened":
QtWidgets.QMessageBox.critical(self,
"New project",
"Project {} is open you can not overwrite it".format(self._project_settings["project_name"]))
'Project "{}" is opened, it cannot be overwritten'.format(self._project_settings["project_name"]))
return False
reply = QtWidgets.QMessageBox.warning(self,
"New project",
"Project {} already exists, overwrite it?".format(existing_project["name"]),
'Project "{}" already exists in location "{}", overwrite it?'.format(existing_project["name"], existing_project["path"]),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)

View File

@@ -0,0 +1,131 @@
# -*- 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 sys
import datetime
from gns3.qt import QtCore, QtWidgets
from ..local_server import LocalServer
from ..utils.progress_dialog import ProgressDialog
from ..utils.export_project_worker import ExportProjectWorker
from ..ui.export_project_wizard_ui import Ui_ExportProjectWizard
import logging
log = logging.getLogger(__name__)
class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
"""
Export project wizard.
"""
def __init__(self, project, parent):
super().__init__(parent)
self.setupUi(self)
self._project = project
self._path = None
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
self.uiCompressionComboBox.addItem("None", "none")
self.uiCompressionComboBox.addItem("Zip compression (deflate)", "zip")
self.uiCompressionComboBox.addItem("Bzip2 compression", "bzip2")
self.uiCompressionComboBox.addItem("Lzma compression", "lzma")
# set zip compression by default
self.uiCompressionComboBox.setCurrentIndex(1)
self.helpRequested.connect(self._showHelpSlot)
self.uiPathBrowserToolButton.clicked.connect(self._pathBrowserSlot)
readme_text = "Project: '{}' created on {}\nAuthor: John Doe <john.doe@example.com>\n\nNo project description was given".format(self._project.name(), datetime.date.today())
self.uiReadmeTextEdit.setPlainText(readme_text)
def _pathBrowserSlot(self):
directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
if len(directory) == 0:
directory = LocalServer.instance().localServerSettings()["projects_path"]
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export portable project", directory,
"GNS3 Portable Project (*.gns3project *.gns3p)",
"GNS3 Portable Project (*.gns3project *.gns3p)")
if path is None or len(path) == 0:
return
self.uiPathLineEdit.setText(path)
def _showHelpSlot(self):
include_image_help = """Including base images means additional images will not be requested to
import the project on another computer, however the resulting file will be much bigger.
Also, you are responsible to check if you have the right to distribute the image(s) as part of the project.
"""
QtWidgets.QMessageBox.information(self, "Help about export a project", include_image_help)
def validateCurrentPage(self):
"""
Validates if the project can be exported.
"""
if self.currentPage() == self.uiExportOptionsWizardPage:
path = self.uiPathLineEdit.text().strip()
if not path:
QtWidgets.QMessageBox.critical(self, "Export project", "Please select a path where to export the project")
return False
if not path.endswith(".gns3project") and not path.endswith(".gns3p"):
path += ".gns3project"
try:
open(path, 'wb+').close()
except OSError as e:
QtWidgets.QMessageBox.critical(self, "Export project", "Cannot export project to '{}': {}".format(path, e))
return False
self._path = path
elif self.currentPage() == self.uiProjectReadmeWizardPage:
text = self.uiReadmeTextEdit.toPlainText().strip()
if text:
self._project.post("/files/README.txt", self._saveReadmeCallback, body=text)
return True
def _saveReadmeCallback(self, result, error=False, **kwargs):
if error:
QtWidgets.QMessageBox.critical(self, "Export project", "Could not created readme file")
def done(self, result):
"""
This dialog is closed.
"""
if result:
if self.uiIncludeImagesCheckBox.isChecked():
include_images = "yes"
else:
include_images = "no"
if self.uiIncludeSnapshotsCheckBox.isChecked():
include_snapshots = "yes"
else:
include_snapshots = "no"
compression = self.uiCompressionComboBox.currentData()
export_worker = ExportProjectWorker(self._project, self._path, include_images, include_snapshots, compression)
progress_dialog = ProgressDialog(export_worker, "Exporting project", "Exporting portable project files...", "Cancel", parent=self, create_thread=False)
progress_dialog.show()
progress_dialog.exec_()
super().done(result)

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import copy
from gns3.qt import QtWidgets, QtCore, qpartial
from gns3.ui.project_welcome_dialog_ui import Ui_ProjectWelcomeDialog
import logging
log = logging.getLogger(__name__)
class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
"""
This dialog shows when project is imported and global variables assigned to the project are missing.
"""
def __init__(self, parent, project):
super().__init__(parent)
self._project = project
self.setupUi(self)
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
self.gridLayout.setAlignment(QtCore.Qt.AlignTop)
self.label.setOpenExternalLinks(True)
self._variables = self._getVariables(project)
self._loadReadme()
self._addMisingVariablesEdits()
def _getVariables(self, project):
variables = copy.copy(self._project.variables())
if variables is None:
variables = []
return variables
def _addMisingVariablesEdits(self):
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
for i, variable in enumerate(missing, start=0):
nameLabel = QtWidgets.QLabel()
nameLabel.setText(variable.get("name", ""))
self.gridLayout.addWidget(nameLabel, i, 0)
valueEdit = QtWidgets.QLineEdit()
valueEdit.setText(variable.get("value", ""))
valueEdit.textChanged.connect(qpartial(self.onValueChange, variable))
self.gridLayout.addWidget(valueEdit, i, 1)
def _loadReadme(self):
self._project.get("/files/README.txt", self._loadedReadme)
def _loadedReadme(self, result, error=False, raw_body=None, context={}, **kwargs):
if not error:
self.label.setText(raw_body.decode("utf-8"))
def onValueChange(self, variable, text):
variable["value"] = text
def _okButtonClickedSlot(self):
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
if len(missing) > 0:
reply = QtWidgets.QMessageBox.warning(
self, 'Missing values',
'Are you sure you want to continue without providing missing values?',
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return
self._project.setVariables(self._variables)
self._project.update()
self.accept()

View File

@@ -22,8 +22,7 @@ import shutil
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork, qslot
from gns3.controller import Controller
from gns3.local_server import LocalServer
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
from gns3.utils.interfaces import interfaces
from ..settings import DEFAULT_LOCAL_SERVER_HOST
from ..ui.setup_wizard_ui import Ui_SetupWizard
@@ -44,6 +43,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
super().__init__(parent)
self.setupUi(self)
self.adjustSize()
self._gns3_vm_settings = {
"enable": True,
@@ -62,7 +62,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
self.uiLocalServerToolButton.clicked.connect(self._localServerBrowserSlot)
self.uiGNS3VMDownloadLinkUrlLabel.setText('')
self.uiGNS3VMDownloadLinkUrlLabel.setText("")
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
self.uiVirtualBoxRadioButton.clicked.connect(self._listVirtualBoxVMsSlot)
@@ -81,20 +81,22 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
# load all available addresses
for address in QtNetwork.QNetworkInterface.allAddresses():
address_string = address.toString()
if address.protocol() != QtNetwork.QAbstractSocket.IPv6Protocol:
self.uiLocalServerHostComboBox.addItem(address_string, address.toString())
if address.protocol() in [QtNetwork.QAbstractSocket.IPv4Protocol, QtNetwork.QAbstractSocket.IPv6Protocol]:
address_string = address.toString()
if address_string.startswith("169.254") or address_string.startswith("fe80"):
# ignore link-local addresses, could not use https://doc.qt.io/qt-5/qhostaddress.html#isLinkLocal
# because it was introduced in Qt 5.11
continue
self.uiLocalServerHostComboBox.addItem(address_string, address_string)
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")
self.uiLocalRadioButton.setText("Run the topologies on my computer")
self.uiLocalRadioButton.setChecked(True)
self.uiLocalLabel.setVisible(False)
self.uiLocalLabel.setText("Dependencies like Dynamips and Qemu must be manually installed")
Controller.instance().connected_signal.connect(self._refreshLocalServerStatusSlot)
Controller.instance().connection_failed_signal.connect(self._refreshLocalServerStatusSlot)
@@ -116,9 +118,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):
@@ -141,7 +143,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
Slot to refresh the VirtualBox VMs list.
"""
QtWidgets.QMessageBox.warning(self, "GNS3 VM on VirtualBox", "VirtualBox doesn't support nested virtualization, this means running Qemu based VM could be very slow")
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(version=__version__)
self.uiGNS3VMDownloadLinkUrlLabel.setText('If you don\'t have the GNS3 Virtual Machine you can <a href="{download_url}">download it here</a>.<br>And import the VM in the virtualization software and hit refresh.'.format(download_url=download_url))
self.uiVmwareRadioButton.setChecked(False)
@@ -201,6 +202,19 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
index = self.uiLocalServerHostComboBox.findData(local_server_settings["host"])
if index != -1:
self.uiLocalServerHostComboBox.setCurrentIndex(index)
else:
if self.uiVMRadioButton.isChecked():
# Try to bind with the IP address allocated for VMnet1
for interface in interfaces():
if "vmnet1" in interface["name"].lower():
index = self.uiLocalServerHostComboBox.findText(interface["ip_address"])
break
else:
index = self.uiLocalServerHostComboBox.findText(DEFAULT_LOCAL_SERVER_HOST)
if index != -1:
self.uiLocalServerHostComboBox.setCurrentIndex(index)
self.uiLocalServerPortSpinBox.setValue(local_server_settings["port"])
elif self.page(page_id) == self.uiRemoteControllerWizardPage:
@@ -216,7 +230,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
self.uiRemoteMainServerPasswordLineEdit.setText(local_server_settings["password"])
self.uiRemoteMainServerPortSpinBox.setValue(local_server_settings["port"])
self.uiRemoteMainServerProtocolComboBox.setCurrentText(local_server_settings["protocol"])
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
self._refreshLocalServerStatusSlot()
@@ -246,14 +259,16 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
Refresh the local server status page
"""
self.uiLocalServerTextEdit.clear()
if Controller.instance().connected():
self.uiLocalServerStatusLabel.setText("Connection to local server successful")
self.uiLocalServerTextEdit.setText("Connection to the local GNS3 server has been successful!")
Controller.instance().get("/gns3vm", self._getSettingsCallback)
elif Controller.instance().connecting():
self.uiLocalServerStatusLabel.setText("Please wait connection to the GNS3 server")
self.uiLocalServerTextEdit.setText("Please wait connection to the GNS3 server...")
else:
local_server_settings = LocalServer.instance().localServerSettings()
self.uiLocalServerStatusLabel.setText("Connection to local server failed.\n* Make sure GNS3 is allowed in your firewall.\n* Go back and try to change the server port\n* Please check with a browser if you can connect to {protocol}://{host}:{port}.\n* Try to run {path} in a terminal to see if you have an error if the above does not work.".format(protocol=local_server_settings["protocol"], host=local_server_settings["host"], port=local_server_settings["port"], path=local_server_settings["path"]))
self.uiLocalServerTextEdit.setText("Connection to local server failed. Please try one of the following:\n\n- Make sure GNS3 is allowed to run by your firewall.\n- Go back and try to change the server host binding and/or the port\n- Check with a browser if you can connect to {protocol}://{host}:{port}.\n- Try to run {path} in a terminal to see if you have an error.".format(protocol=local_server_settings["protocol"], host=local_server_settings["host"], port=local_server_settings["port"], path=local_server_settings["path"]))
def _GNS3VMSettings(self):
return self._gns3_vm_settings
@@ -264,7 +279,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):
@@ -324,14 +339,15 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
return False
LocalServer.instance().updateLocalServerSettings(local_server_settings)
LocalServer.instance().localServerAutoStartIfRequire()
if not LocalServer.instance().localServerAutoStartIfRequired():
return False
elif self.currentPage() == self.uiRemoteControllerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
local_server_settings["auto_start"] = False
local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
local_server_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText()
local_server_settings["protocol"] = "http"
local_server_settings["user"] = self.uiRemoteMainServerUserLineEdit.text()
local_server_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
local_server_settings["auth"] = self.uiRemoteMainServerAuthCheckBox.isChecked()
@@ -402,8 +418,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
if local_server_settings["host"] is None:
local_server_settings["host"] = DEFAULT_LOCAL_SERVER_HOST
LocalServer.instance().updateLocalServerSettings(local_server_settings)
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
settings["hide_setup_wizard"] = not self.uiShowCheckBox.isChecked()
self.parentWidget().setSettings(settings)
super().done(result)

View File

@@ -85,12 +85,18 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
if ok and snapshot_name and self._project:
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()), self._createSnapshotsCallback, {"name": snapshot_name})
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()),
self._createSnapshotsCallback,
{"name": snapshot_name},
progressText="Creation of snapshot '{}' in progress...".format(snapshot_name),
timeout=None)
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])
else:
log.error("Cannot create snapshot of project")
return
self._listSnapshots()
@@ -105,6 +111,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
Controller.instance().delete("/projects/{}/snapshots/{}".format(self._project.id(), snapshot_id), self._deleteSnapshotsCallback)
def _deleteSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])
@@ -127,13 +134,16 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
:param snapshot_id: id of the snapshot
"""
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken, would you like to proceed?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
if reply == QtWidgets.QMessageBox.Cancel:
return
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback)
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id),
self._restoreSnapshotsCallback, progressText="Restoring snapshot...", timeout=None)
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])

View File

@@ -21,6 +21,7 @@ Style editor to edit Shape items.
from ..qt import QtCore, QtWidgets, QtGui
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
from ..items.shape_item import ShapeItem
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
@@ -52,12 +53,18 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
# use the first item in the list as the model
first_item = items[0]
pen = first_item.pen()
brush = first_item.brush()
self._color = brush.color()
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
self._color.green(),
self._color.blue(),
self._color.alpha()))
if hasattr(first_item, "brush"): # Line don't have brush
brush = first_item.brush()
self._color = brush.color()
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
self._color.green(),
self._color.blue(),
self._color.alpha()))
else:
self.uiColorLabel.hide()
self.uiColorPushButton.hide()
self._color = None
self._border_color = pen.color()
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
self._border_color.green(),
@@ -102,11 +109,17 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
border_style = QtCore.Qt.PenStyle(self.uiBorderStyleComboBox.itemData(self.uiBorderStyleComboBox.currentIndex()))
pen = QtGui.QPen(self._border_color, self.uiBorderWidthSpinBox.value(), border_style, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
brush = QtGui.QBrush(self._color)
if self._color:
brush = QtGui.QBrush(self._color)
else:
brush = None
for item in self._items:
item.setPen(pen)
item.setBrush(brush)
# on multiselection it's possible to select many type of items
# but brush can be applied only on ShapeItem,
if brush and isinstance(item, ShapeItem):
item.setBrush(brush)
item.setRotation(self.uiRotationSpinBox.value())
def done(self, result):

View File

@@ -22,10 +22,9 @@ 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
from ..controller import Controller
from ..symbol import Symbol
@@ -56,7 +55,6 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
self.uiCustomSymbolRadioButton.toggled.connect(self._customSymbolToggledSlot)
self.uiBuiltInSymbolRadioButton.toggled.connect(self._builtInSymbolToggledSlot)
self.uiSearchLineEdit.textChanged.connect(self._searchTextChangedSlot)
self.uiBuiltinSymbolOnlyCheckBox.toggled.connect(self._builtinSymbolOnlyToggledSlot)
if not SymbolSelectionDialog._symbols_dir:
SymbolSelectionDialog._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
@@ -64,9 +62,10 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
self.uiBuiltInSymbolRadioButton.setChecked(True)
self.uiSymbolListWidget.setFocus()
self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
self.uiSymbolTreeWidget.setFocus()
self.uiSymbolTreeWidget.setIconSize(QtCore.QSize(64, 64))
self._symbol_items = []
self._parents = {}
Controller.instance().get("/symbols", self._listSymbolsCallback)
@@ -78,33 +77,39 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
self._symbol_items = []
for symbol in result:
symbol = Symbol(**symbol)
name = os.path.splitext(symbol.filename())[0]
item = QtWidgets.QListWidgetItem(self.uiSymbolListWidget)
item.setData(QtCore.Qt.UserRole, symbol)
self._symbol_items.append(item)
item.setText(name)
theme = symbol.theme()
if theme not in self._parents:
parent = QtWidgets.QTreeWidgetItem(self.uiSymbolTreeWidget)
parent.setText(0, theme)
font = parent.font(0)
font.setBold(True)
parent.setFont(0, font)
parent.setFlags(parent.flags() & ~QtCore.Qt.ItemIsSelectable)
self._parents[theme] = parent
else:
parent = self._parents[theme]
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
# Set the ARGB to 0 to prevent rendering artifacts
image.fill(0x00000000)
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
item.setIcon(icon)
name = os.path.splitext(symbol.filename())[0]
item = QtWidgets.QTreeWidgetItem(parent)
item.setData(0, QtCore.Qt.UserRole, symbol)
item.setToolTip(0, symbol.id())
self._symbol_items.append(item)
item.setText(0, name)
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
image.fill(0x00000000)
svg_renderer.render(QtGui.QPainter(image))
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
item.setIcon(icon)
item.setIcon(0, icon)
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
self.adjustSize()
def _builtinSymbolOnlyToggledSlot(self, checked):
self._filter()
def _searchTextChangedSlot(self, text):
self._filter()
@@ -114,10 +119,10 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
"""
text = self.uiSearchLineEdit.text()
for item in self._symbol_items:
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not item.data(QtCore.Qt.UserRole).builtin():
if not item.data(0, QtCore.Qt.UserRole).builtin():
item.setHidden(True)
else:
if len(text.strip()) == 0 or text.strip().lower() in item.text().lower():
if len(text.strip()) == 0 or text.strip().lower() in item.text(0).lower():
item.setHidden(False)
else:
item.setHidden(True)
@@ -156,16 +161,18 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
"""
symbol_path = self.getSymbol()
if not symbol_path:
return False
for item in self._items:
item.setSymbol(symbol_path)
return True
def getSymbol(self):
if self.uiSymbolListWidget.isEnabled():
current = self.uiSymbolListWidget.currentItem()
if current:
return current.data(QtCore.Qt.UserRole).id()
if self.uiSymbolTreeWidget.isEnabled():
current = self.uiSymbolTreeWidget.currentItem()
if current and current.parent():
return current.data(0, QtCore.Qt.UserRole).id()
else:
return os.path.basename(self.uiSymbolLineEdit.text())
return None
@@ -184,7 +191,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
if error:
log.error("Error while uploading symbol: {}".format(path))
log.error("Error while uploading symbol: {}: {}".format(path, result.get("message", "unknown")))
return
self.uiSymbolLineEdit.clear()
self.uiSymbolLineEdit.setText(path)

View File

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

View File

@@ -88,7 +88,7 @@ class VMWithImagesWizard(VMWizard):
self._radio_existing_images_buttons.add(radio_button)
def _imageCreateSlot(self, line_edit, create_image_wizard, image_suffix):
create_dialog = create_image_wizard(self, self.getSettings()["server"], self.uiNameLineEdit.text() + image_suffix)
create_dialog = create_image_wizard(self, self.getSettings()["compute_id"], self.uiNameLineEdit.text() + image_suffix)
if QtWidgets.QDialog.Accepted == create_dialog.exec_():
line_edit.setText(create_dialog.uiLocationLineEdit.text())

View File

@@ -21,8 +21,8 @@ Graphical view on the scene where items are drawn.
import logging
import os
import sip
import pickle
from .qt import sip
import sys
from .qt import QtCore, QtGui, QtNetwork, QtWidgets, qpartial, qslot
from .items.node_item import NodeItem
@@ -31,38 +31,43 @@ 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 .template_manager import TemplateManager
from .dialogs.style_editor_dialog import StyleEditorDialog
from .dialogs.text_editor_dialog import TextEditorDialog
from .dialogs.symbol_selection_dialog import SymbolSelectionDialog
from .dialogs.idlepc_dialog import IdlePCDialog
from .dialogs.console_command_dialog import ConsoleCommandDialog
from .dialogs.file_editor_dialog import FileEditorDialog
from .dialogs.node_info_dialog import NodeInfoDialog
from .local_config import LocalConfig
from .progress import Progress
from .utils.server_select import server_select
from .compute_manager import ComputeManager
from .utils.get_icon import get_icon
# link items
from .items.link_item import LinkItem
from .items.link_item import LinkItem, SvgIconItem
from .items.ethernet_link_item import EthernetLinkItem
from .items.serial_link_item import SerialLinkItem
# other items
from .items.note_item import NoteItem
from .items.label_item import LabelItem
from .items.text_item import TextItem
from .items.shape_item import ShapeItem
from .items.drawing_item import DrawingItem
from .items.rectangle_item import RectangleItem
from .items.line_item import LineItem
from .items.ellipse_item import EllipseItem
from .items.image_item import ImageItem
from .items.logo_item import LogoItem
log = logging.getLogger(__name__)
class GraphicsView(QtWidgets.QGraphicsView):
"""
Graphics view that displays the scene.
@@ -82,8 +87,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._adding_note = False
self._adding_rectangle = False
self._adding_ellipse = False
self._adding_line = False
self._newlink = None
self._dragging = False
self._grid_size = 75
self._drawing_grid_size = 25
self._last_mouse_position = None
self._topology = Topology.instance()
self._background_warning_msgbox = QtWidgets.QErrorMessage(self)
@@ -103,16 +111,49 @@ class GraphicsView(QtWidgets.QGraphicsView):
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter)
# default directories for QFileDialog
self._import_configs_from_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
self._import_config_dir = ""
self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
self._export_config_dir = ""
self._import_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
self._export_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
self._local_addresses = ['0.0.0.0', '127.0.0.1', 'localhost', '::1', '0:0:0:0:0:0:0:1', '::', QtNetwork.QHostInfo.localHostName()]
def setSceneSize(self, width, height):
self.scene().setSceneRect(-(width / 2), -(height / 2), width, height)
def setZoom(self, zoom):
"""
Sets zoom of the Graphics View
:param zoom:
:return:
"""
if zoom:
factor = zoom / 100.
self.scale(factor, factor)
def setNodeGridSize(self, grid_size):
"""
Sets the grid size for nodes.
"""
self._grid_size = grid_size
def nodeGridSize(self):
"""
Returns the grid size for nodes.
:return: integer
"""
return self._grid_size
def setDrawingGridSize(self, grid_size):
"""
Sets the grid size for drawings
"""
self._drawing_grid_size = grid_size
def drawingGridSize(self):
"""
Returns the grid size for drawings
:return: integer
"""
return self._drawing_grid_size
def setEnabled(self, enabled):
if enabled is False:
@@ -122,6 +163,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
@@ -144,9 +187,13 @@ class GraphicsView(QtWidgets.QGraphicsView):
# clear the topology summary
self._main_window.uiTopologySummaryTreeWidget.clear()
# reset the lock button
self._main_window.uiLockAllAction.setChecked(False)
# clear all objects on the scene
self.scene().clear()
def _loadSettings(self):
"""
Loads the settings from the persistent settings file.
@@ -233,6 +280,20 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._adding_ellipse = False
self.setCursor(QtCore.Qt.ArrowCursor)
def addLine(self, state):
"""
Adds a line.
:param state: boolean
"""
if state:
self._adding_line = True
self.setCursor(QtCore.Qt.PointingHandCursor)
else:
self._adding_line = False
self.setCursor(QtCore.Qt.ArrowCursor)
def addImage(self, image_path):
"""
Adds an image.
@@ -245,6 +306,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.
@@ -365,12 +430,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):
@@ -390,20 +459,15 @@ 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():
if not event.modifiers() & QtCore.Qt.ControlModifier:
for it in self.scene().items():
it.setSelected(False)
if item.zValue() < 0:
item.setFlag(item.ItemIsSelectable, True)
item.setSelected(True)
self._showDeviceContextualMenu(QtGui.QCursor.pos())
if not sip.isdeleted(item) and item.zValue() < 0:
item.setFlag(item.ItemIsSelectable, False)
else:
self._showDeviceContextualMenu(QtGui.QCursor.pos())
# when more than one item is selected display the contextual menu even if mouse is not above an item
@@ -417,7 +481,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._userNodeLinking(event, item)
elif event.button() == QtCore.Qt.LeftButton and self._adding_note:
pos = self.mapToScene(event.pos())
note = self.createDrawingItem("text", pos.x(), pos.y(), 1)
note = self.createDrawingItem("text", pos.x(), pos.y(), 2)
pos_x = note.pos().x()
pos_y = note.pos().y() - (note.boundingRect().height() / 2)
note.setPos(pos_x, pos_y)
@@ -427,19 +491,27 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._adding_note = False
elif event.button() == QtCore.Qt.LeftButton and self._adding_rectangle:
pos = self.mapToScene(event.pos())
self.createDrawingItem("rect", pos.x(), pos.y(), 0)
self.createDrawingItem("rect", pos.x(), pos.y(), 1)
self._main_window.uiDrawRectangleAction.setChecked(False)
self.setCursor(QtCore.Qt.ArrowCursor)
self._adding_rectangle = False
elif event.button() == QtCore.Qt.LeftButton and self._adding_ellipse:
pos = self.mapToScene(event.pos())
self.createDrawingItem("ellipse", pos.x(), pos.y(), 0)
self.createDrawingItem("ellipse", pos.x(), pos.y(), 1)
self._main_window.uiDrawEllipseAction.setChecked(False)
self.setCursor(QtCore.Qt.ArrowCursor)
self._adding_ellipse = False
elif event.button() == QtCore.Qt.LeftButton and self._adding_line:
pos = self.mapToScene(event.pos())
self.createDrawingItem("line", pos.x(), pos.y(), 1)
self._main_window.uiDrawLineAction.setChecked(False)
self.setCursor(QtCore.Qt.ArrowCursor)
self._adding_line = False
else:
super().mousePressEvent(event)
self.toggleUiDeviceMenu()
def mouseReleaseEvent(self, event):
"""
Handles all mouse release events.
@@ -462,6 +534,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
item.setSelected(True)
super().mouseReleaseEvent(event)
self.toggleUiDeviceMenu()
def wheelEvent(self, event):
"""
Handles zoom in or out using the mouse wheel.
@@ -474,6 +548,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
if delta is not None and delta.x() == 0:
# CTRL is pressed then use the mouse wheel to zoom in or out.
self.scaleView(pow(2.0, delta.y() / 240.0))
self._topology.project().setZoom(round(self.transform().m11() * 100))
self._topology.project().update()
else:
super().wheelEvent(event)
@@ -486,6 +562,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
if factor < 0.10 or factor > 10:
return
self.scale(scale_factor, scale_factor)
self._main_window.uiStatusBar.showMessage("Zoom: {}%".format(round(self.transform().m11() * 100)), 2000)
def keyPressEvent(self, event):
"""
@@ -495,9 +572,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
if event.key() == QtCore.Qt.Key_Delete:
# check if we are editing an NoteItem instance, then send the delete key event to it
# check if we are editing an LabelItem instance, then send the delete key event to it
for item in self.scene().selectedItems():
if (isinstance(item, NoteItem) or isinstance(item, TextItem)) and item.hasFocus():
if (isinstance(item, LabelItem) or isinstance(item, TextItem)) and item.hasFocus():
super().keyPressEvent(event)
return
self.deleteActionSlot()
@@ -554,9 +631,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
self.configureSlot()
return
else:
if sys.platform.startswith("win") and item.node().bringToFront():
return
self.consoleFromItems(self.scene().selectedItems())
return
elif isinstance(item, NoteItem) and isinstance(item.parentItem(), NodeItem):
elif isinstance(item, LabelItem) and isinstance(item.parentItem(), NodeItem):
if item.parentItem().node().initialized():
item.parentItem().setSelected(True)
self.changeHostnameActionSlot()
@@ -587,7 +666,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
# check if what is dragged is handled by this view
if event.mimeData().hasFormat("application/x-gns3-node") or event.mimeData().hasFormat("text/uri-list"):
if event.mimeData().hasFormat("text/uri-list") \
or event.mimeData().hasFormat("application/x-gns3-template"):
event.acceptProposedAction()
event.accept()
else:
@@ -601,10 +681,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
# check if what has been dropped is handled by this view
if event.mimeData().hasFormat("application/x-gns3-node"):
data = event.mimeData().data("application/x-gns3-node")
# load the pickled node data
node_data = pickle.loads(data)
if event.mimeData().hasFormat("application/x-gns3-template"):
template_id = event.mimeData().data("application/x-gns3-template").data().decode()
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
if event.keyboardModifiers() == QtCore.Qt.ShiftModifier:
@@ -615,12 +693,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
for node_number in range(integer):
x = event.pos().x() - (150 / 2) + (node_number % max_nodes_per_line) * offset
y = event.pos().y() - (70 / 2) + (node_number // max_nodes_per_line) * offset
node_item = self.createNode(node_data, QtCore.QPoint(x, y))
if node_item is None:
# stop if there is any error
if self.createNodeFromTemplateId(template_id, QtCore.QPoint(x, y)) is False:
event.ignore()
break
else:
self.createNode(node_data, event.pos())
if self.createNodeFromTemplateId(template_id, event.pos()) is False:
event.ignore()
elif event.mimeData().hasFormat("text/uri-list") and event.mimeData().hasUrls():
# This should not arrive but we received bug report with it...
if len(event.mimeData().urls()) == 0:
@@ -659,139 +737,147 @@ class GraphicsView(QtWidgets.QGraphicsView):
return
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configPage"), items)):
# Action: Configure node
configure_action = QtWidgets.QAction("Configure", menu)
configure_action.setIcon(QtGui.QIcon(':/icons/configuration.svg'))
configure_action.setIcon(get_icon("configuration.svg"))
configure_action.triggered.connect(self.configureActionSlot)
menu.addAction(configure_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and item.node().console() is not None, items)):
console_action = QtWidgets.QAction("Console", menu)
console_action.setIcon(get_icon("console.svg"))
console_action.triggered.connect(self.consoleActionSlot)
menu.addAction(console_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole"), items)):
aux_console_action = QtWidgets.QAction("Auxiliary console", menu)
aux_console_action.setIcon(get_icon("aux-console.svg"))
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
menu.addAction(aux_console_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
start_action = QtWidgets.QAction("Start", menu)
start_action.setIcon(get_icon("start.svg"))
start_action.triggered.connect(self.startActionSlot)
menu.addAction(start_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
suspend_action = QtWidgets.QAction("Suspend", menu)
suspend_action.setIcon(get_icon("pause.svg"))
suspend_action.triggered.connect(self.suspendActionSlot)
menu.addAction(suspend_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
stop_action = QtWidgets.QAction("Stop", menu)
stop_action.setIcon(get_icon("stop.svg"))
stop_action.triggered.connect(self.stopActionSlot)
menu.addAction(stop_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
reload_action = QtWidgets.QAction("Reload", menu)
reload_action.setIcon(get_icon("reload.svg"))
reload_action.triggered.connect(self.reloadActionSlot)
menu.addAction(reload_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and item.node().console() is not None, items)):
console_edit_action = QtWidgets.QAction("Custom console", menu)
console_edit_action.setIcon(get_icon("console_edit.svg"))
console_edit_action.triggered.connect(self.customConsoleActionSlot)
menu.addAction(console_edit_action)
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
# Action: Change hostname
change_hostname_action = QtWidgets.QAction("Change hostname", menu)
change_hostname_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
change_hostname_action.setIcon(get_icon("show-hostname.svg"))
change_hostname_action.triggered.connect(self.changeHostnameActionSlot)
menu.addAction(change_hostname_action)
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
# Action: Change symbol
change_symbol_action = QtWidgets.QAction("Change symbol", menu)
change_symbol_action.setIcon(QtGui.QIcon(':/icons/node_conception.svg'))
change_symbol_action.setIcon(get_icon("node_conception.svg"))
change_symbol_action.triggered.connect(self.changeSymbolActionSlot)
menu.addAction(change_symbol_action)
if True in list(map(lambda item: isinstance(item, DrawingItem) or isinstance(item, NodeItem), items)):
duplicate_action = QtWidgets.QAction("Duplicate", menu)
duplicate_action.setIcon(get_icon("duplicate.svg"))
duplicate_action.triggered.connect(self.duplicateActionSlot)
menu.addAction(duplicate_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "info"), items)):
# Action: Show node information
show_node_info_action = QtWidgets.QAction("Show node information", menu)
show_node_info_action.setIcon(get_icon("help.svg"))
show_node_info_action.triggered.connect(self.showNodeInfoSlot)
menu.addAction(show_node_info_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "nodeDir"), items)):
# Action: Show in file manager
show_in_file_manager_action = QtWidgets.QAction("Show in file manager", menu)
show_in_file_manager_action.setIcon(QtGui.QIcon(':/icons/open.svg'))
show_in_file_manager_action.setIcon(get_icon("open.svg"))
show_in_file_manager_action.triggered.connect(self.showInFileManagerSlot)
menu.addAction(show_in_file_manager_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "console"), items)):
console_action = QtWidgets.QAction("Console", menu)
console_action.setIcon(QtGui.QIcon(':/icons/console.svg'))
console_action.triggered.connect(self.consoleActionSlot)
menu.addAction(console_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "console"), items)):
console_edit_action = QtWidgets.QAction("Custom console", menu)
console_edit_action.setIcon(QtGui.QIcon(':/icons/console_edit.svg'))
console_edit_action.triggered.connect(self.customConsoleActionSlot)
menu.addAction(console_edit_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole"), items)):
aux_console_action = QtWidgets.QAction("Auxiliary console", menu)
aux_console_action.setIcon(QtGui.QIcon(':/icons/aux-console.svg'))
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
menu.addAction(aux_console_action)
if sys.platform.startswith("win") and True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"), items)):
# Action: bring console or window to front (Windows only)
bring_to_front_action = QtWidgets.QAction("Bring to front", menu)
bring_to_front_action.setIcon(get_icon("front.svg"))
bring_to_front_action.triggered.connect(self.bringToFrontSlot)
menu.addAction(bring_to_front_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
import_config_action = QtWidgets.QAction("Import config", menu)
import_config_action.setIcon(QtGui.QIcon(':/icons/import_config.svg'))
import_config_action.setIcon(get_icon("import.svg"))
import_config_action.triggered.connect(self.importConfigActionSlot)
menu.addAction(import_config_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
export_config_action = QtWidgets.QAction("Export config", menu)
export_config_action.setIcon(QtGui.QIcon(':/icons/export_config.svg'))
export_config_action.setIcon(get_icon("export.svg"))
export_config_action.triggered.connect(self.exportConfigActionSlot)
menu.addAction(export_config_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
export_config_action = QtWidgets.QAction("Edit config", menu)
export_config_action.setIcon(QtGui.QIcon(':/icons/edit.svg'))
export_config_action.setIcon(get_icon("edit.svg"))
export_config_action.triggered.connect(self.editConfigActionSlot)
menu.addAction(export_config_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)):
idlepc_action = QtWidgets.QAction("Idle-PC", menu)
idlepc_action.setIcon(QtGui.QIcon(':/icons/calculate.svg'))
idlepc_action.setIcon(get_icon("calculate.svg"))
idlepc_action.triggered.connect(self.idlepcActionSlot)
menu.addAction(idlepc_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)):
auto_idlepc_action = QtWidgets.QAction("Auto Idle-PC", menu)
auto_idlepc_action.setIcon(QtGui.QIcon(':/icons/calculate.svg'))
auto_idlepc_action.setIcon(get_icon("calculate.svg"))
auto_idlepc_action.triggered.connect(self.autoIdlepcActionSlot)
menu.addAction(auto_idlepc_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
start_action = QtWidgets.QAction("Start", menu)
start_action.setIcon(QtGui.QIcon(':/icons/start.svg'))
start_action.triggered.connect(self.startActionSlot)
menu.addAction(start_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
suspend_action = QtWidgets.QAction("Suspend", menu)
suspend_action.setIcon(QtGui.QIcon(':/icons/pause.svg'))
suspend_action.triggered.connect(self.suspendActionSlot)
menu.addAction(suspend_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
stop_action = QtWidgets.QAction("Stop", menu)
stop_action.setIcon(QtGui.QIcon(':/icons/stop.svg'))
stop_action.triggered.connect(self.stopActionSlot)
menu.addAction(stop_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
reload_action = QtWidgets.QAction("Reload", menu)
reload_action.setIcon(QtGui.QIcon(':/icons/reload.svg'))
reload_action.triggered.connect(self.reloadActionSlot)
menu.addAction(reload_action)
if True in list(map(lambda item: isinstance(item, DrawingItem), items)):
duplicate_action = QtWidgets.QAction("Duplicate", menu)
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
duplicate_action.triggered.connect(self.duplicateActionSlot)
menu.addAction(duplicate_action)
if True in list(map(lambda item: isinstance(item, NoteItem), items)):
if True in list(map(lambda item: isinstance(item, LabelItem), items)):
text_edit_action = QtWidgets.QAction("Text edit", menu)
text_edit_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
text_edit_action.setIcon(get_icon("show-hostname.svg"))
text_edit_action.triggered.connect(self.textEditActionSlot)
menu.addAction(text_edit_action)
if True in list(map(lambda item: isinstance(item, TextItem), items)):
text_edit_action = QtWidgets.QAction("Text edit", menu)
text_edit_action.setIcon(QtGui.QIcon(':/icons/edit.svg'))
text_edit_action.setIcon(get_icon("edit.svg"))
text_edit_action.triggered.connect(self.textEditActionSlot)
menu.addAction(text_edit_action)
if True in list(map(lambda item: isinstance(item, ShapeItem), items)):
if True in list(map(lambda item: isinstance(item, ShapeItem) or isinstance(item, LineItem), items)):
style_action = QtWidgets.QAction("Style", menu)
style_action.setIcon(QtGui.QIcon(':/icons/drawing.svg'))
style_action.setIcon(get_icon("node_conception.svg"))
style_action.triggered.connect(self.styleActionSlot)
menu.addAction(style_action)
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "commandLine"), items)):
# Action: Get command line
show_in_file_manager_action = QtWidgets.QAction("Command line", menu)
show_in_file_manager_action.setIcon(QtGui.QIcon(':/icons/console.svg'))
show_in_file_manager_action.triggered.connect(self.getCommandLineSlot)
menu.addAction(show_in_file_manager_action)
if True in list(map(lambda item: isinstance(item, NoteItem), items)) and False in list(map(lambda item: item.parentItem() is None, items)):
if True in list(map(lambda item: isinstance(item, LabelItem), items)) and False in list(map(lambda item: item.parentItem() is None, items)):
# action only for port labels
reset_label_position_action = QtWidgets.QAction("Reset position", menu)
reset_label_position_action.setIcon(QtGui.QIcon(':/icons/reset.svg'))
reset_label_position_action.setIcon(get_icon("reset.svg"))
reset_label_position_action.triggered.connect(self.resetLabelPositionActionSlot)
menu.addAction(reset_label_position_action)
@@ -800,27 +886,41 @@ class GraphicsView(QtWidgets.QGraphicsView):
if len(items) > 1:
horizontal_align_action = QtWidgets.QAction("Align horizontally", menu)
horizontal_align_action.setIcon(QtGui.QIcon(':/icons/horizontally.svg'))
horizontal_align_action.setIcon(get_icon("horizontally.svg"))
horizontal_align_action.triggered.connect(self.horizontalAlignmentSlot)
menu.addAction(horizontal_align_action)
vertical_align_action = QtWidgets.QAction("Align vertically", menu)
vertical_align_action.setIcon(QtGui.QIcon(':/icons/vertically.svg'))
vertical_align_action.setIcon(get_icon("vertically.svg"))
vertical_align_action.triggered.connect(self.verticalAlignmentSlot)
menu.addAction(vertical_align_action)
raise_layer_action = QtWidgets.QAction("Raise one layer", menu)
raise_layer_action.setIcon(QtGui.QIcon(':/icons/raise_z_value.svg'))
raise_layer_action.setIcon(get_icon("raise_z_value.svg"))
raise_layer_action.triggered.connect(self.raiseLayerActionSlot)
menu.addAction(raise_layer_action)
lower_layer_action = QtWidgets.QAction("Lower one layer", menu)
lower_layer_action.setIcon(QtGui.QIcon(':/icons/lower_z_value.svg'))
lower_layer_action.setIcon(get_icon("lower_z_value.svg"))
lower_layer_action.triggered.connect(self.lowerLayerActionSlot)
menu.addAction(lower_layer_action)
if len(items) > 1:
lock_action = QtWidgets.QAction("Lock or unlock items", menu)
lock_action.setIcon(get_icon("lock.svg"))
else:
item = items[0]
if item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable:
lock_action = QtWidgets.QAction("Lock item", menu)
lock_action.setIcon(get_icon("lock.svg"))
else:
lock_action = QtWidgets.QAction("Unlock item", menu)
lock_action.setIcon(get_icon("unlock.svg"))
lock_action.triggered.connect(self.lockActionSlot)
menu.addAction(lock_action)
delete_action = QtWidgets.QAction("Delete", menu)
delete_action.setIcon(QtGui.QIcon(':/icons/delete.svg'))
delete_action.setIcon(get_icon("delete.svg"))
delete_action.triggered.connect(self.deleteActionSlot)
menu.addAction(delete_action)
@@ -952,6 +1052,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
# returns True to ignore this node.
return True
# TightVNC has lack support of IPv6 host at this moment
if "vncviewer" in node.consoleCommand() and ":" in node.consoleHost():
QtWidgets.QMessageBox.warning(
self, "TightVNC", "TightVNC (vncviewer) may not start because of lack of IPv6 support.")
try:
node.openConsole(aux=aux)
except (OSError, ValueError) as e:
@@ -969,7 +1074,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
nodes = {}
node_initialized = False
for item in items:
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized():
if isinstance(item, NodeItem) and item.node().console() is not None and item.node().initialized():
node_initialized = True
if item.node().status() == Node.started:
node = item.node()
@@ -989,6 +1094,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
@@ -1005,8 +1119,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
current_cmd = None
console_type = "telnet"
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
if item.node().consoleType() not in ("telnet", "serial", "vnc"):
if isinstance(item, NodeItem) and item.node().console() is not None and item.node().initialized() and item.node().status() == Node.started:
if item.node().consoleType() not in ("telnet", "serial", "vnc", "spice", "spice+agent"):
continue
current_cmd = item.node().consoleCommand()
console_type = item.node().consoleType()
@@ -1014,9 +1128,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
(ok, cmd) = ConsoleCommandDialog.getCommand(self, console_type=console_type, current=current_cmd)
if ok:
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
if isinstance(item, NodeItem) and item.node().console() is not None and item.node().initialized() and item.node().status() == Node.started:
node = item.node()
if node.consoleType() not in ("telnet", "serial", "vnc"):
if node.consoleType() not in ("telnet", "serial", "vnc", "spice", "spice+agent"):
continue
try:
node.openConsole(command=cmd)
@@ -1083,15 +1197,14 @@ class GraphicsView(QtWidgets.QGraphicsView):
if not ok:
continue
if not self._import_config_dir:
self._import_config_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
path, _ = QtWidgets.QFileDialog.getOpenFileName(self,
"Import {}".format(os.path.basename(config_file)),
self._import_config_dir,
self._import_config_directory,
"All files (*.*);;Config files (*.cfg)",
"Config files (*.cfg)")
self._import_config_dir = os.path.dirname(path)
if not path:
continue
self._import_config_directory = os.path.dirname(path)
item.node().importFile(config_file, path)
def editConfigActionSlot(self):
@@ -1132,44 +1245,39 @@ class GraphicsView(QtWidgets.QGraphicsView):
if not items:
return
if not self._export_configs_to_dir:
self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
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)")
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_config_directory, 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)
self._export_config_directory = os.path.dirname(path)
item.node().exportFile(config_file, path)
def getCommandLineSlot(self):
def showNodeInfoSlot(self):
"""
Slot to receive events from the get command line action in the
Slot to receive events from the show node info action in the
contextual menu.
"""
items = self.scene().selectedItems()
if len(items) != 1:
QtWidgets.QMessageBox.critical(self, "Command line", "Please select only one router")
QtWidgets.QMessageBox.critical(self, "Show node information", "Please select only one node")
return
item = items[0]
if isinstance(item, NodeItem) and hasattr(item.node(), "commandLine"):
router = item.node()
if router.commandLine() is None:
QtWidgets.QMessageBox.warning(self, "Command line", "Get command line is not supported for this type of node.")
elif router.commandLine() == '':
QtWidgets.QMessageBox.warning(self, "Command line", "Please start the node in order to get the command line.")
else:
dialog = QtWidgets.QInputDialog(self)
dialog.setOptions(QtWidgets.QInputDialog.NoButtons)
dialog.setLabelText("Command used to start the VM:")
dialog.setTextValue(router.commandLine())
dialog.show()
dialog.exec_()
if isinstance(item, NodeItem):
dialog = NodeInfoDialog(item.node(), parent=self)
dialog.show()
dialog.exec_()
def bringToFrontSlot(self):
"""
Slot to receive events from the bring to front action in the
contextual menu.
"""
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"):
item.node().bringToFront()
def idlepcActionSlot(self):
"""
@@ -1195,7 +1303,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Error: {}".format(result["message"]))
else:
router = context["router"]
log.info("{} has received Idle-PC proposals".format(router.name()))
log.debug("{} has received Idle-PC proposals".format(router.name()))
idlepcs = result
if idlepcs and idlepcs[0] != "0x0":
dialog = IdlePCDialog(router, idlepcs, parent=self)
@@ -1229,7 +1337,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
else:
router = context["router"]
idlepc = result["idlepc"]
log.info("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
log.debug("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
router.setIdlepc(idlepc)
# apply Idle-PC to all routers with the same IOS image
ios_image = os.path.basename(router.settings()["image"])
@@ -1238,9 +1346,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
node.setIdlepc(idlepc)
# apply the idle-pc to templates with the same IOS image
router.module().updateImageIdlepc(ios_image, idlepc)
QtWidgets.QMessageBox.information(self, "Auto Idle-PC", "Idle-PC value {} has been applied on {} and all routers with IOS image {}".format(idlepc,
router.name(),
ios_image))
QtWidgets.QMessageBox.information(self, "Auto Idle-PC", "Idle-PC value {} has been applied on {} and all templates with IOS image {}".format(idlepc,
router.name(),
ios_image))
def duplicateActionSlot(self):
"""
@@ -1259,6 +1367,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
else:
type = "image"
self.createDrawingItem(type, item.pos().x() + 20, item.pos().y() + 20, item.zValue(), rotation=item.rotation(), svg=item.toSvg())
elif isinstance(item, NodeItem):
item.node().duplicate(item.pos().x() + 20, item.pos().y() + 20, item.zValue())
def styleActionSlot(self):
"""
@@ -1268,7 +1378,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
items = []
for item in self.scene().selectedItems():
if isinstance(item, ShapeItem):
if isinstance(item, ShapeItem) or isinstance(item, LineItem):
items.append(item)
if items:
style_dialog = StyleEditorDialog(self._main_window, items)
@@ -1283,7 +1393,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
items = []
for item in self.scene().selectedItems():
if isinstance(item, NoteItem) or isinstance(item, TextItem):
if isinstance(item, LabelItem) or isinstance(item, TextItem):
items.append(item)
if items:
text_edit_dialog = TextEditorDialog(self._main_window, items)
@@ -1297,7 +1407,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
for item in self.scene().selectedItems():
if isinstance(item, NoteItem) and item.parentItem():
if isinstance(item, LabelItem) and item.parentItem():
links = item.parentItem().links()
for port in item.parentItem().node().ports():
# find the correct port associated with the label
@@ -1346,8 +1456,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
for item in self.scene().selectedItems():
if item.parentItem() is None:
current_zvalue = item.zValue()
item.setZValue(current_zvalue + 1)
if not (item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable):
log.error("Cannot move object to a upper layer because it is locked")
continue
item.setZValue(item.zValue() + 1)
item.updateNode()
item.update()
@@ -1359,13 +1471,28 @@ class GraphicsView(QtWidgets.QGraphicsView):
for item in self.scene().selectedItems():
if item.parentItem() is None:
current_zvalue = item.zValue()
item.setZValue(current_zvalue - 1)
if not (item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable):
log.error("Cannot move object to a lower layer because it is locked")
continue
item.setZValue(item.zValue() - 1)
item.updateNode()
item.update()
if item.zValue() == -1:
self._background_warning_msgbox.showMessage("Object moved to a background layer. You will now have to use the right-click action to select this object in the future and raise it to layer 0 to be able to move it")
def lockActionSlot(self):
"""
Slot to receive events from the lock action in the
contextual menu.
"""
for item in self.scene().selectedItems():
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem) and not isinstance(item, SvgIconItem):
if item.locked() is True:
item.setLocked(False)
else:
item.setLocked(True)
if item.parentItem() is None:
item.updateNode()
item.update()
def deleteActionSlot(self):
"""
@@ -1376,7 +1503,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
selected_nodes = []
for item in self.scene().selectedItems():
if isinstance(item, NodeItem):
selected_nodes.append(item.node())
node = item.node()
if node.locked():
QtWidgets.QMessageBox.critical(self, "Delete", "Cannot delete node '{}' because it is locked".format(node.name()))
return
selected_nodes.append(node)
if selected_nodes:
if len(selected_nodes) > 1:
question = "Do you want to permanently delete these {} nodes?".format(len(selected_nodes))
@@ -1393,6 +1524,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.
@@ -1402,59 +1536,29 @@ class GraphicsView(QtWidgets.QGraphicsView):
from .main_window import MainWindow
mainwindow = MainWindow.instance()
if "server" in node_data:
if "compute_id" in node_data:
try:
return ComputeManager.instance().getCompute(node_data["server"])
return ComputeManager.instance().getCompute(node_data["compute_id"])
except KeyError:
raise ModuleError("Compute {} doesn't exists".format(node_data["server"]))
raise ModuleError("Compute {} doesn't exists".format(node_data["compute_id"]))
server = server_select(mainwindow, node_data.get("node_type"))
if server is None:
raise ModuleError("Please select a server")
return server
def createNode(self, node_data, pos):
def createNodeFromTemplateId(self, template_id, pos):
"""
Creates a new node on the scene.
:param node_data: node data to create a new node
:param pos: position of the drop event
:returns: NodeItem instance
Ask the server to create a node using this template
"""
try:
node_module = None
for module in MODULES:
instance = module.instance()
node_class = module.getNodeClass(node_data["class"])
if node_class in instance.classes():
node_module = instance
break
if not node_module:
raise ModuleError("Could not find any module for {}".format(node_class))
if self._topology.project() is None:
return
node = node_module.instantiateNode(node_class, self.allocateCompute(node_data, instance), self._topology.project())
# If no server is available a ValueError is raised
except (ModuleError, ValueError) as e:
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
return
pos = self.mapToScene(pos)
node_item = self.createNodeItem(node, node_data["symbol"], pos.x(), pos.y())
node.setGraphics(node_item)
try:
node_module.createNode(node, node_data["name"])
except ModuleError as e:
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
return
return node_item
return TemplateManager().instance().createNodeFromTemplateId(self._topology.project(), template_id, pos.x(), pos.y())
def createNodeItem(self, node, symbol, x, y):
node.setSymbol(symbol)
node.setPos(x, y)
node_item = NodeItem(node)
self.scene().addItem(node_item)
self._topology.addNode(node)
@@ -1470,22 +1574,23 @@ 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())
def createDrawingItem(self, type, x, y, z, rotation=0, svg=None, drawing_id=None):
def createDrawingItem(self, type, x, y, z, locked=False, rotation=0, svg=None, drawing_id=None):
if type == "ellipse":
item = EllipseItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
item = EllipseItem(pos=QtCore.QPoint(x, y), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
elif type == "rect":
item = RectangleItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
item = RectangleItem(pos=QtCore.QPoint(x, y), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
elif type == "line":
item = LineItem(pos=QtCore.QPoint(x, y), dst=QtCore.QPoint(200, 0), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
elif type == "image":
item = ImageItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
item = ImageItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, locked=locked, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
elif type == "text":
item = TextItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
item = TextItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, locked=locked, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
if drawing_id is None:
item.create()
@@ -1497,19 +1602,31 @@ class GraphicsView(QtWidgets.QGraphicsView):
def drawBackground(self, painter, rect):
super().drawBackground(painter, rect)
if self._main_window.uiShowGridAction.isChecked():
gridSize = 75
grids = [(self.drawingGridSize(), QtGui.QColor(208, 208, 208)),
(self.nodeGridSize(), QtGui.QColor(190, 190, 190))]
painter.save()
painter.setPen(QtGui.QPen(QtGui.QColor(190, 190, 190)))
for (grid, colour) in grids:
if not grid:
continue
painter.setPen(QtGui.QPen(colour))
left = int(rect.left()) - (int(rect.left()) % gridSize)
top = int(rect.top()) - (int(rect.top()) % gridSize)
left = int(rect.left()) - (int(rect.left()) % grid)
top = int(rect.top()) - (int(rect.top()) % grid)
x = left
while x < rect.right():
painter.drawLine(x, rect.top(), x, rect.bottom())
x += gridSize
y = top
while y < rect.bottom():
painter.drawLine(rect.left(), y, rect.right(), y)
y += gridSize
x = left
while x < rect.right():
painter.drawLine(x, rect.top(), x, rect.bottom())
x += grid
y = top
while y < rect.bottom():
painter.drawLine(rect.left(), y, rect.right(), y)
y += grid
painter.restore()
def toggleUiDeviceMenu(self):
""" Hook which enables/disables uiDeviceMenu based on the current items selection"""
items = self.scene().selectedItems()
if len(items) > 0:
self._main_window.uiDeviceMenu.setEnabled(True)
else:
self._main_window.uiDeviceMenu.setEnabled(False)

View File

@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sip
from .qt import sip
import json
import copy
import http
@@ -25,6 +25,8 @@ import base64
import datetime
import ipaddress
import urllib.request
import urllib.parse
from .version import __version__, __version_info__
from .qt import QtCore, QtNetwork, qpartial, sip_is_deleted
@@ -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:
@@ -167,6 +165,25 @@ class HTTPClient(QtCore.QObject):
self.createHTTPQuery("POST", "/shutdown", None, showProgress=False)
self._shutdown = True
def getNetworkManager(self):
"""
:return: instance of NetworkManager
"""
return self._network_manager
def setMaxRetryConnection(self, retries):
"""
Sets how many times we need to retry a connection
:param retries: integer
"""
self._max_retry_connection = retries
def getMaxRetryConnection(self):
"""
Returns how many times we need to retry a connection
"""
return self._max_retry_connection
def _notify_progress_start_query(self, query_id, progress_text, response):
"""
Called when a query start
@@ -190,16 +207,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):
@@ -253,6 +270,7 @@ class HTTPClient(QtCore.QObject):
prefix="/v2",
params={},
networkManager=None,
eventsHandler=None,
**kwargs):
"""
Call the remote server, if not connected, check connection before
@@ -270,6 +288,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
"""
@@ -281,16 +301,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,
@@ -301,16 +322,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.info("Connection to {}".format(self.url()))
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5, showProgress=False)
log.debug("Connection to {}".format(self.url()))
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=10, showProgress=False)
def _connectionError(self, callback, msg="", server=None):
"""
@@ -327,7 +349,7 @@ class HTTPClient(QtCore.QObject):
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall. And that server version is {}.".format(self.url(), __version__)
for request, callback in self._query_waiting_connections:
if callback is not None:
callback({"message": msg}, error=True, server=server)
callback({"message": msg}, error=True, server=server, connection_error=True)
self._query_waiting_connections = []
def _retryConnection(self, server=None):
@@ -347,7 +369,7 @@ class HTTPClient(QtCore.QObject):
"""
if error is not False:
if self._retry < self.MAX_RETRY_CONNECTION:
if self._retry < self.getMaxRetryConnection():
self._retryConnection(server=server)
return
for request, callback in self._query_waiting_connections:
@@ -356,7 +378,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())
@@ -368,21 +390,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
@@ -441,7 +464,49 @@ class HTTPClient(QtCore.QObject):
request.setRawHeader(b"Authorization", auth_string.encode())
return request
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, networkManager=None, **kwargs):
def connectWebSocket(self, websocket, path, prefix="/v2"):
"""
Path of the websocket endpoint
"""
host = self._getHostForQuery()
request = websocket.request()
ws_url = "ws://{host}:{port}{prefix}{path}".format(host=host, port=self._port, path=path, prefix=prefix)
log.debug("Connecting to WebSocket endpoint: {}".format(ws_url))
request.setUrl(QtCore.QUrl(ws_url))
self._addAuth(request)
websocket.open(request)
return websocket
def _getHostForQuery(self):
"""
Get hostname that could be use by Qt
"""
try:
ip = self._host.rsplit('%', 1)[0]
ipaddress.IPv6Address(ip) # remove any scope ID
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
host = "[{}]".format(ip)
except ipaddress.AddressValueError:
host = self._host
return host
def _paramsToQueryString(self, params):
"""
:param params: Dictionary of query string parameters
:returns: String of the query string
"""
if params == {}:
query_string = ""
else:
query_string = "?"
params = params.copy()
for key, value in params.copy().items():
if value is None:
del params[key]
query_string += urllib.parse.urlencode(params)
return query_string
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, networkManager=None, eventsHandler=None, **kwargs):
"""
Call the remote server
@@ -457,22 +522,14 @@ 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
"""
try:
ip = self._host.rsplit('%', 1)[0]
ipaddress.IPv6Address(ip) # remove any scope ID
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
host = "[{}]".format(ip)
except ipaddress.AddressValueError:
host = self._host
if params == {}:
query_string = ""
else:
query_string = "?" + urllib.parse.urlencode(params)
host = self._getHostForQuery()
query_string = self._paramsToQueryString(params)
log.debug("{method} {protocol}://{host}:{port}{prefix}{path} {body}{query_string}".format(method=method, protocol=self._protocol, host=host, port=self._port, path=path, body=body, prefix=prefix, query_string=query_string))
if self._user:
@@ -491,7 +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())
@@ -502,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:
@@ -514,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
@@ -549,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):
@@ -569,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)
@@ -586,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:
@@ -607,7 +674,7 @@ class HTTPClient(QtCore.QObject):
if not body or content_type != "application/json":
callback({"message": error_message}, error=True, server=server, context=context)
else:
log.debug(body)
# log.debug(body)
try:
callback(json.loads(body), error=True, server=server, context=context)
except ValueError:
@@ -637,9 +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":
params = json.loads(body)
try:
params = json.loads(body)
except ValueError: # Partial JSON
params = {}
status = 504
else:
params = {}
if callback is not None:
@@ -657,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=5):
"""
Synchronous check if a server is running
:returns: Tuple (Status code, json of anwser). Status 0 is a non HTTP error
:returns: Tuple (Status code, json of answer). Status 0 is a non HTTP error
"""
try:
url = "{protocol}://{host}:{port}/v2/{endpoint}".format(protocol=self._protocol, host=self._host, port=self._port, endpoint=endpoint)
if self._user is not None and len(self._user) > 0:
log.debug("Synchronous get {} with user '{}'".format(url, self._user))
auth_handler = urllib.request.HTTPBasicAuthHandler()
auth_handler.add_password(realm="GNS3 server",
uri=url,
user=self._user,
passwd=self._password)
opener = urllib.request.build_opener(auth_handler)
urllib.request.install_opener(opener)
else:
log.debug("Synchronous get {} (no authentication)".format(url))
response = urllib.request.urlopen(url, timeout=timeout)
content_type = response.getheader("CONTENT-TYPE")
if response.status == 200:
host = self._getHostForQuery()
log.debug("{method} {protocol}://{host}:{port}{prefix}{endpoint}".format(method=method, protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
if self._user:
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, user=self._user, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
else:
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
request = self._request(url)
request = self._addAuth(request)
request.setRawHeader(b"User-Agent", "GNS3 QT Client v{version}".format(version=__version__).encode())
try:
response = self._network_manager.sendCustomRequest(request, method.encode())
except SystemError as e:
log.error("Can't send query: {}".format(str(e)))
return
loop = QtCore.QEventLoop()
response.finished.connect(loop.quit)
if timeout is not None:
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response, timeout))
if not loop.isRunning():
loop.exec_()
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
if response.error() != QtNetwork.QNetworkReply.NoError:
log.debug("Error while connecting to local server {}".format(response.errorString()))
return status, None
else:
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
if status == 200:
if content_type == "application/json":
content = response.read()
content = bytes(response.readAll())
json_data = json.loads(content.decode("utf-8"))
return response.status, json_data
return status, json_data
else:
return response.status, None
except http.client.InvalidURL as e:
log.warn("Invalid local server url: {}".format(e))
return 0, None
except urllib.error.URLError:
# Connection refused. It's a normal behavior if server is not started
return 0, None
except urllib.error.HTTPError as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return e.code, None
except (OSError, http.client.BadStatusLine, ValueError) as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return status, None
return 0, None
@classmethod
def fromUrl(cls, url, network_manager=None, base_settings=None):
"""
Returns HttpClient instance based on the url
:param url: Url to parse
:param network_manager: Optional network_manager
:param base_settings: Source of the settings, if necessary
:return: HttpClient
"""
settings = {}
if base_settings is not None:
settings.update(**base_settings)
parse_results = urllib.parse.urlparse(url)
settings['protocol'] = parse_results.scheme
settings['host'] = parse_results.hostname
settings['port'] = parse_results.port
settings['user'] = parse_results.username
settings['password'] = parse_results.password
return cls(settings, network_manager)

View File

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

View File

@@ -0,0 +1,90 @@
# -*- 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 os
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 not os.path.exists(self._image.path):
log.error("Image '{}' could not be found".format(self._image.path))
return
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.debug("Uploading image '{}' to compute".format(self._image.path))
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),
context={"image_path": self._image.path}, progressText="Uploading {}".format(self._image.filename), timeout=None, prefix="")
def _fileUploadToController(self):
log.debug("Uploading image '{}' to controller".format(self._image.path))
self._controller.postCompute(self._getComputePath(), self._compute_id, self._callback, body=pathlib.Path(self._image.path),
context={"image_path": self._image.path}, progressText="Uploading {}".format(self._image.filename), timeout=None)

View File

@@ -15,7 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from ..qt import QtCore, QtWidgets, qslot
from ..qt import QtCore, QtWidgets, qslot, QtGui
from .utils import colorFromSvg
import uuid
import logging
@@ -24,6 +25,15 @@ log = logging.getLogger(__name__)
class DrawingItem:
# Map QT stroke to SVG style
QT_DASH_TO_SVG = {
QtCore.Qt.SolidLine: "",
QtCore.Qt.NoPen: None,
QtCore.Qt.DashLine: "25, 25",
QtCore.Qt.DotLine: "5, 25",
QtCore.Qt.DashDotLine: "5, 25, 25",
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
}
show_layer = False
@@ -31,8 +41,10 @@ class DrawingItem:
Base class for non emulation item
"""
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, rotation=0, **kws):
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, locked=False, rotation=0, **kws):
self._id = drawing_id
self._deleting = False
self._locked = locked
if self._id is None:
self._id = str(uuid.uuid4())
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
@@ -54,6 +66,8 @@ class DrawingItem:
if rotation:
self.setRotation(rotation)
self.setLocked(locked)
def drawing_id(self):
return self._id
@@ -71,14 +85,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):
@@ -91,10 +105,11 @@ 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"])
self.setLocked(result["locked"])
self.setRotation(result["rotation"])
if "svg" in result:
self.fromSvg(result["svg"])
@@ -138,6 +153,7 @@ class DrawingItem:
"x": int(self.pos().x()),
"y": int(self.pos().y()),
"z": int(self.zValue()),
"locked": self._locked,
"rotation": int(self.rotation())
}
svg = self.toSvg()
@@ -147,20 +163,39 @@ class DrawingItem:
self._hash_svg = hash_svg
return data
def setZValue(self, value):
def locked(self):
"""
Sets a new Z value.
Is the drawing locked
"""
return self._locked
def setLocked(self, locked):
"""
Sets the locked value.
:param value: Z value
"""
QtWidgets.QGraphicsItem.setZValue(self, value)
if self.zValue() < 0:
self.setFlag(self.ItemIsSelectable, False)
if locked is True:
self.setFlag(self.ItemIsMovable, False)
else:
self.setFlag(self.ItemIsSelectable, True)
self.setFlag(self.ItemIsMovable, True)
self._locked = locked
def deleting(self):
"""
Is the drawing being deleted
"""
return self._deleting
def setDeleting(self):
"""
Mark this drawing as being deleted
"""
self._deleting = True
def delete(self, skip_controller=False):
"""
@@ -169,6 +204,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)
@@ -177,11 +213,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.drawingGridSize()
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)
@@ -215,3 +251,48 @@ class DrawingItem:
painter.setPen(QtCore.Qt.black)
zval = str(int(self.zValue()))
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
def _styleSvg(self, element):
"""
Add style from the shape item to the SVG element that we will
export
"""
style = ""
pen = self.pen()
if hasattr(self, "brush"): # Line don't have a brush
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
element.set("fill-opacity", str(self.brush().color().alphaF()))
dasharray = self.QT_DASH_TO_SVG[pen.style()]
if dasharray is None: # No border to the element
return element
elif dasharray == "":
pass # Solid line
else:
element.set("stroke-dasharray", dasharray)
element.set("stroke-width", str(pen.width()))
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
return element
def _penFromSVGElement(self, svg):
"""
Get a pen from a SVG element
:param svg:
"""
pen = QtGui.QPen()
if svg.get("stroke-width"):
pen.setWidth(int(svg.get("stroke-width")))
if svg.get("stroke"):
pen.setColor(colorFromSvg(svg.get("stroke")))
# Map SVG stroke style (border of the element to the Qt version)
if not svg.get("stroke"):
pen.setStyle(QtCore.Qt.NoPen)
else:
pen.setStyle(QtCore.Qt.SolidLine)
stroke = svg.get("stroke-dasharray")
if stroke:
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
if svg_stroke == stroke:
pen.setStyle(qt_stroke)
return pen

View File

@@ -22,7 +22,7 @@ Graphical representation of an ellipse on the QGraphicsScene.
import math
import xml.etree.ElementTree as ET
from ..qt import QtCore, QtGui, QtWidgets
from ..qt import QtWidgets
from .shape_item import ShapeItem

View File

@@ -21,7 +21,7 @@ Graphical representation of an Ethernet link for QGraphicsScene.
from ..qt import QtCore, QtGui, QtWidgets
from .link_item import LinkItem
from .note_item import NoteItem
from .label_item import LabelItem
from ..ports.port import Port
@@ -106,20 +106,20 @@ 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:
return
if self._source_port.status() == Port.started:
if self._link.suspended() or self._source_port.status() == Port.suspended:
# link or port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
elif self._source_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
elif self._source_port.status() == Port.suspended:
# port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
@@ -141,26 +141,28 @@ class EthernetLinkItem(LinkItem):
source_port_label = self._source_port.label()
if source_port_label is None:
source_port_label = NoteItem(self._source_item)
source_port_label = LabelItem(self._source_item)
source_port_label.setPlainText(self._source_port.shortName())
source_port_label.setPos(self.mapToItem(self._source_item, point1))
self._source_port.setLabel(source_port_label)
if self._draw_port_labels:
source_port_label.setFlag(source_port_label.ItemIsMovable, not self._source_item.locked())
source_port_label.show()
else:
source_port_label.hide()
painter.drawPoint(point1)
if self._settings["draw_link_status_points"]:
painter.drawPoint(point1)
if self._destination_port.status() == Port.started:
if self._link.suspended() or self._destination_port.status() == Port.suspended:
# link or port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
elif self._destination_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
elif self._destination_port.status() == Port.suspended:
# port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
@@ -182,16 +184,18 @@ class EthernetLinkItem(LinkItem):
destination_port_label = self._destination_port.label()
if destination_port_label is None:
destination_port_label = NoteItem(self._destination_item)
destination_port_label = LabelItem(self._destination_item)
destination_port_label.setPlainText(self._destination_port.shortName())
destination_port_label.setPos(self.mapToItem(self._destination_item, point2))
self._destination_port.setLabel(destination_port_label)
if self._draw_port_labels:
destination_port_label.setFlag(destination_port_label.ItemIsMovable, not self._destination_item.locked())
destination_port_label.show()
else:
destination_port_label.hide()
painter.drawPoint(point2)
if self._settings["draw_link_status_points"]:
painter.drawPoint(point2)
self._drawCaptureSymbol()
self._drawSymbol()

View File

@@ -52,6 +52,9 @@ class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
if svg:
svg = self.fromSvg(svg)
if 'z' in kws.keys():
self.setZValue(kws['z'])
def paint(self, painter, option, widget=None):
"""
Paints the contents of an item in local coordinates.

View File

@@ -15,20 +15,17 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Graphical representation of a note on the QGraphicsScene.
"""
from ..qt import QtCore, QtWidgets, QtGui
from .utils import colorFromSvg
class NoteItem(QtWidgets.QGraphicsTextItem):
class LabelItem(QtWidgets.QGraphicsTextItem):
"""
Text note for the QGraphicsView.
Label for links and nodes.
:param parent: optional parent
"""
item_unselected_signal = QtCore.Signal()
show_layer = False
@@ -45,8 +42,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
qt_font.fromString(view_settings["default_label_font"])
self.setDefaultTextColor(QtGui.QColor(view_settings["default_label_color"]))
self.setFont(qt_font)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemIsSelectable)
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable)
self.setZValue(2)
self._editable = True
@@ -174,34 +170,19 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
zval = str(int(self.zValue()))
painter.drawText(QtCore.QPointF(center.x(), center.y()), zval)
def setZValue(self, value):
"""
Sets a new Z value.
:param value: Z value
"""
super().setZValue(value)
if self.zValue() < 0:
self.setFlag(self.ItemIsSelectable, False)
self.setFlag(self.ItemIsMovable, False)
else:
self.setFlag(self.ItemIsSelectable, True)
self.setFlag(self.ItemIsMovable, True)
def setStyle(self, styles):
def setStyle(self, new_style):
"""
Set text style using a SVG style
"""
font = QtGui.QFont()
for style in styles.split(";"):
for style in new_style.split(";"):
if ":" in style:
key, val = style.split(":")
key = key.strip()
val = val.strip()
if key == "font-size":
font.setPointSize(int(val))
font.setPointSizeF(float(val))
elif key == "font-family":
font.setFamily(val)
elif key == "font-style" and val == "italic":
@@ -252,7 +233,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
style = ""
style += "font-family: {};".format(self.font().family())
style += "font-size: {};".format(self.font().pointSize())
style += "font-size: {};".format(self.font().pointSizeF())
if self.font().italic():
style += "font-style: italic;"

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

@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Graphical representation of a rectangle on the QGraphicsScene.
"""
import xml.etree.ElementTree as ET
from ..qt import QtCore, QtGui, QtWidgets
from .drawing_item import DrawingItem
class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
"""
Class to draw a rectangle on the scene.
"""
def __init__(self, dst=None, svg=None, **kws):
super().__init__(svg=svg, **kws)
self.setAcceptHoverEvents(True)
self._edge = None
self._border = 20
if svg is None:
if dst is not None:
self.setLine(0,
0,
dst.x(),
dst.y())
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
self.setPen(pen)
else:
self.fromSvg(svg)
if self._id is None:
self.create()
def paint(self, painter, option, widget=None):
"""
Paints the contents of an item in local coordinates.
:param painter: QPainter instance
:param option: QStyleOptionGraphicsItem instance
:param widget: QWidget instance
"""
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def toSvg(self):
"""
Return an SVG version of the shape
"""
svg = ET.Element("svg")
width = abs(self.line().x1() - self.line().x2())
height = abs(self.line().y1() - self.line().y2())
svg.set("width", str(int(width)))
svg.set("height", str(int(height)))
line = ET.SubElement(svg, "line")
line.set("x1", str(int(self.line().x1())))
line.set("x2", str(int(self.line().x2())))
line.set("y1", str(int(self.line().y1())))
line.set("y2", str(int(self.line().y2())))
line = self._styleSvg(line)
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
def fromSvg(self, svg):
"""
Import element informations from an SVG
"""
svg = ET.fromstring(svg)
width = float(svg.get("width", 0))
height = float(svg.get("height", 0))
# Backup the pos and restore it
pos = self.pos()
y1 = self.line().y1()
self.setLine(0, 0, width, height)
pen = QtGui.QPen()
if len(svg):
pen = self._penFromSVGElement(svg[0])
self.setLine(
float(svg[0].get("x1")),
float(svg[0].get("y1")),
float(svg[0].get("x2")),
float(svg[0].get("y2"))
)
self.setPos(pos)
self.setPen(pen)
self.update()
def _isHorizontalLine(self):
return abs(self.line().x1() - self.line().x2()) > abs(self.line().y1() - self.line().y2())
def hoverMoveEvent(self, event):
"""
Handles all hover move events.
:param event: QGraphicsSceneHoverEvent instance
"""
# locked objects don't need cursors
if not self.locked():
if self._isHorizontalLine():
if event.pos().x() > (self.line().x2() - self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
elif event.pos().x() < self._border:
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
else:
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
# Vertical line
else:
if event.pos().y() > (self.line().y2() - self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
elif event.pos().y() < self._border:
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
else:
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
def mouseMoveEvent(self, event):
"""
Handles all mouse move events.
:param event: QMouseEvent instance
"""
self.update()
if self._edge:
scenePos = event.scenePos()
if self._edge == "left" or self._edge == "bottom":
diff_x = self.x() - scenePos.x()
diff_y = self.y() - scenePos.y()
self.setPos(scenePos.x(), scenePos.y())
self.setLine(
0,
0,
self.line().x2() + diff_x,
self.line().y2() + diff_y)
elif self._edge == "right" or self._edge == "top":
pos = self.mapFromScene(scenePos)
self.setLine(
0,
0,
pos.x(),
pos.y())
self.setPos(self.x(), self.y())
super().mouseMoveEvent(event)
def mousePressEvent(self, event):
"""
Handles all mouse press events.
:param event: QMouseEvent instance
"""
self.update()
if self._isHorizontalLine():
if event.pos().x() > (self.line().x2() - self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self._edge = "right"
elif event.pos().x() < (self.line().x1() + self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self._edge = "left"
else:
if event.pos().y() > (self.line().y2() - self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self._edge = "top"
elif event.pos().y() < (self.line().y1() + self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self._edge = "bottom"
super().mousePressEvent(event)
def mouseReleaseEvent(self, event):
"""
Handles all mouse release events.
:param: QMouseEvent instance
"""
self.update()
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
self._edge = None
super().mouseReleaseEvent(event)
def hoverLeaveEvent(self, event):
"""
Handles all hover leave events.
:param event: QGraphicsSceneHoverEvent instance
"""
# locked objects don't need cursors
if not self.locked():
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)

View File

@@ -21,12 +21,14 @@ 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
from ..utils.get_icon import get_icon
class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
class SvgIconItem(QtSvg.QGraphicsSvgItem):
def __init__(self, symbol, parent):
@@ -34,7 +36,8 @@ class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
def mousePressEvent(self, event):
self.parentItem().mousePressEvent(event)
if self.parentItem():
self.parentItem().mousePressEvent(event)
event.accept()
@@ -86,11 +89,17 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
# QGraphicsSvgItem to indicate a capture
self._capturing_item = None
# QGraphicsSvgItem to indicate a filter is applied
self._filter_item = None
# QGraphicsSvgItem to indicate we suspend a link
self._suspend_item = None
# QGraphicsSvgItem to indicate a filter is applied and a capture is active
self._filter_capturing_item = None
if not self._adding_flag:
# there is a destination
self._link = link
self._link.updated_link_signal.connect(self._drawCaptureSymbol)
self._link.updated_link_signal.connect(self._drawSymbol)
self._link.delete_link_signal.connect(self._linkDeletedSlot)
self.setFlag(self.ItemIsFocusable)
source_item.addLink(self)
@@ -118,6 +127,16 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
if self in self.scene().items():
self.scene().removeItem(self)
@qslot
def _filterActionSlot(self, *args):
dialog = FilterDialog(self._main_window, self._link)
dialog.show()
dialog.exec_()
@qslot
def _suspendActionSlot(self, *args):
self._link.toggleSuspend()
def delete(self):
"""
Delete this link
@@ -205,14 +224,14 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
if not self._link.capturing():
# start capture
start_capture_action = QtWidgets.QAction("Start capture", menu)
start_capture_action.setIcon(QtGui.QIcon(':/icons/capture-start.svg'))
start_capture_action.setIcon(get_icon('capture-start.svg'))
start_capture_action.triggered.connect(self._startCaptureActionSlot)
menu.addAction(start_capture_action)
if self._link.capturing():
# stop capture
stop_capture_action = QtWidgets.QAction("Stop capture", menu)
stop_capture_action.setIcon(QtGui.QIcon(':/icons/capture-stop.svg'))
stop_capture_action.setIcon(get_icon('capture-stop.svg'))
stop_capture_action.triggered.connect(self._stopCaptureActionSlot)
menu.addAction(stop_capture_action)
@@ -228,12 +247,32 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
menu.addAction(analyze_action)
if self._link.suspended() is False:
# Edit filters
filter_action = QtWidgets.QAction("Packet filters", menu)
filter_action.setIcon(get_icon('filter.svg'))
filter_action.triggered.connect(self._filterActionSlot)
menu.addAction(filter_action)
# Suspend link
suspend_action = QtWidgets.QAction("Suspend", menu)
suspend_action.setIcon(get_icon('pause.svg'))
suspend_action.triggered.connect(self._suspendActionSlot)
menu.addAction(suspend_action)
else:
# Resume link
resume_action = QtWidgets.QAction("Resume", menu)
resume_action.setIcon(get_icon('start.svg'))
resume_action.triggered.connect(self._suspendActionSlot)
menu.addAction(resume_action)
# delete
delete_action = QtWidgets.QAction("Delete", menu)
delete_action.setIcon(QtGui.QIcon(':/icons/delete.svg'))
delete_action.setIcon(get_icon('delete.svg'))
delete_action.triggered.connect(self._deleteActionSlot)
menu.addAction(delete_action)
@qslot
def mousePressEvent(self, event):
"""
Called when the link is clicked and shows a contextual menu.
@@ -249,14 +288,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):
"""
@@ -433,19 +473,90 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self.update()
@qslot
def _drawCaptureSymbol(self, *args):
def _drawSymbol(self, *args):
"""
Draws a capture symbol in the middle of the link to indicate a capture is active.
Draws a symbol in the middle of the link to indicate a capture, a suspend or a filter is active.
"""
#FIXME: refactor ugly symbol management
if not self._adding_flag:
if self._link.capturing() and self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._capturing_item is None:
self._capturing_item = SvgCaptureItem(':/icons/inspect.svg', self)
self._capturing_item.setScale(0.6)
self._capturing_item.setPos(link_center)
if not self._capturing_item.isVisible():
self._capturing_item.show()
elif self._capturing_item:
self._capturing_item.hide()
if self._link.suspended():
if self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._suspend_item is None:
self._suspend_item = SvgIconItem(':/icons/pause.svg', self)
self._suspend_item.setScale(0.6)
if not self._suspend_item.isVisible():
self._suspend_item.show()
self._suspend_item.setPos(link_center)
if self._filter_item:
self._filter_item.hide()
elif self._suspend_item:
self._suspend_item.hide()
if self._filter_capturing_item:
self._filter_capturing_item.hide()
if self._capturing_item:
self._capturing_item.hide()
if self._filter_item:
self._filter_item.hide()
elif self._link.capturing() and len(self._link.filters()) > 0:
if self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._filter_capturing_item is None:
self._filter_capturing_item = SvgIconItem(':/icons/filter-capture.svg', self)
self._filter_capturing_item.setScale(0.6)
if not self._filter_capturing_item.isVisible():
self._filter_capturing_item.show()
self._filter_capturing_item.setPos(link_center)
elif self._filter_capturing_item:
self._filter_capturing_item.hide()
if self._capturing_item:
self._capturing_item.hide()
if self._filter_item:
self._filter_item.hide()
if self._suspend_item:
self._suspend_item.hide()
elif self._link.capturing():
if self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._capturing_item is None:
self._capturing_item = SvgIconItem(':/icons/inspect.svg', self)
self._capturing_item.setScale(0.6)
self._capturing_item.setPos(link_center)
if not self._capturing_item.isVisible():
self._capturing_item.show()
elif self._capturing_item:
self._capturing_item.hide()
if self._filter_capturing_item:
self._filter_capturing_item.hide()
if self._suspend_item:
self._suspend_item.hide()
elif len(self._link.filters()) > 0:
if self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._filter_item is None:
self._filter_item = SvgIconItem(':/icons/filter.svg', self)
self._filter_item.setScale(0.6)
if not self._filter_item.isVisible():
self._filter_item.show()
self._filter_item.setPos(link_center)
elif self._filter_item:
self._filter_item.hide()
if self._filter_capturing_item:
self._filter_capturing_item.hide()
if self._suspend_item:
self._suspend_item.hide()
else:
if self._capturing_item:
self._capturing_item.hide()
if self._suspend_item:
self._suspend_item.hide()
if self._filter_item:
self._filter_item.hide()
if self._filter_capturing_item:
self._filter_capturing_item.hide()

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

@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urllib.parse
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from ..controller import Controller
import logging
log = logging.getLogger(__name__)
class LogoItem(QtSvg.QGraphicsSvgItem):
"""
Margin for the logo
"""
MARGIN = 20
"""
Logo for the scene.
:param logo_path: Path to the logo (remote)
:param logo_url: URL which needs to be open user clicks on the logo
:param project: Current project
"""
def __init__(self, logo_path, logo_url, project):
super().__init__()
self._logo_path = logo_path
self._logo_url = logo_url
self._project = project
# Temporary symbol during loading
renderer = QImageSvgRenderer(":/icons/reload.svg")
renderer.setObjectName("symbol_loading")
self.setSharedRenderer(renderer)
effect = QtWidgets.QGraphicsColorizeEffect()
effect.setColor(QtGui.QColor("black"))
effect.setStrength(0.8)
self.setGraphicsEffect(effect)
self.graphicsEffect().setEnabled(False)
# set graphical settings for this item
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)
from ..main_window import MainWindow
self._main_window = MainWindow.instance()
self._settings = self._main_window.uiGraphicsView.settings()
self.updatePosition()
self._main_window.uiGraphicsView.viewport().installEventFilter(self)
remote_file = urllib.parse.quote('project-files/images/{}'.format(logo_path))
Controller.instance().getStatic(
'/projects/{}/files/{}'.format(project.id(), remote_file),
self.updateImage
)
# make it the last one
self.setZValue(-2)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.Paint:
self.updatePosition()
return QtWidgets.QWidget.eventFilter(self, source, event)
def updateImage(self, local_path):
renderer = QImageSvgRenderer(local_path)
renderer.setObjectName("project_logo")
self.setSharedRenderer(renderer)
def updatePosition(self):
"""
Updates position to be located in the right bottom corner
"""
logo_rect = self.boundingRect()
width = self._main_window.uiGraphicsView.viewport().width()
height = self._main_window.uiGraphicsView.viewport().height()
rect = self._main_window.uiGraphicsView.mapToScene(QtCore.QRect(0, 0, width, height)).boundingRect()
x = rect.x() + rect.width() - self.MARGIN - logo_rect.width()
y = rect.y() + rect.height() - self.MARGIN - logo_rect.height()
# update only when changes
if [int(self.x()), int(self.y())] != [int(x), int(y)]:
self.setX(x)
self.setY(y)
self.update()
def hoverEnterEvent(self, event):
"""
Handles all hover enter events for this item.
:param event: QGraphicsSceneHoverEvent instance
"""
if self._logo_url is not None:
self.graphicsEffect().setEnabled(True)
def hoverLeaveEvent(self, event):
"""
Handles all hover leave events for this item.
:param event: QGraphicsSceneHoverEvent instance
"""
self.graphicsEffect().setEnabled(False)
def mousePressEvent(self, event):
url = QtCore.QUrl(self._logo_url)
if not QtGui.QDesktopServices.openUrl(url):
QtWidgets.QMessageBox.warning(self, 'Open Url', 'Could not open url')

View File

@@ -19,11 +19,11 @@
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
from .note_item import NoteItem
from .label_item import LabelItem
from ..symbol import Symbol
from ..controller import Controller
@@ -41,7 +41,6 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
"""
show_layer = False
GRID_SIZE = 75
def __init__(self, node):
super().__init__()
@@ -51,6 +50,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
# link items connected to this node item.
self._links = []
self._symbol = None
self._locked = False
# says if the attached node has been initialized
# by the server.
@@ -60,7 +60,6 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self._node_label = None
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
self.setZValue(self._node.z())
# Temporary symbol during loading
renderer = QImageSvgRenderer(":/icons/reload.svg")
@@ -80,6 +79,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)
# update z value and locked state
self.setLocked(self._node.locked())
self.setZValue(self._node.z())
# connect signals to know about some events
# e.g. when the node has been started, stopped or suspended etc.
node.created_signal.connect(self.createdSlot)
@@ -100,26 +103,17 @@ 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):
@@ -142,10 +136,16 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
def symbol(self):
return self._symbol
def _symbolLoadedCallback(self, path):
renderer = QImageSvgRenderer(path)
@qslot
def _symbolLoadedCallback(self, path, *args):
renderer = QImageSvgRenderer(path, fallback=":/icons/cancel.svg")
renderer.setObjectName(path)
self.setSharedRenderer(renderer)
if self._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:
@@ -263,7 +263,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self.setSymbol(self._node.settings().get("symbol"))
self.setPos(self._node.settings().get("x", 0), self._node.settings().get("y", 0))
self.setZValue(self._node.settings().get("z", 0))
self.setLocked(self._node.settings().get("locked", False))
self._updateLabel()
# update the link tooltips in case the
@@ -356,7 +356,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
"""
if not self._node_label:
self._node_label = NoteItem(self)
self._node_label = LabelItem(self)
self._node_label.item_unselected_signal.connect(self._labelUnselectedSlot)
self._node_label.setEditable(False)
self._updateLabel()
@@ -364,7 +364,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
def _updateLabel(self):
"""
Update the label using the informations stored in the node
Update the label using the information stored in the node
"""
if not self._node_label:
return
@@ -373,8 +373,15 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
if self._node_label.toPlainText() != label_data["text"]:
self._node_label.setPlainText(label_data["text"])
self._node_label.setStyle(label_data["style"])
self._node_label.setRotation(label_data["rotation"])
style = label_data.get("style")
if style:
self._node_label.setStyle(style)
self._node_label.setRotation(label_data.get("rotation", 0))
if self._node.locked():
self._node_label.setFlag(self.ItemIsMovable, False)
if label_data["x"] is None:
self._centerLabel()
self.updateNode()
@@ -457,10 +464,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.nodeGridSize()
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:
@@ -514,20 +522,31 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
"""
super().setZValue(value)
if self.zValue() < 0:
self.setFlag(self.ItemIsSelectable, False)
for link in self._links:
link.adjust()
def locked(self):
return self._locked
def setLocked(self, locked):
"""
Sets the locked value.
:param value: Z value
"""
if locked is True:
self.setFlag(self.ItemIsMovable, False)
if self._node_label:
self._node_label.setFlag(self.ItemIsSelectable, False)
self._node_label.setFlag(self.ItemIsMovable, False)
else:
self.setFlag(self.ItemIsSelectable, True)
self.setFlag(self.ItemIsMovable, True)
if self._node_label:
self._node_label.setFlag(self.ItemIsSelectable, True)
self._node_label.setFlag(self.ItemIsMovable, True)
for link in self._links:
link.adjust()
self._locked = locked
def hoverEnterEvent(self, event):
"""

View File

@@ -61,4 +61,3 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
rect = self._styleSvg(rect)
return ET.tostring(svg, encoding="utf-8").decode("utf-8")

View File

@@ -22,7 +22,7 @@ Graphical representation of a Serial link on the QGraphicsScene.
import math
from ..qt import QtCore, QtGui, QtWidgets
from .link_item import LinkItem
from .note_item import NoteItem
from .label_item import LabelItem
from ..ports.port import Port
@@ -107,50 +107,52 @@ 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:
return
# source point color
if self._source_port.status() == Port.started:
if self._link.suspended() or self._source_port.status() == Port.suspended:
# link or port is suspended
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.yellow
elif self._source_port.status() == Port.started:
# port is active
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.green
elif self._source_port.status() == Port.suspended:
# port is suspended
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.yellow
else:
shape = QtCore.Qt.SquareCap
color = QtCore.Qt.red
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
source_port_label = self._source_port.label()
if source_port_label is None:
source_port_label = NoteItem(self._source_item)
source_port_label = LabelItem(self._source_item)
source_port_label.setPlainText(self._source_port.shortName())
source_port_label.setPos(self.mapToItem(self._source_item, self.source))
self._source_port.setLabel(source_port_label)
if self._draw_port_labels:
source_port_label.setFlag(source_port_label.ItemIsMovable, not self._source_item.locked())
source_port_label.show()
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._destination_port.status() == Port.started:
if self._link.suspended() or self._destination_port.status() == Port.suspended:
# link or port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
elif self._destination_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
elif self._destination_port.status() == Port.suspended:
# port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
@@ -160,16 +162,18 @@ class SerialLinkItem(LinkItem):
destination_port_label = self._destination_port.label()
if destination_port_label is None:
destination_port_label = NoteItem(self._destination_item)
destination_port_label = LabelItem(self._destination_item)
destination_port_label.setPlainText(self._destination_port.shortName())
destination_port_label.setPos(self.mapToItem(self._destination_item, self.destination))
self._destination_port.setLabel(destination_port_label)
if self._draw_port_labels:
destination_port_label.setFlag(destination_port_label.ItemIsMovable, not self._destination_item.locked())
destination_port_label.show()
else:
destination_port_label.hide()
painter.drawPoint(self.destination_point)
if self._settings["draw_link_status_points"]:
painter.drawPoint(self.destination_point)
self._drawCaptureSymbol()
self._drawSymbol()

View File

@@ -20,7 +20,7 @@ Base class for shape items (Rectangle, ellipse etc.).
"""
import xml.etree.ElementTree as ET
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
from ..qt import QtCore, QtGui, QtWidgets
from .drawing_item import DrawingItem
from .utils import colorFromSvg
@@ -30,17 +30,6 @@ log = logging.getLogger(__name__)
class ShapeItem(DrawingItem):
# Map QT stroke to SVG style
QT_DASH_TO_SVG = {
QtCore.Qt.SolidLine: "",
QtCore.Qt.NoPen: None,
QtCore.Qt.DashLine: "25, 25",
QtCore.Qt.DotLine: "5, 25",
QtCore.Qt.DashDotLine: "5, 25, 25",
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
}
"""
Base class to draw shapes on the scene.
"""
@@ -51,6 +40,7 @@ class ShapeItem(DrawingItem):
self.setAcceptHoverEvents(True)
self._border = 5
self._edge = None
self._originally_movable = True
if svg is None:
self.setRect(0, 0, width, height)
@@ -71,6 +61,7 @@ class ShapeItem(DrawingItem):
"""
self.update()
self._originally_movable = self.flags() & QtWidgets.QGraphicsItem.ItemIsMovable
if event.pos().x() > (self.rect().right() - self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self._edge = "right"
@@ -86,7 +77,6 @@ class ShapeItem(DrawingItem):
elif event.pos().y() > (self.rect().bottom() - self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self._edge = "bottom"
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
def mouseReleaseEvent(self, event):
@@ -97,7 +87,7 @@ class ShapeItem(DrawingItem):
"""
self.update()
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, self._originally_movable)
self._edge = None
QtWidgets.QGraphicsItem.mouseReleaseEvent(self, event)
@@ -157,8 +147,8 @@ class ShapeItem(DrawingItem):
:param event: QGraphicsSceneHoverEvent instance
"""
# objects on the background layer don't need cursors
if self.zValue() >= 0:
# locked objects don't need cursors
if not self.locked():
if event.pos().x() > (self.rect().right() - self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
elif event.pos().x() < (self.rect().left() + self._border):
@@ -177,31 +167,10 @@ class ShapeItem(DrawingItem):
:param event: QGraphicsSceneHoverEvent instance
"""
# objects on the background layer don't need cursors
if self.zValue() >= 0:
# locked objects don't need cursors
if not self.locked():
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
def _styleSvg(self, element):
"""
Add style from the shape item to the SVG element that we will
export
"""
style = ""
pen = self.pen()
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
element.set("fill-opacity", str(self.brush().color().alphaF()))
dasharray = self.QT_DASH_TO_SVG[pen.style()]
if dasharray is None: # No border to the element
return element
elif dasharray == "":
pass # Solid line
else:
element.set("stroke-dasharray", dasharray)
element.set("stroke-width", str(pen.width()))
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
return element
def fromSvg(self, svg):
"""
Import element informations from an SVG
@@ -215,10 +184,7 @@ class ShapeItem(DrawingItem):
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
if len(svg):
if svg[0].get("stroke-width"):
pen.setWidth(int(svg[0].get("stroke-width")))
if svg[0].get("stroke"):
pen.setColor(colorFromSvg(svg[0].get("stroke")))
pen = self._penFromSVGElement(svg[0])
if svg[0].get("fill"):
new_color = colorFromSvg(svg[0].get("fill"))
color = brush.color()
@@ -231,17 +197,6 @@ class ShapeItem(DrawingItem):
color.setAlphaF(float(svg[0].get("fill-opacity")))
brush.setColor(color)
# Map SVG stroke style (border of the element to the Qt version)
if not svg[0].get("stroke"):
pen.setStyle(QtCore.Qt.NoPen)
else:
pen.setStyle(QtCore.Qt.SolidLine)
stroke = svg[0].get("stroke-dasharray")
if stroke:
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
if svg_stroke == stroke:
pen.setStyle(qt_stroke)
self.setPen(pen)
self.setBrush(brush)
self.update()

View File

@@ -15,10 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Graphical representation of a note on the QGraphicsScene.
"""
import xml.etree.ElementTree as ET
from ..qt import QtCore, QtWidgets, QtGui
@@ -44,8 +40,8 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
main_window = MainWindow.instance()
view_settings = main_window.uiGraphicsView.settings()
qt_font = QtGui.QFont()
qt_font.fromString(view_settings["default_label_font"])
self.setDefaultTextColor(QtGui.QColor(view_settings["default_label_color"]))
qt_font.fromString(view_settings["default_note_font"])
self.setDefaultTextColor(QtGui.QColor(view_settings["default_note_color"]))
self.setFont(qt_font)
if svg:
@@ -54,6 +50,10 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
except ET.ParseError as e:
log.warning(str(e))
# re-evaluate `z` position after creation
if 'z' in kws.keys():
self.setZValue(kws['z'])
if self._id is None:
self.create()
@@ -121,7 +121,7 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
text = ET.SubElement(svg, "text")
text.set("font-family", self.font().family())
text.set("font-size", str(self.font().pointSize()))
text.set("font-size", str(self.font().pointSizeF()))
if self.font().italic():
text.set("font-style", "italic")
if self.font().bold():
@@ -138,7 +138,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()
@@ -157,7 +169,7 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
color.setAlphaF(float(opacity))
self.setDefaultTextColor(color)
font.setPointSize(int(text.get("font-size", self.font().pointSize())))
font.setPointSizeF(float(text.get("font-size", self.font().pointSizeF())))
font.setFamily(text.get("font-family", self.font().family()))
if text.get("font-style") == "italic":
font.setItalic(True)

View File

@@ -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
@@ -59,10 +59,10 @@ class Link(QtCore.QObject):
super().__init__()
log.info("adding link from {} {} to {} {}".format(source_node.name(),
source_port.name(),
destination_node.name(),
destination_port.name()))
log.debug("adding link from {} {} to {} {}".format(source_node.name(),
source_port.name(),
destination_node.name(),
destination_port.name()))
# create an unique ID
self._id = Link._instance_count
@@ -76,13 +76,17 @@ 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._capture_compute_id = None
self._initialized = False
self._filters = {}
self._suspend = False
# Boolean if True we are creatin the first instance of this node
# Boolean if True we are creating the first instance of this node
# if false the node already exist in the topology
# use to avoid erasing informations when reloading
# use to avoid erasing information when reloading
self._creator = False
self._nodes = []
@@ -100,35 +104,48 @@ class Link(QtCore.QObject):
Controller.instance().post("/projects/{project_id}/links".format(project_id=source_node.project().id()), self._linkCreatedCallback, body=body)
def _parseResponse(self, result):
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"]
self._capturing = result.get("capturing", False)
if self._capturing:
self._capture_compute_id = result.get("capture_compute_id", None)
self._capture_file_path = result.get("capture_file_path", None)
if Controller.instance().isRemote() or (self._capture_compute_id and self._capture_compute_id != "local"):
# We need to stream the pcap file content if the controller or compute is remote
if Controller.instance().isRemote() or self._capture_file_path is 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()
else:
self._capture_file = QtCore.QFile(self._capture_file_path)
self._capture_file.open(QtCore.QFile.WriteOnly)
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)
log.debug("Capturing packets to '{}'".format(self._capture_file_path))
if "nodes" in result:
self._nodes = result["nodes"]
self._updateLabels()
if "filters" in result:
self._filters = result["filters"]
if "suspend" in result:
self._suspend = result["suspend"]
self.updated_link_signal.emit(self._id)
def creator(self):
return self._creator
def suspended(self):
return self._suspend
def toggleSuspend(self):
self._suspend = not self._suspend
self.update()
def initialized(self):
return self._initialized
@@ -144,14 +161,20 @@ 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)
def listAvailableFilters(self, callback):
"""
Get the list of available filters
"""
Controller.instance().get("/projects/{project_id}/links/{link_id}/available_filters".format(project_id=self._source_node.project().id(), link_id=self._link_id), callback)
def updateLinkCallback(self, result, error=False, *args, **kwargs):
if error:
QtWidgets.QMessageBox.warning(None, "Update link", "Error while updating link: {}".format(result["message"]))
log.warning("Error while updating link: {}".format(result["message"]))
return
self._parseResponse(result)
@@ -167,10 +190,14 @@ class Link(QtCore.QObject):
def _updateLabel(self, label, label_data):
if not label or sip.isdeleted(label):
return
label.setPlainText(label_data["text"])
label.setPos(label_data["x"], label_data["y"])
label.setStyle(label_data["style"])
label.setRotation(label_data["rotation"])
if "text" in label_data:
label.setPlainText(label_data["text"])
if "x" in label_data and "y" in label_data:
label.setPos(label_data["x"], label_data["y"])
if "style" in label_data:
label.setStyle(label_data["style"])
if "rotation" in label_data:
label.setRotation(label_data["rotation"])
def _prepareParams(self):
body = {
@@ -185,7 +212,9 @@ class Link(QtCore.QObject):
"adapter_number": self._destination_port.adapterNumber(),
"port_number": self._destination_port.portNumber()
}
]
],
"filters": self._filters,
"suspend": self._suspend
}
if self._source_port.label():
body["nodes"][0]["label"] = self._source_port.label().dump()
@@ -195,7 +224,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
@@ -218,6 +247,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?
@@ -243,10 +285,18 @@ class Link(QtCore.QObject):
def __str__(self):
return "Link from {} port {} to {} port {}".format(self._source_node.name(),
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name())
description = "Link from {} port {} to {} port {}".format(self._source_node.name(),
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name())
if self.capturing():
description += "\nPacket capture is active"
for filter_type in self._filters.keys():
description += "\nPacket filter '{}' is active".format(filter_type)
return description
def capture_file_name(self):
"""
@@ -257,23 +307,25 @@ class Link(QtCore.QObject):
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name())
return re.sub("[^0-9A-Za-z_-]", "", capture_file_name)
return re.sub(r"[^0-9A-Za-z_-]", "", capture_file_name)
def deleteLink(self, skip_controller=False):
"""
Deletes this link.
"""
log.info("deleting link from {} {} to {} {}".format(self._source_node.name(),
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name()))
log.debug("deleting link from {} {} to {} {}".format(self._source_node.name(),
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name()))
if skip_controller:
self._linkDeletedCallback({})
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):
"""
@@ -298,62 +350,57 @@ class Link(QtCore.QObject):
"capture_file_name": capture_file_name,
"data_link_type": data_link_type
}
Controller.instance().post(
"/projects/{project_id}/links/{link_id}/start_capture".format(
project_id=self.project().id(),
link_id=self._link_id),
self._startCaptureCallback,
body=data)
Controller.instance().post("/projects/{project_id}/links/{link_id}/start_capture".format(project_id=self.project().id(), link_id=self._link_id),
self._startCaptureCallback,
body=data)
def _startCaptureCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while starting capture on link: {}".format(result["message"]))
return
self._parseResponse(result)
#self._parseResponse(result)
def _downloadPcapProgress(self, content, server=None, context={}, **kwargs):
"""
Called for each part of the file of the PCAP
"""
if not self._capture_file_path:
return
self._capture_file.write(content)
self._capture_file.flush()
def stopCapture(self):
if Controller.instance().isRemote():
if Controller.instance().isRemote() or (self._capture_compute_id and self._capture_compute_id != "local"):
if self._capture_file:
self._capture_file.close()
self._capture_file = None
if 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))
# 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("Cannot 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(
project_id=self.project().id(),
link_id=self._link_id),
self._stopCaptureCallback)
Controller.instance().post("/projects/{project_id}/links/{link_id}/stop_capture".format(project_id=self.project().id(),
link_id=self._link_id),
self._stopCaptureCallback)
def _stopCaptureCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while stopping capture on link: {}".format(result["message"]))
return
self._parseResponse(result)
#self._parseResponse(result)
def get(self, path, callback, **kwargs):
"""
HTTP Get from a link
"""
Controller.instance().get(
"/projects/{project_id}/links/{link_id}{path}".format(
project_id=self.project().id(),
link_id=self._link_id,
path=path),
callback,
**kwargs)
Controller.instance().get("/projects/{project_id}/links/{link_id}{path}".format(project_id=self.project().id(),
link_id=self._link_id,
path=path),
callback,
**kwargs)
def id(self):
"""
@@ -409,3 +456,15 @@ class Link(QtCore.QObject):
if self._destination_node == node:
return self._destination_port
return self._source_port
def filters(self):
"""
:returns: List the filters active on the node
"""
return self._filters
def setFilters(self, filters):
"""
:params filters: List of filters
"""
self._filters = filters

View File

@@ -23,10 +23,11 @@ import copy
import psutil
from .qt import QtCore, QtWidgets, qslot
from .version import __version__
from .qt import QtCore, QtWidgets
from .version import __version__, __version_info__
from .utils import parse_version
from .controller import Controller
from .local_server_config import LocalServerConfig
from .settings import LOCAL_SERVER_SETTINGS
import logging
log = logging.getLogger(__name__)
@@ -39,38 +40,17 @@ class LocalConfig(QtCore.QObject):
"""
config_changed_signal = QtCore.Signal()
# When this signal is emit the config is saved on controller
save_on_controller_signal = QtCore.Signal()
def __init__(self, config_file=None):
"""
:param config_file: Path to the config file (override all other config, usefull for tests)
:param config_file: Path to the config file (override all other config, useful for tests)
"""
super().__init__()
self._profile = None
self._config_file = config_file
# Security to avoid pushing to the controller settings before
# we get the original settings from controller
self._settings_retrieved_from_controller = False
self._migrateOldConfigPath()
self._resetLoadConfig()
self._monitoring_changes = False
Controller.instance().connected_signal.connect(self.refreshConfigFromController)
self.save_on_controller_signal.connect(self._saveOnController)
def _monitorChanges(self):
"""
Poll the remote server waiting for settings update
"""
if self._monitoring_changes:
return
self._monitoring_changes = True
self._timer = QtCore.QTimer()
self._timer.setInterval(5000)
self._refreshingSettings = False
self._timer.timeout.connect(self.refreshConfigFromController)
self._timer.start()
def _resetLoadConfig(self):
"""
@@ -109,8 +89,25 @@ class LocalConfig(QtCore.QObject):
try:
# create the config file if it doesn't exist
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
with open(self._config_file, "w", encoding="utf-8") as f:
json.dump({"version": __version__, "type": "settings"}, f)
if sys.platform.startswith("win"):
old_config_path = os.path.join(os.path.expandvars("%APPDATA%"), "GNS3", filename)
else:
old_config_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3", filename)
# TODO: migrate versioned config file from a previous version of GNS3 (for instance 2.2 -> 2.3) + support profiles
if os.path.exists(old_config_path):
# migrate post version 2.2.0 configuration file
shutil.copyfile(old_config_path, self._config_file)
# reset the local server path and ubridge path
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
settings["path"] = ""
settings["ubridge_path"] = ""
LocalServerConfig.instance().saveSettings("Server", settings)
else:
# create a new config
with open(self._config_file, "w", encoding="utf-8") as f:
json.dump({"version": __version__, "type": "settings"}, f)
except OSError as e:
log.error("Could not create the config file {}: {}".format(self._config_file, e))
@@ -136,53 +133,31 @@ class LocalConfig(QtCore.QObject):
self._config_file = None
self._resetLoadConfig()
@qslot
def refreshConfigFromController(self):
"""
Refresh the configuration from the controller
"""
controller = Controller.instance()
if controller.connected():
self._refreshingSettings = True
controller.get("/settings", self._getSettingsCallback, showProgress=False)
self._monitorChanges()
def _getSettingsCallback(self, result, error=False, **kwargs):
self._refreshingSettings = False
if error:
log.error("Can't get settings from controller")
return
if result == {} and self._settings != {}:
self._settings_retrieved_from_controller = True
self.save_on_controller_signal.emit()
return
# The server return an uuid to keep track of settings version
if self._settings.get("modification_uuid") != result.get("modification_uuid"):
self._settings.update(result)
# Update already loaded section
for section in self._settings.keys():
if isinstance(self._settings[section], dict):
self.loadSectionSettings(section, self._settings[section])
self.config_changed_signal.emit()
self._settings_retrieved_from_controller = True
def configDirectory(self):
"""
Get the configuration directory
"""
version = "{}.{}".format(__version_info__[0], __version_info__[1])
if sys.platform.startswith("win"):
appdata = os.path.expandvars("%APPDATA%")
path = os.path.join(appdata, "GNS3")
path = os.path.join(appdata, "GNS3", version)
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3")
path = os.path.join(home, ".config", "GNS3", version)
if self._profile is not None:
path = os.path.join(path, "profiles", self._profile)
return os.path.normpath(path)
def runAsRootPath(self):
"""
Gets run as root filename
:return: string
"""
return os.path.join(self.configDirectory(), "run_as_root")
def _migrateOldConfigPath(self):
"""
Migrate pre 1.4 config path
@@ -191,8 +166,9 @@ class LocalConfig(QtCore.QObject):
# In < 1.4 on Mac the config was in a gns3.net directory
# We have move to same location as Linux
if sys.platform.startswith("darwin"):
version = "{}.{}".format(__version_info__[0], __version_info__[1])
old_path = os.path.join(os.path.expanduser("~"), ".config", "gns3.net")
new_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3")
new_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3", version)
if os.path.exists(old_path) and not os.path.exists(new_path):
try:
shutil.copytree(old_path, new_path)
@@ -201,7 +177,7 @@ class LocalConfig(QtCore.QObject):
def _migrateOldConfig(self):
"""
Migrate pre 1.4 config
Migrate config from a previous version.
"""
# Display an error if settings come from a more recent version of GNS3
@@ -209,28 +185,31 @@ 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.".format(self._settings["version"]))
app = QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
error_message = "Settings are for version {} of GNS3. It is not possible to use a previous version of GNS3 without risking losing data. Delete the settings in '{}' to start GNS3".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"):
servers = self._settings.get("Servers", {})
servers = self._settings.get("Servers", {})
if "LocalServer" in self._settings:
if "LocalServer" in self._settings:
servers["local_server"] = copy.copy(self._settings["LocalServer"])
# We migrate the server binary for OSX due to the change from py2app to CX freeze
# We migrate the server binary for OSX due to the change from py2app to CX freeze
if servers["local_server"]["path"] == "/Applications/GNS3.app/Contents/Resources/server/Contents/MacOS/gns3server":
servers["local_server"]["path"] = "gns3server"
if "RemoteServers" in self._settings:
if "RemoteServers" in self._settings:
servers["remote_servers"] = copy.copy(self._settings["RemoteServers"])
self._settings["Servers"] = servers
self._settings["Servers"] = servers
if "GUI" in self._settings:
if "GUI" in self._settings:
main_window = self._settings.get("MainWindow", {})
main_window["hide_getting_started_dialog"] = self._settings["GUI"].get("hide_getting_started_dialog", False)
self._settings["MainWindow"] = main_window
@@ -243,7 +222,7 @@ class LocalConfig(QtCore.QObject):
if self._settings["MainWindow"].get("telnet_console_command") not in PRECONFIGURED_TELNET_CONSOLE_COMMANDS.values():
self._settings["MainWindow"]["telnet_console_command"] = DEFAULT_TELNET_CONSOLE_COMMAND
# Migrate 1.X to 2.0
# Migrate 1.X to 2.0
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
if "Qemu" in self._settings:
# The internet VM is replaced by the nat Node
@@ -254,22 +233,24 @@ class LocalConfig(QtCore.QObject):
vms.append(vm)
self._settings["Qemu"]["vms"] = vms
# Starting with 2.0.0dev5 IOU licence is stored in the settings
# Starting with 2.0.0dev5 IOU licence is stored in the settings
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
if "IOU" in self._settings and "iourc_path" in self._settings["IOU"] and "iourc_content" not in self._settings["IOU"]:
try:
with open(self._settings["IOU"]["iourc_path"], "r") 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):
"""
Read the configuration file.
"""
log.info("Load config from %s", config_path)
log.debug("Load config from %s", config_path)
try:
with open(config_path, "r", encoding="utf-8") as f:
self._last_config_changed = os.stat(config_path).st_mtime
@@ -296,35 +277,16 @@ class LocalConfig(QtCore.QObject):
with open(temporary, "w", encoding="utf-8") as f:
json.dump(self._settings, f, sort_keys=True, indent=4)
shutil.move(temporary, self._config_file)
log.info("Configuration save to %s", self._config_file)
log.debug("Configuration save to %s", self._config_file)
self._last_config_changed = os.stat(self._config_file).st_mtime
except (ValueError, OSError) as e:
log.error("Could not write the config file {}: {}".format(self._config_file, e))
self.save_on_controller_signal.emit()
@qslot
def _saveOnController(self, *args):
"""
Save some settings on controller for the transition from
GUI to a central controller. Will be removed later
"""
if Controller.instance().connected() 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"]
controller_settings = {}
for key, val in self._settings.items():
if key in section_to_save_on_controller:
controller_settings[key] = val
# We want only the VM settings on the server
elif key == "Server":
controller_settings["Server"]["vm"] = self._settings["Server"]["vm"]
Controller.instance().post("/settings", None, body=controller_settings)
def checkConfigChanged(self):
try:
if self._last_config_changed and self._last_config_changed < os.stat(self._config_file).st_mtime:
log.info("Client config has changed, reloading it...")
log.debug("Client config has changed, reloading it...")
self._readConfig(self._config_file)
self.config_changed_signal.emit()
except OSError as e:
@@ -402,9 +364,8 @@ class LocalConfig(QtCore.QObject):
self._settings[section] = settings
if changed:
log.info("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
log.debug("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
self.writeConfig()
return copy.deepcopy(settings)
def saveSectionSettings(self, section, settings):
@@ -420,7 +381,7 @@ class LocalConfig(QtCore.QObject):
if self._settings[section] != settings:
self._settings[section].update(copy.deepcopy(settings))
log.info("Section %s has changed. Saving configuration", section)
log.debug("Section %s has changed. Saving configuration", section)
self.writeConfig()
else:
log.debug("Section %s has not changed. Skip saving configuration", section)
@@ -455,6 +416,50 @@ 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)
def showGridOnNewProject(self):
"""
:returns: Boolean. True if show_grid_on_new_project is enabled
"""
from gns3.settings import GRAPHICS_VIEW_SETTINGS
return self.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS).get("show_grid_on_new_project", False)
def snapToGridOnNewProject(self):
"""
:returns: Boolean. True if snap_to_grid_on_new_project is enabled
"""
from gns3.settings import GRAPHICS_VIEW_SETTINGS
return self.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS).get("snap_to_grid_on_new_project", False)
@staticmethod
def instance():
"""

View File

@@ -30,13 +30,12 @@ import signal
import subprocess
from gns3.qt import QtWidgets, QtCore
from gns3.qt import QtWidgets, QtCore, qslot
from gns3.settings import LOCAL_SERVER_SETTINGS
from gns3.local_config import LocalConfig
from gns3.local_server_config import LocalServerConfig
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
@@ -59,17 +58,20 @@ class StopLocalServerWorker(QtCore.QObject):
def __init__(self, local_server_process):
super().__init__()
self._local_server_process = local_server_process
self._precision = 100 # In MS
self._remaining_trial = int(10 * (1000 / self._precision))
@qslot
def _callbackSlot(self, *params):
self._local_server_process.poll()
if self._local_server_process.returncode is None and self._remaining_trial > 0:
self._remaining_trial -= 1
QtCore.QTimer.singleShot(self._precision, self._callbackSlot)
else:
self.finished.emit()
def run(self):
precision = 1
remaining_trial = 4 / precision # 4 Seconds
while remaining_trial > 0:
if self._local_server_process.returncode is None:
remaining_trial -= 1
self.thread().sleep(precision)
else:
break
self.finished.emit()
QtCore.QTimer.singleShot(1000, self._callbackSlot)
def cancel(self):
return
@@ -121,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):
@@ -142,7 +149,7 @@ class LocalServer(QtCore.QObject):
path = os.path.abspath(self._settings["ubridge_path"])
if not path or len(path) == 0 or not os.path.exists(path):
if not path or len(path) == 0 or not os.path.exists(path) or not os.path.isfile(path):
return False
if sys.platform.startswith("win"):
@@ -157,28 +164,23 @@ class LocalServer(QtCore.QObject):
if sys.platform.startswith("linux"):
# test if the executable has the CAP_NET_RAW capability (Linux only)
try:
if "security.capability" in os.listxattr(path):
caps = os.getxattr(path, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
proceed = QtWidgets.QMessageBox.question(
self.parent(),
"uBridge",
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
sudo(["setcap", "cap_net_admin,cap_net_raw=ep"])
else:
# capabilities not supported
request_setuid = True
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if "security.capability" not in os.listxattr(path) or not struct.unpack("<IIIII", os.getxattr(path, "security.capability"))[1] & 1 << 13:
proceed = QtWidgets.QMessageBox.question(
self.parent(),
"uBridge",
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
sudo(["setcap", "cap_net_admin,cap_net_raw=ep", path])
except AttributeError:
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
log.warning("Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)")
return True
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set CAP_NET_RAW capability to uBridge {}: {}".format(path, str(e)))
return False
request_setuid = True
if sys.platform.startswith("darwin") or request_setuid:
try:
@@ -186,7 +188,7 @@ class LocalServer(QtCore.QObject):
proceed = QtWidgets.QMessageBox.question(
self.parent(),
"uBridge",
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
@@ -264,7 +266,7 @@ class LocalServer(QtCore.QObject):
if need_restart:
self.stopLocalServer(wait=True)
self.localServerAutoStartIfRequire()
self.localServerAutoStartIfRequired()
# If the controller is remote:
else:
self.stopLocalServer(wait=True)
@@ -310,9 +312,9 @@ class LocalServer(QtCore.QObject):
# Permission issue, or process no longer exists, or file is empty
return
def localServerAutoStartIfRequire(self):
def localServerAutoStartIfRequired(self):
"""
Try to start the embed gns3 server.
Try to start the embedded gns3 server.
"""
if not self.shouldLocalServerAutoStart():
@@ -332,7 +334,7 @@ class LocalServer(QtCore.QObject):
return True
if self.isLocalServerRunning():
log.info("A local server already running on this host")
log.debug("A local server already running on this host")
# Try to kill the server. The server can be still running after
# if the server was started by hand
self._killAlreadyRunningServer()
@@ -357,7 +359,6 @@ class LocalServer(QtCore.QObject):
self._server_started_by_me = True
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
return True
def initLocalServer(self):
@@ -369,15 +370,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))
@@ -464,10 +463,10 @@ class LocalServer(QtCore.QObject):
except FileNotFoundError:
pass
except OSError as e:
log.warn("could not delete server log file {}: {}".format(logpath, e))
log.warning("could not delete server log file {}: {}".format(logpath, e))
command += ' --log="{}" --pid="{}"'.format(logpath, self._pid_path())
log.info("Starting local server process with {}".format(command))
log.debug("Starting local server process with {}".format(command))
try:
if sys.platform.startswith("win"):
# use the string on Windows
@@ -480,7 +479,7 @@ class LocalServer(QtCore.QObject):
log.warning('Could not start local server "{}": {}'.format(command, e))
return False
log.info("Local server process has started (PID={})".format(self._local_server_process.pid))
log.debug("Local server process has started (PID={})".format(self._local_server_process.pid))
return True
def _checkLocalServerRunningSlot(self):
@@ -514,10 +513,12 @@ 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"])
if json_data is None or status != 200:
status, json_data = HTTPClient(self._settings).getSynchronous("GET", "/version")
if status == 401: # Auth issue that need to be solved later
return True
elif json_data is None:
return False
elif status != 200:
return False
else:
version = json_data.get("version", None)
@@ -535,7 +536,7 @@ class LocalServer(QtCore.QObject):
if self.localServerProcessIsRunning():
self._stopping = True
log.info("Stopping local server (PID={})".format(self._local_server_process.pid))
log.debug("Stopping local server (PID={})".format(self._local_server_process.pid))
# local server is running, let's stop it
if self._http_client:
self._http_client.shutdown()

View File

@@ -138,6 +138,18 @@ class LocalServerConfig:
if changed:
self.writeConfig()
def deleteSetting(self, section, name):
"""
Delete a specific setting in a given section.
:param section: section name
:param name: setting name to delete
"""
if section in self._config and name in self._config[section]:
del self._config[section][name]
self.writeConfig()
@staticmethod
def instance():
"""

View File

@@ -121,7 +121,7 @@ def init_logger(level, logfile, quiet=False):
handler.formatter = logging.Formatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
log.addHandler(handler)
except OSError as e:
log.warn("could not log to {}: {}".format(logfile, e))
log.warning("could not log to {}: {}".format(logfile, e))
log.info('Log level: {}'.format(logging.getLevelName(level)))

View File

@@ -18,6 +18,7 @@
import sys
import os
import faulthandler
# Try to install updates & restart application if an update is installed
try:
@@ -89,13 +90,13 @@ def locale_check():
log.error("could not determine the current locale: {}".format(e))
if not language and not encoding:
try:
log.warn("could not find a default locale, switching to C.UTF-8...")
log.warning("could not find a default locale, switching to C.UTF-8...")
locale.setlocale(locale.LC_ALL, ("C", "UTF-8"))
except locale.Error as e:
log.error("could not switch to the C.UTF-8 locale: {}".format(e))
raise SystemExit
elif encoding != "UTF-8":
log.warn("your locale {}.{} encoding is not UTF-8, switching to the UTF-8 version...".format(language, encoding))
log.warning("your locale {}.{} encoding is not UTF-8, switching to the UTF-8 version...".format(language, encoding))
try:
locale.setlocale(locale.LC_ALL, (language, "UTF-8"))
except locale.Error as e:
@@ -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"):
@@ -133,15 +137,13 @@ def main():
# packaged binary
frozen_dir = os.path.dirname(os.path.abspath(sys.executable))
if sys.platform.startswith("darwin"):
frozen_dirs = [
frozen_dir,
os.path.normpath(os.path.join(frozen_dir, '..', 'Resources'))
]
frozen_dirs = [frozen_dir]
elif sys.platform.startswith("win"):
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", "")
@@ -184,8 +186,8 @@ def main():
if sys.version_info < (3, 4):
raise SystemExit("Python 3.4 or higher is required")
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.0.0"):
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.5.0"):
raise SystemExit("Requirement is PyQt5 version 5.5.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
if parse_version(psutil.__version__) < parse_version("2.2.1"):
raise SystemExit("Requirement is psutil version 2.2.1 or higher, got version {}".format(psutil.__version__))
@@ -228,8 +230,10 @@ def main():
if local_config.multiProfiles() and not options.profile:
profile_select = ProfileSelectDialog()
profile_select.show()
profile_select.exec_()
options.profile = profile_select.profile()
if profile_select.exec_():
options.profile = profile_select.profile()
else:
sys.exit(0)
# Init the config
if options.config:
@@ -261,7 +265,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
@@ -285,7 +292,6 @@ def main():
mainwindow.show()
exit_code = app.exec_()
signal.signal(signal.SIGINT, orig_sigint)
signal.signal(signal.SIGTERM, orig_sigterm)
@@ -294,7 +300,7 @@ def main():
# We force deleting the app object otherwise it's segfault on Fedora
del app
# We force a full garbage collect before exit
# for unknow reason otherwise Qt Segfault on OSX in some
# for unknown reason otherwise Qt Segfault on OSX in some
# conditions
import gc
gc.collect()

View File

@@ -42,16 +42,21 @@ from .dialogs.edit_project_dialog import EditProjectDialog
from .dialogs.setup_wizard import SetupWizard
from .settings import GENERAL_SETTINGS
from .items.node_item import NodeItem
from .items.link_item import LinkItem
from .items.link_item import LinkItem, SvgIconItem
from .items.shape_item import ShapeItem
from .items.label_item import LabelItem
from .topology import Topology
from .http_client import HTTPClient
from .progress import Progress
from .update_manager import UpdateManager
from .utils.analytics import AnalyticsClient
from .dialogs.appliance_wizard import ApplianceWizard
from .dialogs.new_appliance_dialog import NewApplianceDialog
from .dialogs.new_template_wizard import NewTemplateWizard
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
from .status_bar import StatusBarHandler
from .registry.appliance import ApplianceError
from .template_manager import TemplateManager
from .appliance_manager import ApplianceManager
log = logging.getLogger(__name__)
@@ -67,14 +72,24 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# signal to tell the view if the user is adding a link or not
adding_link_signal = QtCore.pyqtSignal(bool)
# Signal of settings updates
settings_updated_signal = QtCore.Signal()
def __init__(self, parent=None, open_file=None):
"""
:param open_file: Open this file instead of asking for a new project
"""
super().__init__(parent)
self._settings = {}
self.setupUi(self)
self._notif_dialog = NotifDialog(self)
# Setup logger
logging.getLogger().addHandler(NotifDialogHandler(self._notif_dialog))
logging.getLogger().addHandler(StatusBarHandler(self.uiStatusBar))
self._open_file_at_startup = open_file
MainWindow._instance = self
@@ -82,8 +97,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
topology.setMainWindow(self)
topology.project_changed_signal.connect(self._projectChangedSlot)
Controller.instance().setParent(self)
LocalServer.instance().setParent(self)
self._settings = {}
HTTPClient.setProgressCallback(Progress.instance(self))
self._first_file_load = True
@@ -96,11 +111,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.recent_project_actions = []
self._start_time = time.time()
local_config = LocalConfig.instance()
local_config.config_changed_signal.connect(self._localConfigChangedSlot)
#local_config.config_changed_signal.connect(self._localConfigChangedSlot)
self._local_config_timer = QtCore.QTimer(self)
self._local_config_timer.timeout.connect(local_config.checkConfigChanged)
self._local_config_timer.start(1000) # milliseconds
self._analytics_client = AnalyticsClient()
self._template_manager = TemplateManager().instance()
self._appliance_manager = ApplianceManager().instance()
# restore the geometry and state of the main window.
self.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["geometry"].encode()))
@@ -114,6 +131,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
action.setIconText("All devices")
self.uiDocksMenu.addAction(action)
# Sometimes the parent seem invalid https://github.com/GNS3/gns3-gui/issues/2182
self.uiNodesDockWidget.setParent(self)
# make sure the dock widget is not open
self.uiNodesDockWidget.setVisible(False)
@@ -122,6 +141,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
self._screenshots_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
self._pictures_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
self._appliance_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
self._portable_project_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
self._project_dir = None
# add recent file actions to the File menu
for i in range(0, self._maxrecent_files):
@@ -150,8 +172,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# restore the style
self._setStyle(self._settings.get("style"))
if self._settings["hide_new_appliance_template_button"]:
self.uiNewAppliancePushButton.hide()
if self._settings.get("hide_new_template_button"):
self.uiNewTemplatePushButton.hide()
self.setWindowTitle("[*] GNS3")
@@ -168,7 +190,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiSnapshotAction,
self.uiEditProjectAction,
self.uiDeleteProjectAction,
self.uiImportExportConfigsAction
self.uiImportExportConfigsAction,
self.uiLockAllAction
]
# This widgets are not enabled if it's a remote controller (no access to the local file system)
@@ -188,6 +211,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiNewProjectAction.triggered.connect(self._newProjectActionSlot)
self.uiOpenProjectAction.triggered.connect(self.openProjectActionSlot)
self.uiOpenApplianceAction.triggered.connect(self.openApplianceActionSlot)
self.uiNewTemplateAction.triggered.connect(self._newTemplateActionSlot)
self.uiSaveProjectAsAction.triggered.connect(self._saveProjectAsActionSlot)
self.uiExportProjectAction.triggered.connect(self._exportProjectActionSlot)
self.uiImportProjectAction.triggered.connect(self._importProjectActionSlot)
@@ -212,9 +236,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiResetPortLabelsAction.triggered.connect(self._resetPortLabelsActionSlot)
self.uiShowPortNamesAction.triggered.connect(self._showPortNamesActionSlot)
self.uiShowGridAction.triggered.connect(self._showGridActionSlot)
self.uiSnapToGridAction.triggered.connect(self._snapToGridActionSlot)
self.uiLockAllAction.triggered.connect(self._lockActionSlot)
# tool menu connections
self.uiWebInterfaceAction.triggered.connect(self._openWebInterfaceActionSlot)
self.uiWebInterfaceAction.triggered.connect(self._openLightWebInterfaceActionSlot)
self.uiWebUIAction.triggered.connect(self._openWebInterfaceActionSlot)
# control menu connections
self.uiStartAllAction.triggered.connect(self._startAllActionSlot)
@@ -232,7 +259,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiInsertImageAction.triggered.connect(self._insertImageActionSlot)
self.uiDrawRectangleAction.triggered.connect(self._drawRectangleActionSlot)
self.uiDrawEllipseAction.triggered.connect(self._drawEllipseActionSlot)
self.uiEditReadmeAction.triggered.connect(self._editReadmeActionSlot)
self.uiDrawLineAction.triggered.connect(self._drawLineActionSlot)
# help menu connections
self.uiOnlineHelpAction.triggered.connect(self._onlineHelpActionSlot)
@@ -252,12 +279,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiBrowseAllDevicesAction.triggered.connect(self._browseAllDevicesActionSlot)
self.uiAddLinkAction.triggered.connect(self._addLinkActionSlot)
# new appliance button
self.uiNewAppliancePushButton.clicked.connect(self._newApplianceActionSlot)
# new template button
self.uiNewTemplatePushButton.clicked.connect(self._newTemplateActionSlot)
# connect the signal to the view
self.adding_link_signal.connect(self.uiGraphicsView.addingLinkSlot)
# connect to the signal when settings change
self.settings_updated_signal.connect(self.settingsChangedSlot)
def _loadSettings(self):
"""
Loads the settings from the persistent settings file.
@@ -290,17 +320,59 @@ 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 _openLightWebInterfaceActionSlot(self):
if Controller.instance().connected():
QtGui.QDesktopServices.openUrl(QtCore.QUrl(Controller.instance().httpClient().fullUrl()))
def _openWebInterfaceActionSlot(self):
if Controller.instance().connected():
QtGui.QDesktopServices.openUrl(QtCore.QUrl(Controller.instance().httpClient().fullUrl()))
base_url = Controller.instance().httpClient().fullUrl()
webui_url = "{}/static/web-ui/bundled".format(base_url)
QtGui.QDesktopServices.openUrl(QtCore.QUrl(webui_url))
def _showGridActionSlot(self):
"""
Called when we ask to display the grid
"""
self.showGrid(self.uiShowGridAction.isChecked())
self.uiGraphicsView.viewport().update()
# save settings
project = Topology.instance().project()
if project is not None:
project.setShowGrid(self.uiShowGridAction.isChecked())
project.update()
def _snapToGridActionSlot(self):
"""
Called when user click on the snap to grid menu item
:return: None
"""
self.snapToGrid(self.uiSnapToGridAction.isChecked())
# save settings
project = Topology.instance().project()
if project is not None:
project.setSnapToGrid(self.uiSnapToGridAction.isChecked())
project.update()
def _lockActionSlot(self):
"""
Called when user click on the lock menu item
:return: None
"""
if self.uiGraphicsView.isEnabled():
for item in self.uiGraphicsView.items():
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem) and not isinstance(item, SvgIconItem):
if self.uiLockAllAction.isChecked() and not item.locked():
item.setLocked(True)
elif not self.uiLockAllAction.isChecked() and item.locked():
item.setLocked(False)
if item.parentItem() is None:
item.updateNode()
item.update()
def analyticsClient(self):
"""
@@ -313,48 +385,44 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
Slot called to create a new project.
"""
# prevents race condition
if self._project_dialog is not None:
return
self._project_dialog = ProjectDialog(self)
self._project_dialog.show()
create_new_project = self._project_dialog.exec_()
# Close the device dock so it repopulates. Done in case switching between cloud and local.
self.uiNodesDockWidget.setVisible(False)
self.uiNodesDockWidget.setWindowTitle("")
if create_new_project:
Topology.instance().createLoadProject(self._project_dialog.getProjectSettings())
self._project_dialog = None
def _newApplianceActionSlot(self):
def _newTemplateActionSlot(self):
"""
Called when user want to create a new appliance
Called when user want to create a new template.
"""
dialog = NewApplianceDialog(self)
dialog = NewTemplateWizard(self)
dialog.show()
dialog.exec_()
# No projects
if Topology.instance().project() is None:
if self._open_file_at_startup:
self.loadPath(self._open_file_at_startup)
self._open_file_at_startup = None
else:
self._newProjectActionSlot()
@qslot
def openApplianceActionSlot(self, *args):
"""
Slot called to open an appliance.
"""
directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
if len(directory) == 0:
directory = self._appliance_dir
if not os.path.exists(self._appliance_dir):
directory = Topology.instance().projectsDirPath()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open appliance", directory,
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Import appliance", directory,
"All files (*.*);;GNS3 Appliance (*.gns3appliance *.gns3a)",
"GNS3 Appliance (*.gns3appliance *.gns3a)")
if path:
self.loadPath(path)
self._appliance_dir = os.path.dirname(path)
def openProjectActionSlot(self):
"""
@@ -365,11 +433,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# If the server is remote we use the new project windows with the project library
self._newProjectActionSlot()
else:
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open project", Topology.instance().projectsDirPath(),
directory = self._project_dir
if self._project_dir is None or not os.path.exists(self._project_dir):
directory = Topology.instance().projectsDirPath()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open project", directory,
"All files (*.*);;GNS3 Project (*.gns3);;GNS3 Portable Project (*.gns3project *.gns3p);;NET files (*.net)",
"GNS3 Project (*.gns3)")
if path:
self.loadPath(path)
self._project_dir = os.path.dirname(path)
def openRecentFileSlot(self):
"""
@@ -454,6 +526,19 @@ 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 template manager
# and appliance manager
project = Topology.instance().project()
if project is None:
self._template_manager.instance().refresh()
self._appliance_manager.instance().refresh()
def _refreshVisibleWidgets(self):
"""
Refresh widgets that should be visible or not
@@ -479,7 +564,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def _importExportConfigsActionSlot(self):
"""
Slot called when importing and exporting configs
for the entire topology.
for the entire project.
"""
options = ["Export configs to a directory", "Import configs from a directory"]
@@ -537,6 +622,60 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# TODO: quality option
return image.save(path)
def showLayers(self, show_layers):
"""
Shows layers in GUI
:param show_layers: boolean
:return: None
"""
NodeItem.show_layer = show_layers
ShapeItem.show_layer = show_layers
for item in self.uiGraphicsView.items():
item.update()
def showGrid(self, show_grid):
"""
Shows grid in GUI
:param show_grid: boolean
:return: None
"""
self.uiGraphicsView.viewport().update()
def snapToGrid(self, snap_to_grid):
"""
Snap to grid in GUI
:param snap_to_grid: boolean
:return: None
"""
self.uiGraphicsView.viewport().update()
def showInterfaceLabels(self, show_interface_labels):
"""
Show interface labels in GUI
:param show_interface_labels: boolean
:return: None
"""
LinkItem.showPortLabels(show_interface_labels)
for item in self.uiGraphicsView.scene().items():
if isinstance(item, LinkItem):
item.adjust()
def _updateZoomSettings(self, zoom=None):
"""
Updates zoom settings
:param zoom integer optional, when not provided then calculated from current view
:return: None
"""
if zoom is None:
zoom = round(self.uiGraphicsView.transform().m11() * 100)
# save settings
project = Topology.instance().project()
if project is not None:
project.setZoom(zoom)
project.update()
def _screenshotActionSlot(self):
"""
Slot called to take a screenshot of the scene.
@@ -549,10 +688,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))
@@ -603,16 +743,18 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot called to scale in the view.
"""
factor_in = pow(2.0, 120 / 240.0)
factor_in = pow(2.0, 60 / 240.0)
self.uiGraphicsView.scaleView(factor_in)
self._updateZoomSettings()
def _zoomOutActionSlot(self):
"""
Slot called to scale out the view.
"""
factor_out = pow(2.0, -120 / 240.0)
factor_out = pow(2.0, -60 / 240.0)
self.uiGraphicsView.scaleView(factor_out)
self._updateZoomSettings()
def _zoomResetActionSlot(self):
"""
@@ -620,6 +762,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
self.uiGraphicsView.resetTransform()
self._updateZoomSettings()
def _fitInViewActionSlot(self):
"""
@@ -635,11 +778,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
Slot called to show the layer positions on the scene.
"""
self.showLayers(self.uiShowLayersAction.isChecked())
NodeItem.show_layer = self.uiShowLayersAction.isChecked()
ShapeItem.show_layer = self.uiShowLayersAction.isChecked()
for item in self.uiGraphicsView.items():
item.update()
# save settings
project = Topology.instance().project()
if project is not None:
project.setShowLayers(self.uiShowLayersAction.isChecked())
project.update()
def _resetPortLabelsActionSlot(self):
"""
@@ -656,10 +801,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot called to show the port names on the scene.
"""
LinkItem.showPortLabels(self.uiShowPortNamesAction.isChecked())
for item in self.uiGraphicsView.scene().items():
if isinstance(item, LinkItem):
item.adjust()
self.showInterfaceLabels(self.uiShowPortNamesAction.isChecked())
# save settings
project = Topology.instance().project()
if project is not None:
project.setShowInterfaceLabels(self.uiShowPortNamesAction.isChecked())
project.update()
def _startAllActionSlot(self):
"""
@@ -716,7 +864,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):
"""
@@ -737,7 +885,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
return
self._pictures_dir = os.path.dirname(path)
image = QtGui.QPixmap(path)
QtGui.QPixmap(path)
self.uiGraphicsView.addImage(path)
def _drawRectangleActionSlot(self):
@@ -754,6 +902,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiGraphicsView.addEllipse(self.uiDrawEllipseAction.isChecked())
def _drawLineActionSlot(self):
"""
Slot called when adding a line on the scene.
"""
self.uiGraphicsView.addLine(self.uiDrawLineAction.isChecked())
def _onlineHelpActionSlot(self):
"""
Slot to launch a browser pointing to the documentation page.
@@ -781,9 +936,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
setup_wizard.show()
res = setup_wizard.exec_()
# start and connect to the local server if needed
LocalServer.instance().localServerAutoStartIfRequire()
if res:
self._newApplianceActionSlot()
LocalServer.instance().localServerAutoStartIfRequired()
def _aboutQtActionSlot(self):
"""
@@ -842,8 +995,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
else:
self.uiNodesDockWidget.setWindowTitle(title)
self.uiNodesDockWidget.setVisible(True)
self.uiNodesView.clear()
self.uiNodesView.populateNodesView(category)
self.uiNodesDockWidget.populateNodesView(category)
def _localConfigChangedSlot(self):
"""
@@ -912,11 +1064,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._settings["preferences_dialog_geometry"] = bytes(dialog.saveGeometry().toBase64()).decode()
self.setSettings(self._settings)
def _editReadmeActionSlot(self):
"""
Slot to edit the README file
"""
Topology.instance().editReadme()
def resizeEvent(self, event):
self._notif_dialog.resize()
super().resizeEvent(event)
def keyPressEvent(self, event):
"""
@@ -940,6 +1090,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")
@@ -965,6 +1122,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._settings["state"] = bytes(self.saveState().toBase64()).decode()
self.setSettings(self._settings)
Controller.instance().stopListenNotifications()
server = LocalServer.instance()
server.stopLocalServer(wait=True)
@@ -997,12 +1155,31 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
reply = QtWidgets.QMessageBox.warning(self, "GNS3", "Another GNS3 GUI is already running. Continue?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
self.close()
sys.exit(1)
return
run_as_root_path = LocalConfig.instance().runAsRootPath()
if not sys.platform.startswith("win") and os.geteuid() == 0:
# touches file to know that user has run GNS3 as root and to prevent
# from running as user
if not os.path.exists(run_as_root_path):
try:
open(run_as_root_path, 'a').close()
except OSError as e:
log.warning("Cannot write `run_as_root` file due to: {}".format(str(e)))
QtWidgets.QMessageBox.warning(self, "Root", "Running GNS3 as root is not recommended and could be dangerous")
if not sys.platform.startswith("win") and os.geteuid() != 0 and os.path.exists(run_as_root_path):
QtWidgets.QMessageBox.critical(
self, "Run as user",
"GNS3 has been previously run as root. It is not possible "
"to change to another user and GNS3 will be shutdown. Please delete the '{}' file "
"and start the program again.".format(run_as_root_path))
sys.exit(1)
# restore debug level
if self._settings["debug_level"]:
root = logging.getLogger()
@@ -1022,7 +1199,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._setupWizardActionSlot()
else:
# start and connect to the local server if needed
LocalServer.instance().localServerAutoStartIfRequire()
LocalServer.instance().localServerAutoStartIfRequired()
if self._open_file_at_startup:
self.loadPath(self._open_file_at_startup)
self._open_file_at_startup = None
@@ -1151,7 +1328,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
try:
if file_path and os.path.exists(file_path):
action = self.recent_file_actions[index]
action.setText(" {}. {}".format(index + 1, os.path.basename(file_path)))
duplicate = False
for file_path_2 in self._settings["recent_files"]:
if file_path != file_path_2 and os.path.basename(file_path) == os.path.basename(file_path_2):
duplicate = True
break
if duplicate:
action.setText(" {}. {} [{}]".format(index + 1, os.path.basename(file_path), file_path))
else:
action.setText(" {}. {}".format(index + 1, os.path.basename(file_path)))
action.setData(file_path)
action.setVisible(True)
index += 1
@@ -1196,14 +1381,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
Slot called to import a portable project
"""
directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
if len(directory) == 0:
directory = self._portable_project_dir
if not os.path.exists(directory):
directory = Topology.instance().projectsDirPath()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open appliance", directory,
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open portable project", directory,
"All files (*.*);;GNS3 Portable Project (*.gns3project *.gns3p)",
"GNS3 Portable Project (*.gns3project *.gns3p)")
if path:
Topology.instance().importProject(path)
self._portable_project_dir = os.path.dirname(path)
def _editProjectActionSlot(self):
if Topology.instance().project() is None:

View File

@@ -19,9 +19,12 @@ 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
#MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker, TraceNG]
#FIXME: deactivate TraceNG module
MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker]

View File

@@ -21,6 +21,7 @@ Built-in module implementation.
from gns3.qt import QtWidgets
from gns3.local_config import LocalConfig
from gns3.local_server_config import LocalServerConfig
from ..module import Module
from .cloud import Cloud
@@ -29,14 +30,7 @@ from .ethernet_hub import EthernetHub
from .ethernet_switch import EthernetSwitch
from .frame_relay_switch import FrameRelaySwitch
from .atm_switch import ATMSwitch
from .settings import (
BUILTIN_SETTINGS,
CLOUD_SETTINGS,
NAT_SETTINGS,
ETHERNET_HUB_SETTINGS,
ETHERNET_SWITCH_SETTINGS
)
from .settings import BUILTIN_SETTINGS
import logging
log = logging.getLogger(__name__)
@@ -50,39 +44,8 @@ class Builtin(Module):
def __init__(self):
super().__init__()
self._settings = {}
self._nodes = []
self._cloud_nodes = {}
self._nat_nodes = {}
self._ethernet_hubs = {}
self._ethernet_switches = {}
# load the settings
self._loadSettings()
def configChangedSlot(self):
pass
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 _saveSettings(self):
"""
Saves the settings to the persistent settings file.
@@ -90,6 +53,15 @@ class Builtin(Module):
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
server_settings = {}
config = LocalServerConfig.instance()
if self._settings["default_nat_interface"]:
# save some settings to the local server config file
server_settings["default_nat_interface"] = self._settings["default_nat_interface"]
config.saveSettings(self.__class__.__name__, server_settings)
else:
config.deleteSetting(self.__class__.__name__, "default_nat_interface")
def _loadSettings(self):
"""
Loads the settings from the persistent settings file.
@@ -97,219 +69,48 @@ class Builtin(Module):
local_config = LocalConfig.instance()
self._settings = local_config.loadSectionSettings(self.__class__.__name__, BUILTIN_SETTINGS)
self._loadNodes()
def _loadBuilinNodesPerType(self, node_dict, node_type, default_settings):
settings = LocalConfig.instance().settings()
if node_type in settings.get(self.__class__.__name__, {}):
for device in settings[self.__class__.__name__][node_type]:
name = device.get("name")
server = device.get("server")
key = "{server}:{name}".format(server=server, name=name)
if key in node_dict or not name or not server:
continue
node_settings = default_settings.copy()
node_settings.update(device)
node_dict[key] = node_settings
def _loadNodes(self):
"""
Load the built-in nodes from the persistent settings file.
"""
self._loadBuilinNodesPerType(self._cloud_nodes, "cloud_nodes", CLOUD_SETTINGS)
self._loadBuilinNodesPerType(self._ethernet_hubs, "ethernet_hubs", ETHERNET_HUB_SETTINGS)
self._loadBuilinNodesPerType(self._ethernet_switches, "ethernet_switches", ETHERNET_SWITCH_SETTINGS)
def _saveNodes(self):
"""
Saves the built-in nodes to the persistent settings file.
"""
self._settings["cloud_nodes"] = list(self._cloud_nodes.values())
self._settings["ethernet_hubs"] = list(self._ethernet_hubs.values())
self._settings["ethernet_switches"] = list(self._ethernet_switches.values())
self._saveSettings()
def cloudNodes(self):
"""
Returns cloud nodes settings.
:returns: Cloud nodes settings (dictionary)
"""
return self._cloud_nodes
def setCloudNodes(self, new_cloud_nodes):
"""
Sets cloud nodes settings.
:param new_cloud_nodes: cloud nodes settings (dictionary)
"""
self._cloud_nodes = new_cloud_nodes.copy()
self._saveNodes()
def ethernetHubs(self):
"""
Returns Ethernet hubs settings.
:returns: Ethernet hubs settings (dictionary)
"""
return self._ethernet_hubs
def setEthernetHubs(self, new_ethernet_hubs):
"""
Sets Ethernet hubs settings.
:param new_ethernet_hubs: Ethernet hubs settings (dictionary)
"""
self._ethernet_hubs = new_ethernet_hubs.copy()
self._saveNodes()
def ethernetSwitches(self):
"""
Returns Ethernet switches settings.
:returns: Ethernet switches settings (dictionary)
"""
return self._ethernet_switches
def setEthernetSwitches(self, new_ethernet_switches):
"""
Sets Ethernet switches settings.
:param new_ethernet_switches: Ethernet switches settings (dictionary)
"""
self._ethernet_switches = new_ethernet_switches.copy()
self._saveNodes()
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 reset(self):
"""
Resets the module.
"""
self._nodes.clear()
def instantiateNode(self, node_class, server, project):
"""
Instantiate a new node.
:param node_class: Node object
:param server: HTTPClient instance
:param project: Project instance
"""
log.info("instantiating node {}".format(node_class))
# create an instance of the node class
return node_class(self, server, project)
def createNode(self, node, node_name):
"""
Creates a node.
:param node: Node instance
:param node_name: Node name
"""
log.info("creating node {}".format(node))
if isinstance(node, Cloud):
for key, info in self._cloud_nodes.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
return
elif isinstance(node, Nat):
for key, info in self._nat_nodes.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(default_name_format=default_name_format)
return
elif isinstance(node, EthernetHub):
for key, info in self._ethernet_hubs.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
return
elif isinstance(node, EthernetSwitch):
for key, info in self._ethernet_switches.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
return
node.create()
@staticmethod
def findAlternativeInterface(node, missing_interface):
from gns3.main_window import MainWindow
mainwindow = MainWindow.instance()
available_interfaces = []
for interface in node.settings()["interfaces"]:
available_interfaces.append(interface["name"])
if available_interfaces:
selection, ok = QtWidgets.QInputDialog.getItem(mainwindow,
"Cloud interfaces", "Interface {} could not be found\nPlease select an alternative from your existing interfaces:".format(missing_interface),
available_interfaces, 0, False)
if ok:
return selection
QtWidgets.QMessageBox.warning(mainwindow, "Cloud interface", "No alternative interface chosen to replace {} on this host, this may lead to issues".format(missing_interface))
return None
else:
QtWidgets.QMessageBox.critical(mainwindow, "Cloud interface", "Could not find interface {} on this host".format(missing_interface))
return missing_interface
@staticmethod
def getNodeClass(name):
def configurationPage(node_type):
"""
Returns the object with the corresponding name.
Returns the configuration page for this module.
:param name: object name
:returns: QWidget object
"""
if name in globals():
return globals()[name]
from .pages.ethernet_hub_configuration_page import EthernetHubConfigurationPage
from .pages.ethernet_switch_configuration_page import EthernetSwitchConfigurationPage
from .pages.cloud_configuration_page import CloudConfigurationPage
if node_type == "ethernet_hub":
return EthernetHubConfigurationPage
elif node_type == "ethernet_switch":
return EthernetSwitchConfigurationPage
elif node_type == "cloud":
return CloudConfigurationPage
return None
@staticmethod
def getNodeType(name, platform=None):
if name == "cloud":
def getNodeClass(node_type, platform=None):
"""
Returns the class corresponding to node type.
:param node_type: node type (string)
:param platform: not used
:returns: class or None
"""
if node_type == "cloud":
return Cloud
elif name == "nat":
elif node_type == "nat":
return Nat
elif name == "ethernet_hub":
elif node_type == "ethernet_hub":
return EthernetHub
elif name == "ethernet_switch":
elif node_type == "ethernet_switch":
return EthernetSwitch
elif name == "frame_relay_switch":
elif node_type == "frame_relay_switch":
return FrameRelaySwitch
elif name == "atm_switch":
elif node_type == "atm_switch":
return ATMSwitch
return None
@@ -323,59 +124,6 @@ class Builtin(Module):
return [Nat, Cloud, EthernetHub, EthernetSwitch, FrameRelaySwitch, ATMSwitch]
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 = []
for node_class in Builtin.classes():
nodes.append(
{"class": node_class.__name__,
"name": node_class.symbolName(),
"categories": node_class.categories(),
"symbol": node_class.defaultSymbol(),
"builtin": True,
"node_type": node_class.URL_PREFIX
}
)
# add custom cloud node templates
for cloud_node in self._cloud_nodes.values():
nodes.append(
{"class": Cloud.__name__,
"name": cloud_node["name"],
"server": cloud_node["server"],
"symbol": cloud_node["symbol"],
"categories": [cloud_node["category"]]
}
)
# add custom Ethernet hub templates
for hub in self._ethernet_hubs.values():
nodes.append(
{"class": EthernetHub.__name__,
"name": hub["name"],
"server": hub["server"],
"symbol": hub["symbol"],
"categories": [hub["category"]]
}
)
# add custom Ethernet switch templates
for switch in self._ethernet_switches.values():
nodes.append(
{"class": EthernetSwitch.__name__,
"name": switch["name"],
"server": switch["server"],
"symbol": switch["symbol"],
"categories": [switch["category"]]
}
)
return nodes
@staticmethod
def preferencePages():
"""
@@ -400,3 +148,10 @@ class Builtin(Module):
if not hasattr(Builtin, "_instance"):
Builtin._instance = Builtin()
return Builtin._instance
def __str__(self):
"""
Returns the module name.
"""
return "builtin"

View File

@@ -17,7 +17,6 @@
import re
import uuid
from gns3.node import Node
@@ -34,6 +33,7 @@ class ATMSwitch(Node):
:param server: GNS3 server instance
:param project: Project instance
"""
URL_PREFIX = "atm_switch"
def __init__(self, module, server, project):
@@ -44,50 +44,6 @@ class ATMSwitch(Node):
self._always_on = True
self.settings().update({"mappings": {}})
def create(self, name=None, node_id=None, mappings=None, default_name_format="ATM{0}"):
"""
Creates this ATM switch.
:param name: optional name for this switch.
:param node_id: Node identifier on the server
:param mappings: mappings to be automatically added when creating this ATM switch
"""
params = {}
if mappings:
params["mappings"] = mappings
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
:param result: server response (dict)
"""
self.settings()["mappings"] = result["mappings"]
def update(self, new_settings):
"""
Updates the settings for this ATM switch.
: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 _updateCallback(self, result):
"""
Callback for update.
:param result: server response
"""
self.settings()["mappings"] = result["mappings"]
def info(self):
"""
Returns information about this ATM switch.
@@ -96,14 +52,14 @@ class ATMSwitch(Node):
"""
info = """ATM switch {name} is always-on
Local node ID is {id}
Server's Node ID is {node_id}
Running on server {host} with port {port}
Local ID is {id} and server ID is {node_id}
Hardware is Dynamips emulated simple ATM switch
Switch's server runs on {host}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self._compute.name())
host=self._compute.name(),
port=self._compute.port())
port_info = ""
mapping = re.compile(r"""^([0-9]*):([0-9]*):([0-9]*)$""")
@@ -154,41 +110,6 @@ class ATMSwitch(Node):
return info + port_info
def dump(self):
"""
Returns a representation of this ATM switch
(to be saved in a topology file).
:returns: dictionary
"""
atmsw = super().dump()
if self._settings["mappings"]:
atmsw["properties"]["mappings"] = self._settings["mappings"]
return atmsw
def load(self, node_info):
"""
Loads an ATM switch representation
(from a topology file).
:param node_info: representation of the node (dictionary)
"""
super().load(node_info)
properties = node_info["properties"]
name = properties.pop("name")
# ATM switches do not have an UUID before version 2.0
node_id = properties.get("node_id", str(uuid.uuid4()))
mappings = {}
if "mappings" in properties:
mappings = properties["mappings"]
log.info("ATM switch {} is loading".format(name))
self.create(name, node_id, mappings)
def configPage(self):
"""
Returns the configuration page widget to be used by the node properties dialog.
@@ -209,11 +130,6 @@ class ATMSwitch(Node):
return ":/symbols/atm_switch.svg"
@staticmethod
def symbolName():
return "ATM switch"
@staticmethod
def categories():
"""

View File

@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3.node import Node
from .settings import CLOUD_SETTINGS
import logging
log = logging.getLogger(__name__)
@@ -36,56 +37,32 @@ class Cloud(Node):
def __init__(self, module, server, project):
super().__init__(module, server, project)
# this is an always-on node
self.setStatus(Node.started)
self._always_on = True
self._interfaces = {}
self._cloud_settings = {"ports_mapping": []}
self._cloud_settings = {"ports_mapping": [],
"remote_console_host": CLOUD_SETTINGS["remote_console_host"],
"remote_console_port": CLOUD_SETTINGS["remote_console_port"],
"remote_console_type": CLOUD_SETTINGS["remote_console_type"],
"remote_console_http_path": CLOUD_SETTINGS["remote_console_http_path"]
}
self.settings().update(self._cloud_settings)
def interfaces(self):
return self._interfaces
def create(self, name=None, node_id=None, ports=None, default_name_format="Cloud{0}"):
"""
Creates this cloud.
:param name: optional name for this cloud
:param node_id: Node identifier on the server
:param ports: ports to be automatically added when creating this cloud
"""
params = {}
if ports:
params["ports_mapping"] = ports
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result, error=False, **kwargs):
def _createCallback(self, result):
"""
Callback for create.
:param result: server response
"""
if "ports_mapping" in result:
self._settings["ports_mapping"] = result["ports_mapping"].copy()
if "interfaces" in result:
self._interfaces = result["interfaces"].copy()
def update(self, new_settings):
"""
Updates the settings for this cloud.
: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 _updateCallback(self, result):
"""
Callback for update.
@@ -93,12 +70,42 @@ class Cloud(Node):
:param result: server response
"""
if "ports_mapping" in result:
self._settings["ports_mapping"] = result["ports_mapping"].copy()
if "interfaces" in result:
self._interfaces = result["interfaces"].copy()
def consoleType(self):
"""
Get the console type.
"""
return self.settings()["remote_console_type"]
def consoleHost(self):
"""
Returns the host to connect to the console.
:returns: host (string)
"""
return self.settings()["remote_console_host"]
def console(self):
"""
Returns the console port number of this node
:returns: port number
"""
return self.settings()["remote_console_port"]
def consoleHttpPath(self):
"""
Returns the path of the web ui
:returns: string
"""
return self._settings["remote_console_http_path"]
def info(self):
"""
Returns information about this cloud.
@@ -106,9 +113,23 @@ class Cloud(Node):
:returns: formatted string
"""
info = """Cloud device {name} is always-on
This is a node for external connections
""".format(name=self.name())
info = """Cloud {name} is always-on
Running on server {host} with port {port}
""".format(name=self.name(),
host=self.compute().name(),
port=self.compute().port())
if self.consoleType() != "none":
info += """ Remote console is {console_host} on port {console} and type is {console_type}
""".format(console_host=self.consoleHost(),
console=self.console(),
console_type=self.consoleType())
if self.consoleType() in ("http", "https"):
info += """ Remote console HTTP path is '{console_http_path}'
""".format(console_http_path=self.consoleHttpPath())
else:
info += """ No remote console configured
"""
port_info = ""
for port in self._ports:
@@ -140,11 +161,6 @@ This is a node for external connections
return ":/symbols/cloud.svg"
@staticmethod
def symbolName():
return "Cloud"
@staticmethod
def categories():
"""

View File

@@ -20,17 +20,15 @@ Wizard for cloud nodes.
"""
from gns3.qt import QtGui, QtWidgets
from gns3.node import Node
from gns3.dialogs.vm_wizard import VMWizard
from ..ui.cloud_wizard_ui import Ui_CloudNodeWizard
from .. import Builtin
class CloudWizard(VMWizard, Ui_CloudNodeWizard):
"""
Wizard to create a cloud node template.
Wizard to create a cloud node.
:param parent: parent widget
"""
@@ -51,7 +49,6 @@ class CloudWizard(VMWizard, Ui_CloudNodeWizard):
settings = {"name": self.uiNameLineEdit.text(),
"symbol": ":/symbols/cloud.svg",
"category": Node.end_devices,
"server": self._compute_id}
"compute_id": self._compute_id}
return settings

View File

@@ -24,13 +24,12 @@ from gns3.node import Node
from gns3.dialogs.vm_wizard import VMWizard
from ..ui.ethernet_hub_wizard_ui import Ui_EthernetHubWizard
from .. import Builtin
class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
"""
Wizard to create an Ethernet hub template.
Wizard to create an Ethernet hub.
:param parent: parent widget
"""
@@ -57,7 +56,7 @@ class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
settings = {"name": self.uiNameLineEdit.text(),
"symbol": ":/symbols/hub.svg",
"category": Node.switches,
"server": self._compute_id,
"compute_id": self._compute_id,
"ports_mapping": ports}
return settings

View File

@@ -24,13 +24,12 @@ from gns3.node import Node
from gns3.dialogs.vm_wizard import VMWizard
from ..ui.ethernet_switch_wizard_ui import Ui_EthernetSwitchWizard
from .. import Builtin
class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
"""
Wizard to create an Ethernet switch template.
Wizard to create an Ethernet switch.
:param parent: parent widget
"""
@@ -60,7 +59,7 @@ class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
settings = {"name": self.uiNameLineEdit.text(),
"symbol": ":/symbols/ethernet_switch.svg",
"category": Node.switches,
"server": self._compute_id,
"compute_id": self._compute_id,
"ports_mapping": ports}
return settings

View File

@@ -29,6 +29,7 @@ class EthernetHub(Node):
:param server: GNS3 server instance
:param project: Project instance
"""
URL_PREFIX = "ethernet_hub"
def __init__(self, module, server, project):
@@ -39,51 +40,6 @@ class EthernetHub(Node):
self._always_on = True
self.settings().update({"ports_mapping": []})
def create(self, name=None, node_id=None, ports=None, default_name_format="Hub{0}"):
"""
Creates this hub.
:param name: optional name for this hub
:param node_id: node identifier on the server
:param ports: ports to automatically be added when creating this hub
"""
params = {}
if ports:
params["ports_mapping"] = ports
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
:param result: server response (dict)
"""
self.settings()["ports_mapping"] = result["ports_mapping"]
def update(self, new_settings):
"""
Updates the settings for this Ethernet hub.
:param new_settings: settings dictionary
"""
params = {}
if "name" in new_settings:
params["name"] = new_settings["name"]
if "ports_mapping" in new_settings:
params["ports_mapping"] = new_settings["ports_mapping"]
if params:
self._update(params)
def _updateCallback(self, result):
"""
Callback for update.
:param result: server response
"""
self.settings()["ports_mapping"] = result["ports_mapping"]
def info(self):
"""
Returns information about this Ethernet hub.
@@ -92,13 +48,13 @@ class EthernetHub(Node):
"""
info = """Ethernet hub {name} is always-on
Local node ID is {id}
Server's node ID is {node_id}
Hub's server runs on {host}
Running on server {host} with port {port}
Local ID is {id} and server ID is {node_id}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self.compute().name())
host=self.compute().name(),
port=self.compute().port())
port_info = ""
for port in self._ports:
@@ -130,11 +86,6 @@ class EthernetHub(Node):
return ":/symbols/hub.svg"
@staticmethod
def symbolName():
return "Ethernet hub"
@staticmethod
def categories():
"""

View File

@@ -22,7 +22,6 @@ log = logging.getLogger(__name__)
class EthernetSwitch(Node):
"""
Ethernet switch.
@@ -30,6 +29,7 @@ class EthernetSwitch(Node):
:param server: GNS3 server instance
:param project: Project instance
"""
URL_PREFIX = "ethernet_switch"
def __init__(self, module, server, project):
@@ -38,51 +38,7 @@ class EthernetSwitch(Node):
# this is an always-on node
self.setStatus(Node.started)
self._always_on = True
self.settings().update({"ports_mapping": []})
def create(self, name=None, node_id=None, ports=None, default_name_format="SW{0}"):
"""
Creates this Ethernet switch.
:param name: optional name for this switch
:param node_id: node identifier on the server
:param ports: ports to be automatically added when creating this switch
"""
params = {}
if ports:
params["ports_mapping"] = ports
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
:param result: server response (dict)
"""
self.settings()["ports_mapping"] = result["ports_mapping"]
def update(self, new_settings):
"""
Updates the settings for this Ethernet switch.
: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 _updateCallback(self, result):
"""
Callback for update.
:param result: server response
"""
self.settings()["ports_mapping"] = result["ports_mapping"]
self.settings().update({"ports_mapping": [], "console_type": "none"})
def info(self):
"""
@@ -92,13 +48,16 @@ class EthernetSwitch(Node):
"""
info = """Ethernet switch {name} is always-on
Local node ID is {id}
Server's Node ID is {node_id}
Switch's server runs on {host}
Running on server {host} with port {port}
Local ID is {id} and server ID is {node_id}
Console is on port {console} and type is {console_type}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self.compute().name())
host=self.compute().name(),
port=self.compute().port(),
console=self._settings["console"],
console_type=self._settings["console_type"])
port_info = ""
for port in self._ports:
@@ -150,11 +109,6 @@ class EthernetSwitch(Node):
return ":/symbols/ethernet_switch.svg"
@staticmethod
def symbolName():
return "Ethernet switch"
@staticmethod
def categories():
"""

View File

@@ -41,50 +41,6 @@ class FrameRelaySwitch(Node):
self._always_on = True
self.settings().update({"mappings": {}})
def create(self, name=None, node_id=None, mappings={}, default_name_format="FR{0}"):
"""
Creates this Frame Relay switch.
:param name: name for this switch.
:param node_id: node identifier on the server
:param mappings: mappings to be automatically added when creating this Frame relay switch
"""
params = {}
if mappings:
params["mappings"] = mappings
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
:param result: server response (dict)
"""
self.settings()["mappings"] = result["mappings"]
def update(self, new_settings):
"""
Updates the settings for this Frame Relay switch.
: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 _updateCallback(self, result):
"""
Callback for update.
:param result: server response
"""
self.settings()["mappings"] = result["mappings"]
def info(self):
"""
Returns information about this Frame Relay switch.
@@ -93,14 +49,13 @@ class FrameRelaySwitch(Node):
"""
info = """Frame relay switch {name} is always-on
Local node ID is {id}
Server's Node ID is {node_id}
Running on server {host} with port {port}
Local ID is {id} and server ID is {node_id}
Hardware is Dynamips emulated simple Frame relay switch
Switch's server runs on {host}:{port}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self._compute.host(),
host=self._compute.name(),
port=self._compute.port())
port_info = ""
@@ -151,11 +106,6 @@ class FrameRelaySwitch(Node):
return ":/symbols/frame_relay_switch.svg"
@staticmethod
def symbolName():
return "Frame Relay switch"
@staticmethod
def categories():
"""

View File

@@ -42,56 +42,6 @@ class Nat(Node):
self._nat_settings = {}
self.settings().update(self._nat_settings)
def interfaces(self):
return self._interfaces
def create(self, name=None, node_id=None, default_name_format="Nat{0}"):
"""
Creates this nat.
:param name: optional name for this nat
:param node_id: Node identifier on the server
"""
params = {}
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result, error=False, **kwargs):
"""
Callback for create.
:param result: server response
"""
if error:
log.error("Error while creating nat: {}".format(result["message"]))
return
def update(self, new_settings):
"""
Updates the settings for this nat.
: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 _updateCallback(self, result, error=False, **kwargs):
"""
Callback for update.
:param result: server response
"""
if error:
log.error("Error while creating nat: {}".format(result["message"]))
return
def info(self):
"""
Returns information about this nat.
@@ -99,9 +49,11 @@ class Nat(Node):
:returns: formatted string
"""
info = """Nat device {name} is always-on
This is a node for external connections
""".format(name=self.name())
info = """NAT node {name} is always-on
Running on server {host} with port {port}
""".format(name=self.name(),
host=self.compute().name(),
port=self.compute().port())
port_info = ""
for port in self._ports:
@@ -123,11 +75,6 @@ This is a node for external connections
return ":/symbols/cloud.svg"
@staticmethod
def symbolName():
return "Nat"
@staticmethod
def categories():
"""

View File

@@ -35,8 +35,8 @@ class ATMSwitchConfigurationPage(QtWidgets.QWidget, Ui_atmSwitchConfigPageWidget
super().__init__()
self.setupUi(self)
self._mapping = {}
self._node = None
# connect slots
self.uiAddPushButton.clicked.connect(self._addMappingSlot)
self.uiDeletePushButton.clicked.connect(self._deleteMappingSlot)
self.uiMappingTreeWidget.itemActivated.connect(self._mappingSelectedSlot)

View File

@@ -16,45 +16,69 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Configuration page for Built-in preferences.
Configuration page for builtins preferences.
"""
from gns3.qt import QtWidgets
from gns3.utils.interfaces import interfaces
from .. import Builtin
from ..ui.builtin_preferences_page_ui import Ui_BuiltinPreferencesPageWidget
from ..settings import BUILTIN_SETTINGS
class BuiltinPreferencesPage(QtWidgets.QWidget, Ui_BuiltinPreferencesPageWidget):
"""QWidget preference page for Built-in."""
"""
QWidget preference page for builtins.
"""
def __init__(self):
super().__init__()
self.setupUi(self)
# connect signals
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
def _restoreDefaultsSlot(self):
"""Slot to populate the page widgets with the default settings."""
"""
Slot to populate the page widgets with the default settings.
"""
self._populateWidgets(BUILTIN_SETTINGS)
def _populateWidgets(self, settings):
"""Populates the widgets with the settings.
"""
Populates the widgets with the settings.
:param settings: Built-in settings
:param settings: builtins settings
"""
self.uiNATInterfaceComboBox.clear()
self.uiNATInterfaceComboBox.addItem("")
for interface in interfaces():
self.uiNATInterfaceComboBox.addItem(interface["name"])
# load the default NAT interface
index = self.uiNATInterfaceComboBox.findText(settings["default_nat_interface"])
if index != -1:
self.uiNATInterfaceComboBox.setCurrentIndex(index)
def loadPreferences(self):
"""Loads Built-in preferences."""
"""
Loads builtins preferences.
"""
builtin_settings = Builtin.instance().settings()
self._populateWidgets(builtin_settings)
def savePreferences(self):
"""Saves Built-in preferences."""
"""
Saves builtins preferences.
"""
new_settings = {}
# save the default NAT interface
default_nat_interface = self.uiNATInterfaceComboBox.currentText()
new_settings["default_nat_interface"] = default_nat_interface
Builtin.instance().setSettings(new_settings)

View File

@@ -19,8 +19,9 @@
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 ....dialogs.node_properties_dialog import ConfigurationError
from gns3.controller import Controller
from gns3.node import Node
@@ -50,6 +51,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 +59,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
@@ -65,8 +68,29 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
self.uiAddUDPPushButton.clicked.connect(self._UDPAddSlot)
self.uiDeleteUDPPushButton.clicked.connect(self._UDPDeleteSlot)
# connect other slots
self.uiShowSpecialInterfacesCheckBox.stateChanged.connect(self._showSpecialInterfacesSlot)
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
self.uiConsoleTypeComboBox.currentTextChanged.connect(self._consoleTypeChangedSlot)
# add an icon to the 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):
"""
@@ -115,6 +139,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 +226,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.
@@ -312,6 +354,9 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
self.uiUDPNameLineEdit.setText("UDP tunnel {}".format(nb_tunnels + 1))
def _showSpecialInterfacesSlot(self, state):
"""
Shows special Ethernet interfaces.
"""
self.uiEthernetComboBox.clear()
index = 0
@@ -338,6 +383,9 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
def _loadNetworkInterfaces(self, interfaces):
"""
Loads Ethernet and TAP interfaces.
"""
self.uiEthernetComboBox.clear()
index = 0
@@ -369,6 +417,25 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
self._interfaces = result
self._loadNetworkInterfaces(result)
def _consoleTypeChangedSlot(self, console_type):
"""
Slot called when the console type has been changed.
:param console_type: console type
"""
if console_type in ("http", "https"):
self.uiConsoleHttpPathLineEdit.setEnabled(True)
else:
self.uiConsoleHttpPathLineEdit.setEnabled(False)
if console_type != "none":
self.uiConsoleHostLineEdit.setEnabled(True)
self.uiConsolePortSpinBox.setEnabled(True)
else:
self.uiConsoleHostLineEdit.setEnabled(False)
self.uiConsolePortSpinBox.setEnabled(False)
def loadSettings(self, settings, node=None, group=False):
"""
Loads the cloud settings.
@@ -380,13 +447,18 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
if not group:
self.uiNameLineEdit.setText(settings["name"])
self.uiConsoleHostLineEdit.setText(settings["remote_console_host"])
self.uiConsolePortSpinBox.setValue(settings["remote_console_port"])
index = self.uiConsoleTypeComboBox.findText(settings["remote_console_type"])
if index != -1:
self.uiConsoleTypeComboBox.setCurrentIndex(index)
self.uiConsoleHttpPathLineEdit.setText(settings["remote_console_http_path"])
else:
self.uiNameLineEdit.setEnabled(False)
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
@@ -401,7 +473,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
if index != -1:
self.uiCategoryComboBox.setCurrentIndex(index)
Controller.instance().getCompute("/network/interfaces", settings["server"],
Controller.instance().getCompute("/network/interfaces", settings["compute_id"],
self._getInterfacesFromServerCallback,
progressText="Retrieving network interfaces...")
@@ -459,6 +531,17 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
if not group:
settings["name"] = self.uiNameLineEdit.text()
console_host = self.uiConsoleHostLineEdit.text().strip()
if self.uiConsoleTypeComboBox.currentText().lower() != "none":
if not console_host:
QtWidgets.QMessageBox.critical(self, "Console host", "Console host cannot be blank if console type is not set to none")
raise ConfigurationError()
settings["remote_console_host"] = console_host
settings["remote_console_port"] = self.uiConsolePortSpinBox.value()
settings["remote_console_type"] = self.uiConsoleTypeComboBox.currentText().lower()
settings["remote_console_http_path"] = self.uiConsoleHttpPathLineEdit.text().strip()
if not node:
# these are template settings

View File

@@ -21,13 +21,14 @@ Configuration page for cloud node preferences.
import copy
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
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.template_manager import TemplateManager
from gns3.controller import Controller
from gns3.template import Template
from .. import Builtin
from ..settings import CLOUD_SETTINGS
from ..ui.cloud_preferences_page_ui import Ui_CloudPreferencesPageWidget
from ..pages.cloud_configuration_page import CloudConfigurationPage
@@ -53,6 +54,11 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
self.uiCloudNodesTreeWidget.itemSelectionChanged.connect(self._cloudNodeChangedSlot)
def _createSectionItem(self, name):
"""
Adds a new section to the tree widget.
:param name: section name
"""
section_item = QtWidgets.QTreeWidgetItem(self.uiCloudNodeInfoTreeWidget)
section_item.setText(0, name)
@@ -62,15 +68,25 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
return section_item
def _refreshInfo(self, cloud_node):
"""
Refreshes the content of the tree widget.
"""
self.uiCloudNodeInfoTreeWidget.clear()
# fill out the General section
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", cloud_node["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", cloud_node.get("template_id", "none")])
if cloud_node["remote_console_type"] != "none":
QtWidgets.QTreeWidgetItem(section_item, ["Console host:", cloud_node["remote_console_host"]])
QtWidgets.QTreeWidgetItem(section_item, ["Console port:", "{}".format(cloud_node["remote_console_port"])])
if cloud_node["remote_console_type"] in ("http", "https"):
QtWidgets.QTreeWidgetItem(section_item, ["Console HTTP path:", cloud_node["remote_console_http_path"]])
QtWidgets.QTreeWidgetItem(section_item, ["Console type:", cloud_node["remote_console_type"]])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", cloud_node["default_name_format"]])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["server"]).name()])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["compute_id"]).name()])
except KeyError:
pass
@@ -81,7 +97,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
def _cloudNodeChangedSlot(self):
"""
Loads a selected cloud node template from the tree widget.
Loads a selected cloud nodes from the tree widget.
"""
selection = self.uiCloudNodesTreeWidget.selectedItems()
@@ -98,14 +114,14 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
def _newCloudNodeSlot(self):
"""
Creates a new cloud node template.
Creates a new cloud node.
"""
wizard = CloudWizard(self._cloud_nodes, parent=self)
wizard.show()
if wizard.exec_():
new_cloud_settings = wizard.getSettings()
key = "{server}:{name}".format(server=new_cloud_settings["server"], name=new_cloud_settings["name"])
key = "{server}:{name}".format(server=new_cloud_settings["compute_id"], name=new_cloud_settings["name"])
self._cloud_nodes[key] = CLOUD_SETTINGS.copy()
self._cloud_nodes[key].update(new_cloud_settings)
@@ -119,7 +135,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
def _editCloudNodeSlot(self):
"""
Edits a cloud node template.
Edits a cloud node.
"""
item = self.uiCloudNodesTreeWidget.currentItem()
@@ -132,10 +148,10 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
# update the icon
Controller.instance().getSymbolIcon(cloud_node["symbol"], qpartial(self._setItemIcon, item))
if cloud_node["name"] != item.text(0):
new_key = "{server}:{name}".format(server=cloud_node["server"], name=cloud_node["name"])
new_key = "{server}:{name}".format(server=cloud_node["compute_id"], name=cloud_node["name"])
if new_key in self._cloud_nodes:
QtWidgets.QMessageBox.critical(self, "Cloud node", "Cloud node name {} already exists for server {}".format(cloud_node["name"],
cloud_node["server"]))
cloud_node["compute_id"]))
cloud_node["name"] = item.text(0)
return
self._cloud_nodes[new_key] = self._cloud_nodes[key]
@@ -146,7 +162,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
def _deleteCloudNodeSlot(self):
"""
Deletes a cloud node template.
Deletes a cloud node.
"""
for item in self.uiCloudNodesTreeWidget.selectedItems():
@@ -160,10 +176,17 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
Loads the cloud node preferences.
"""
builtin_module = Builtin.instance()
self._cloud_nodes = copy.deepcopy(builtin_module.cloudNodes())
self._items.clear()
self._cloud_nodes = {}
templates = TemplateManager.instance().templates()
for template_id, template in templates.items():
if template.template_type() == "cloud" and not template.builtin():
name = template.name()
server = template.compute_id()
#TODO: use template id for the key
key = "{server}:{name}".format(server=server, name=name)
self._cloud_nodes[key] = copy.deepcopy(template.settings())
self._items.clear()
for key, cloud_node in self._cloud_nodes.items():
item = QtWidgets.QTreeWidgetItem(self.uiCloudNodesTreeWidget)
item.setText(0, cloud_node["name"])
@@ -177,6 +200,10 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
def _setItemIcon(self, item, icon):
"""
Sets an item icon.
"""
item.setIcon(0, icon)
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
@@ -185,4 +212,11 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
Saves the cloud node preferences.
"""
Builtin.instance().setCloudNodes(self._cloud_nodes)
templates = []
for template in TemplateManager.instance().templates().values():
if template.template_type() != "cloud":
templates.append(template)
for template_settings in self._cloud_nodes.values():
templates.append(Template(template_settings))
TemplateManager.instance().updateList(templates)

View File

@@ -75,7 +75,6 @@ class EthernetHubConfigurationPage(QtWidgets.QWidget, Ui_ethernetHubConfigPageWi
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

View File

@@ -21,14 +21,15 @@ Configuration page for Ethernet hub preferences.
import copy
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
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.template_manager import TemplateManager
from gns3.controller import Controller
from gns3.template import Template
from .. import Builtin
from ..settings import ETHERNET_HUB_SETTINGS
from ..ui.ethernet_hub_preferences_page_ui import Ui_EthernetHubPreferencesPageWidget
from ..pages.ethernet_hub_configuration_page import EthernetHubConfigurationPage
@@ -54,6 +55,11 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
self.uiEthernetHubsTreeWidget.itemSelectionChanged.connect(self._ethernetHubChangedSlot)
def _createSectionItem(self, name):
"""
Adds a new section to the tree widget.
:param name: section name
"""
section_item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubInfoTreeWidget)
section_item.setText(0, name)
@@ -63,15 +69,19 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
return section_item
def _refreshInfo(self, ethernet_hub):
"""
Refreshes the content of the tree widget.
"""
self.uiEthernetHubInfoTreeWidget.clear()
# fill out the General section
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_hub["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", ethernet_hub.get("template_id", "none")])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_hub["default_name_format"]])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["server"]).name()])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["compute_id"]).name()])
except KeyError:
pass
QtWidgets.QTreeWidgetItem(section_item, ["Number of ports:", str(len(ethernet_hub["ports_mapping"]))])
@@ -83,7 +93,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
def _ethernetHubChangedSlot(self):
"""
Loads a selected Ethernet hub template from the tree widget.
Loads a selected Ethernet hub from the tree widget.
"""
selection = self.uiEthernetHubsTreeWidget.selectedItems()
@@ -100,14 +110,14 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
def _newEthernetHubSlot(self):
"""
Creates a new Ethernet hub template.
Creates a new Ethernet hub.
"""
wizard = EthernetHubWizard(self._ethernet_hubs, parent=self)
wizard.show()
if wizard.exec_():
new_ethernet_hub_settings = wizard.getSettings()
key = "{server}:{name}".format(server=new_ethernet_hub_settings["server"], name=new_ethernet_hub_settings["name"])
key = "{server}:{name}".format(server=new_ethernet_hub_settings["compute_id"], name=new_ethernet_hub_settings["name"])
self._ethernet_hubs[key] = ETHERNET_HUB_SETTINGS.copy()
self._ethernet_hubs[key].update(new_ethernet_hub_settings)
@@ -120,7 +130,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
def _editEthernetHubSlot(self):
"""
Edits an Ethernet hub template.
Edits an Ethernet hub.
"""
item = self.uiEthernetHubsTreeWidget.currentItem()
@@ -133,10 +143,10 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
# update the icon
Controller.instance().getSymbolIcon(ethernet_hub["symbol"], qpartial(self._setItemIcon, item))
if ethernet_hub["name"] != item.text(0):
new_key = "{server}:{name}".format(server=ethernet_hub["server"], name=ethernet_hub["name"])
new_key = "{server}:{name}".format(server=ethernet_hub["compute_id"], name=ethernet_hub["name"])
if new_key in self._ethernet_hubs:
QtWidgets.QMessageBox.critical(self, "Ethernet hub", "Ethernet hub name {} already exists for server {}".format(ethernet_hub["name"],
ethernet_hub["server"]))
ethernet_hub["compute_id"]))
ethernet_hub["name"] = item.text(0)
return
self._ethernet_hubs[new_key] = self._ethernet_hubs[key]
@@ -147,7 +157,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
def _deleteEthernetHubSlot(self):
"""
Deletes an Ethernet hub template.
Deletes an Ethernet hub.
"""
for item in self.uiEthernetHubsTreeWidget.selectedItems():
@@ -161,10 +171,17 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
Loads the ethernet hub preferences.
"""
builtin_module = Builtin.instance()
self._ethernet_hubs = copy.deepcopy(builtin_module.ethernetHubs())
self._items.clear()
self._ethernet_hubs = {}
templates = TemplateManager.instance().templates()
for template_id, template in templates.items():
if template.template_type() == "ethernet_hub" and not template.builtin():
name = template.name()
server = template.compute_id()
#TODO: use template id for the key
key = "{server}:{name}".format(server=server, name=name)
self._ethernet_hubs[key] = copy.deepcopy(template.settings())
self._items.clear()
for key, ethernet_hub in self._ethernet_hubs.items():
item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubsTreeWidget)
item.setText(0, ethernet_hub["name"])
@@ -178,6 +195,10 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
def _setItemIcon(self, item, icon):
"""
Sets an item icon.
"""
item.setIcon(0, icon)
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
@@ -186,4 +207,11 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
Saves the Ethernet hub preferences.
"""
Builtin.instance().setEthernetHubs(self._ethernet_hubs)
templates = []
for template in TemplateManager.instance().templates().values():
if template.template_type() != "ethernet_hub":
templates.append(template)
for template_settings in self._ethernet_hubs.values():
templates.append(Template(template_settings))
TemplateManager.instance().updateList(templates)

View File

@@ -196,7 +196,6 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
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
@@ -223,12 +222,17 @@ 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
# load the console type
index = self.uiConsoleTypeComboBox.findText(settings["console_type"])
if index != -1:
self.uiConsoleTypeComboBox.setCurrentIndex(index)
self.uiPortsTreeWidget.resizeColumnToContents(0)
self.uiPortsTreeWidget.resizeColumnToContents(1)
if len(self._ports) > 0:
@@ -266,5 +270,8 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
# save console type
settings["console_type"] = self.uiConsoleTypeComboBox.currentText().lower()
settings["ports_mapping"] = list(self._ports.values())
return settings

View File

@@ -21,14 +21,15 @@ Configuration page for Ethernet switch preferences.
import copy
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
from gns3.qt import QtCore, QtWidgets, qpartial
from gns3.controller import Controller
from gns3.main_window import MainWindow
from gns3.dialogs.configuration_dialog import ConfigurationDialog
from gns3.compute_manager import ComputeManager
from gns3.template_manager import TemplateManager
from gns3.template import Template
from .. import Builtin
from ..settings import ETHERNET_SWITCH_SETTINGS
from ..ui.ethernet_switch_preferences_page_ui import Ui_EthernetSwitchPreferencesPageWidget
from ..pages.ethernet_switch_configuration_page import EthernetSwitchConfigurationPage
@@ -54,6 +55,11 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
self.uiEthernetSwitchesTreeWidget.itemSelectionChanged.connect(self._ethernetSwitchChangedSlot)
def _createSectionItem(self, name):
"""
Adds a new section to the tree widget.
:param name: section name
"""
section_item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchInfoTreeWidget)
section_item.setText(0, name)
@@ -63,18 +69,23 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
return section_item
def _refreshInfo(self, ethernet_switch):
"""
Refreshes the content of the tree widget.
"""
self.uiEthernetSwitchInfoTreeWidget.clear()
# fill out the General section
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_switch["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", ethernet_switch.get("template_id", "none")])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_switch["default_name_format"]])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["server"]).name()])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["compute_id"]).name()])
except KeyError:
pass
QtWidgets.QTreeWidgetItem(section_item, ["Console type:", ethernet_switch["console_type"]])
for port in ethernet_switch["ports_mapping"]:
section_item = self._createSectionItem("Port{}".format(port["port_number"]))
QtWidgets.QTreeWidgetItem(section_item, ["Name:", port["name"]])
@@ -88,7 +99,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
def _ethernetSwitchChangedSlot(self):
"""
Loads a selected Ethernet switch template from the tree widget.
Loads a selected Ethernet switch from the tree widget.
"""
selection = self.uiEthernetSwitchesTreeWidget.selectedItems()
@@ -105,14 +116,14 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
def _newEthernetSwitchSlot(self):
"""
Creates a new Ethernet switch template.
Creates a new Ethernet switch.
"""
wizard = EthernetSwitchWizard(self._ethernet_switches, parent=self)
wizard.show()
if wizard.exec_():
new_ethernet_switch_settings = wizard.getSettings()
key = "{server}:{name}".format(server=new_ethernet_switch_settings["server"], name=new_ethernet_switch_settings["name"])
key = "{server}:{name}".format(server=new_ethernet_switch_settings["compute_id"], name=new_ethernet_switch_settings["name"])
self._ethernet_switches[key] = ETHERNET_SWITCH_SETTINGS.copy()
self._ethernet_switches[key].update(new_ethernet_switch_settings)
@@ -126,7 +137,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
def _editEthernetSwitchSlot(self):
"""
Edits an Ethernet switch template.
Edits an Ethernet switch.
"""
item = self.uiEthernetSwitchesTreeWidget.currentItem()
@@ -139,10 +150,10 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
# update the icon
Controller.instance().getSymbolIcon(ethernet_switch["symbol"], qpartial(self._setItemIcon, item))
if ethernet_switch["name"] != item.text(0):
new_key = "{server}:{name}".format(server=ethernet_switch["server"], name=ethernet_switch["name"])
new_key = "{server}:{name}".format(server=ethernet_switch["compute_id"], name=ethernet_switch["name"])
if new_key in self._ethernet_switches:
QtWidgets.QMessageBox.critical(self, "Ethernet switch", "Ethernet switch name {} already exists for server {}".format(ethernet_switch["name"],
ethernet_switch["server"]))
ethernet_switch["compute_id"]))
ethernet_switch["name"] = item.text(0)
return
self._ethernet_switches[new_key] = self._ethernet_switches[key]
@@ -153,7 +164,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
def _deleteEthernetSwitchSlot(self):
"""
Deletes an Ethernet switch template.
Deletes an Ethernet switch.
"""
for item in self.uiEthernetSwitchesTreeWidget.selectedItems():
if item:
@@ -166,10 +177,17 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
Loads the ethernet switch preferences.
"""
builtin_module = Builtin.instance()
self._ethernet_switches = copy.deepcopy(builtin_module.ethernetSwitches())
self._items.clear()
self._ethernet_switches = {}
templates = TemplateManager.instance().templates()
for template_id, template in templates.items():
if template.template_type() == "ethernet_switch" and not template.builtin():
name = template.name()
server = template.compute_id()
#TODO: use template id for the key
key = "{server}:{name}".format(server=server, name=name)
self._ethernet_switches[key] = copy.deepcopy(template.settings())
self._items.clear()
for key, ethernet_switch in self._ethernet_switches.items():
item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchesTreeWidget)
item.setText(0, ethernet_switch["name"])
@@ -182,13 +200,24 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
self.uiEthernetSwitchesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
def _setItemIcon(self, item, icon):
"""
Sets an item icon.
"""
item.setIcon(0, icon)
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
def savePreferences(self):
"""
Saves the Ethernet switch preferences.
"""
Builtin.instance().setEthernetSwitches(self._ethernet_switches)
templates = []
for template in TemplateManager.instance().templates().values():
if template.template_type() != "ethernet_switch":
templates.append(template)
for template_settings in self._ethernet_switches.values():
templates.append(Template(template_settings))
TemplateManager.instance().updateList(templates)
def _setItemIcon(self, item, icon):
item.setIcon(0, icon)
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)

View File

@@ -19,26 +19,30 @@
Default Built-in settings.
"""
import sys
from gns3.node import Node
if sys.platform.startswith("linux"):
DEFAULT_NAT_INTERFACE = "virbr0"
else:
DEFAULT_NAT_INTERFACE = "vmnet8"
BUILTIN_SETTINGS = {
}
NAT_SETTINGS = {
"name": "",
"default_name_format": "Nat{0}",
"symbol": ":/symbols/cloud.svg",
"category": Node.end_devices,
"ports_mapping": [],
"default_nat_interface": DEFAULT_NAT_INTERFACE
}
CLOUD_SETTINGS = {
"name": "",
"remote_console_host": "127.0.0.1",
"remote_console_port": 23,
"remote_console_type": "none",
"remote_console_http_path": "/",
"default_name_format": "Cloud{0}",
"symbol": ":/symbols/cloud.svg",
"category": Node.end_devices,
"ports_mapping": [],
"node_type": "cloud"
}
ETHERNET_HUB_SETTINGS = {
@@ -47,6 +51,7 @@ ETHERNET_HUB_SETTINGS = {
"symbol": ":/symbols/hub.svg",
"category": Node.switches,
"ports_mapping": [],
"node_type": "ethernet_hub"
}
ETHERNET_SWITCH_SETTINGS = {
@@ -54,5 +59,7 @@ ETHERNET_SWITCH_SETTINGS = {
"default_name_format": "Switch{0}",
"symbol": ":/symbols/ethernet_switch.svg",
"category": Node.switches,
"console_type": "none",
"ports_mapping": [],
"node_type": "ethernet_switch"
}

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>330</width>
<height>200</height>
<width>456</width>
<height>385</height>
</rect>
</property>
<property name="windowTitle">
@@ -26,8 +26,18 @@
<attribute name="title">
<string>Local settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Default NAT interface:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QComboBox" name="uiNATInterfaceComboBox"/>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>

View File

@@ -2,8 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/builtin_preferences_page.ui'
#
# Created: Wed Dec 7 21:40:18 2016
# by: PyQt5 UI code generator 5.2.1
# Created by: PyQt5 UI code generator 5.9.1
#
# WARNING! All changes made in this file will be lost!
@@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_BuiltinPreferencesPageWidget(object):
def setupUi(self, BuiltinPreferencesPageWidget):
BuiltinPreferencesPageWidget.setObjectName("BuiltinPreferencesPageWidget")
BuiltinPreferencesPageWidget.resize(330, 200)
BuiltinPreferencesPageWidget.resize(456, 385)
self.verticalLayout = QtWidgets.QVBoxLayout(BuiltinPreferencesPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(BuiltinPreferencesPageWidget)
@@ -20,10 +19,16 @@ class Ui_BuiltinPreferencesPageWidget(object):
self.uiTabWidget.setObjectName("uiTabWidget")
self.uiServerSettingsTabWidget = QtWidgets.QWidget()
self.uiServerSettingsTabWidget.setObjectName("uiServerSettingsTabWidget")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.uiServerSettingsTabWidget)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.gridLayout = QtWidgets.QGridLayout(self.uiServerSettingsTabWidget)
self.gridLayout.setObjectName("gridLayout")
self.label = QtWidgets.QLabel(self.uiServerSettingsTabWidget)
self.label.setObjectName("label")
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
self.uiNATInterfaceComboBox = QtWidgets.QComboBox(self.uiServerSettingsTabWidget)
self.uiNATInterfaceComboBox.setObjectName("uiNATInterfaceComboBox")
self.gridLayout.addWidget(self.uiNATInterfaceComboBox, 1, 0, 1, 1)
spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_2.addItem(spacerItem)
self.gridLayout.addItem(spacerItem, 2, 0, 1, 1)
self.uiTabWidget.addTab(self.uiServerSettingsTabWidget, "")
self.verticalLayout.addWidget(self.uiTabWidget)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
@@ -42,6 +47,7 @@ class Ui_BuiltinPreferencesPageWidget(object):
def retranslateUi(self, BuiltinPreferencesPageWidget):
_translate = QtCore.QCoreApplication.translate
BuiltinPreferencesPageWidget.setWindowTitle(_translate("BuiltinPreferencesPageWidget", "Built-in"))
self.label.setText(_translate("BuiltinPreferencesPageWidget", "Default NAT interface:"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("BuiltinPreferencesPageWidget", "Local settings"))
self.uiRestoreDefaultsPushButton.setText(_translate("BuiltinPreferencesPageWidget", "Restore defaults"))

View File

@@ -6,12 +6,12 @@
<rect>
<x>0</x>
<y>0</y>
<width>821</width>
<height>363</height>
<width>979</width>
<height>564</height>
</rect>
</property>
<property name="windowTitle">
<string>Cloud configuration</string>
<string>Cloud template configuration</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;A cloud node allows you to connect your project to the &amp;quot;real world&amp;quot; (a network or host) using either an Ethernet interface, a TAP interface (Linux only) or even an UDP tunnel. &lt;span style=&quot; font-weight:600;&quot;&gt;Please be aware that Wifi interfaces may not work properly.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@@ -57,7 +57,7 @@
</property>
</widget>
</item>
<item row="0" column="4">
<item row="0" column="5">
<widget class="QPushButton" name="uiDeleteEthernetPushButton">
<property name="enabled">
<bool>false</bool>
@@ -67,7 +67,7 @@
</property>
</widget>
</item>
<item row="1" column="0" colspan="5">
<item row="1" column="0" colspan="6">
<widget class="QListWidget" name="uiEthernetListWidget">
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
@@ -82,9 +82,6 @@
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="dialog-warning"/>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
@@ -94,6 +91,13 @@
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="uiRefreshEthernetPushButton">
<property name="text">
<string>&amp;Refresh</string>
</property>
</widget>
</item>
</layout>
<zorder>uiEthernetListWidget</zorder>
<zorder>uiEthernetComboBox</zorder>
@@ -102,13 +106,14 @@
<zorder>uiAddAllEthernetPushButton</zorder>
<zorder>uiShowSpecialInterfacesCheckBox</zorder>
<zorder>uiEthernetWarningPushButton</zorder>
<zorder>uiRefreshEthernetPushButton</zorder>
</widget>
<widget class="QWidget" name="TAPTab">
<attribute name="title">
<string>TAP interfaces</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="4">
<item row="1" column="5">
<widget class="QPushButton" name="uiDeleteTAPPushButton">
<property name="enabled">
<bool>false</bool>
@@ -118,7 +123,7 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="5">
<item row="2" column="0" colspan="6">
<widget class="QListWidget" name="uiTAPListWidget">
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
@@ -145,7 +150,7 @@
</property>
</widget>
</item>
<item row="0" column="1" colspan="4">
<item row="0" column="1" colspan="5">
<widget class="QComboBox" name="uiTAPComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@@ -168,6 +173,13 @@
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QPushButton" name="uiRefreshTAPPushButton">
<property name="text">
<string>&amp;Refresh</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="UDPTab">
@@ -378,6 +390,19 @@
<string>Misc.</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="2">
<widget class="QSpinBox" name="uiConsolePortSpinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>23</number>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="uiNameLabel">
<property name="text">
@@ -388,24 +413,64 @@
<item row="0" column="2">
<widget class="QLineEdit" name="uiNameLineEdit"/>
</item>
<item row="1" column="0" colspan="2">
<item row="2" column="0">
<widget class="QLabel" name="uiConsoleHostLabel">
<property name="text">
<string>Console host:</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLineEdit" name="uiConsoleHostLineEdit"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labeluiConsolePortLabel">
<property name="text">
<string>Console port:</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="uiConsoleHttpPathLabel">
<property name="text">
<string>Console HTTP path:</string>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QLineEdit" name="uiConsoleHttpPathLineEdit"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="uiDefaultNameFormatLabel">
<property name="text">
<string>Default name format:</string>
</property>
</widget>
</item>
<item row="1" column="2">
<item row="6" column="2">
<widget class="QLineEdit" name="uiDefaultNameFormatLineEdit"/>
</item>
<item row="2" column="0" colspan="2">
<item row="7" column="0">
<widget class="QLabel" name="uiSymbolLabel">
<property name="text">
<string>Symbol:</string>
</property>
</widget>
</item>
<item row="2" column="2">
<item row="9" column="0" colspan="3">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>399</height>
</size>
</property>
</spacer>
</item>
<item row="7" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLineEdit" name="uiSymbolLineEdit"/>
@@ -422,30 +487,73 @@
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<item row="8" column="0">
<widget class="QLabel" name="uiCategoryLabel">
<property name="text">
<string>Category:</string>
</property>
</widget>
</item>
<item row="3" column="2">
<item row="8" column="2">
<widget class="QComboBox" name="uiCategoryComboBox"/>
</item>
<item row="4" column="1" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<item row="1" column="0">
<widget class="QLabel" name="uiConsoleTypeLabel">
<property name="text">
<string>Console type:</string>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>399</height>
</size>
</property>
</spacer>
</widget>
</item>
<item row="1" column="2">
<widget class="QComboBox" name="uiConsoleTypeComboBox">
<item>
<property name="text">
<string>telnet</string>
</property>
</item>
<item>
<property name="text">
<string>vnc</string>
</property>
</item>
<item>
<property name="text">
<string>spice</string>
</property>
</item>
<item>
<property name="text">
<string>http</string>
</property>
</item>
<item>
<property name="text">
<string>https</string>
</property>
</item>
<item>
<property name="text">
<string>none</string>
</property>
</item>
</widget>
</item>
</layout>
<zorder>uiNameLabel</zorder>
<zorder>uiNameLineEdit</zorder>
<zorder>uiDefaultNameFormatLabel</zorder>
<zorder>uiDefaultNameFormatLineEdit</zorder>
<zorder>uiSymbolLabel</zorder>
<zorder>uiCategoryLabel</zorder>
<zorder>uiCategoryComboBox</zorder>
<zorder>labeluiConsolePortLabel</zorder>
<zorder>uiConsoleHostLabel</zorder>
<zorder>uiConsoleHostLineEdit</zorder>
<zorder>uiConsolePortSpinBox</zorder>
<zorder>uiConsoleHttpPathLabel</zorder>
<zorder>uiConsoleHttpPathLineEdit</zorder>
<zorder>uiConsoleTypeLabel</zorder>
<zorder>uiConsoleTypeComboBox</zorder>
</widget>
</widget>
</item>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
#
# Created by: PyQt5 UI code generator 5.5.1
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!
@@ -11,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_cloudConfigPageWidget(object):
def setupUi(self, cloudConfigPageWidget):
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
cloudConfigPageWidget.resize(758, 299)
cloudConfigPageWidget.resize(979, 564)
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")
@@ -194,21 +199,44 @@ class Ui_cloudConfigPageWidget(object):
self.MiscTab.setObjectName("MiscTab")
self.gridLayout = QtWidgets.QGridLayout(self.MiscTab)
self.gridLayout.setObjectName("gridLayout")
self.uiConsolePortSpinBox = QtWidgets.QSpinBox(self.MiscTab)
self.uiConsolePortSpinBox.setMinimum(1)
self.uiConsolePortSpinBox.setMaximum(65535)
self.uiConsolePortSpinBox.setProperty("value", 23)
self.uiConsolePortSpinBox.setObjectName("uiConsolePortSpinBox")
self.gridLayout.addWidget(self.uiConsolePortSpinBox, 3, 2, 1, 1)
self.uiNameLabel = QtWidgets.QLabel(self.MiscTab)
self.uiNameLabel.setObjectName("uiNameLabel")
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
self.uiNameLineEdit = QtWidgets.QLineEdit(self.MiscTab)
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 2, 1, 1)
self.uiConsoleHostLabel = QtWidgets.QLabel(self.MiscTab)
self.uiConsoleHostLabel.setObjectName("uiConsoleHostLabel")
self.gridLayout.addWidget(self.uiConsoleHostLabel, 2, 0, 1, 1)
self.uiConsoleHostLineEdit = QtWidgets.QLineEdit(self.MiscTab)
self.uiConsoleHostLineEdit.setObjectName("uiConsoleHostLineEdit")
self.gridLayout.addWidget(self.uiConsoleHostLineEdit, 2, 2, 1, 1)
self.labeluiConsolePortLabel = QtWidgets.QLabel(self.MiscTab)
self.labeluiConsolePortLabel.setObjectName("labeluiConsolePortLabel")
self.gridLayout.addWidget(self.labeluiConsolePortLabel, 3, 0, 1, 1)
self.uiConsoleHttpPathLabel = QtWidgets.QLabel(self.MiscTab)
self.uiConsoleHttpPathLabel.setObjectName("uiConsoleHttpPathLabel")
self.gridLayout.addWidget(self.uiConsoleHttpPathLabel, 5, 0, 1, 1)
self.uiConsoleHttpPathLineEdit = QtWidgets.QLineEdit(self.MiscTab)
self.uiConsoleHttpPathLineEdit.setObjectName("uiConsoleHttpPathLineEdit")
self.gridLayout.addWidget(self.uiConsoleHttpPathLineEdit, 5, 2, 1, 1)
self.uiDefaultNameFormatLabel = QtWidgets.QLabel(self.MiscTab)
self.uiDefaultNameFormatLabel.setObjectName("uiDefaultNameFormatLabel")
self.gridLayout.addWidget(self.uiDefaultNameFormatLabel, 1, 0, 1, 2)
self.gridLayout.addWidget(self.uiDefaultNameFormatLabel, 6, 0, 1, 1)
self.uiDefaultNameFormatLineEdit = QtWidgets.QLineEdit(self.MiscTab)
self.uiDefaultNameFormatLineEdit.setObjectName("uiDefaultNameFormatLineEdit")
self.gridLayout.addWidget(self.uiDefaultNameFormatLineEdit, 1, 2, 1, 1)
self.gridLayout.addWidget(self.uiDefaultNameFormatLineEdit, 6, 2, 1, 1)
self.uiSymbolLabel = QtWidgets.QLabel(self.MiscTab)
self.uiSymbolLabel.setObjectName("uiSymbolLabel")
self.gridLayout.addWidget(self.uiSymbolLabel, 2, 0, 1, 2)
self.gridLayout.addWidget(self.uiSymbolLabel, 7, 0, 1, 1)
spacerItem2 = QtWidgets.QSpacerItem(20, 399, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem2, 9, 0, 1, 3)
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
self.uiSymbolLineEdit = QtWidgets.QLineEdit(self.MiscTab)
@@ -218,15 +246,40 @@ class Ui_cloudConfigPageWidget(object):
self.uiSymbolToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
self.uiSymbolToolButton.setObjectName("uiSymbolToolButton")
self.horizontalLayout_7.addWidget(self.uiSymbolToolButton)
self.gridLayout.addLayout(self.horizontalLayout_7, 2, 2, 1, 1)
self.gridLayout.addLayout(self.horizontalLayout_7, 7, 2, 1, 1)
self.uiCategoryLabel = QtWidgets.QLabel(self.MiscTab)
self.uiCategoryLabel.setObjectName("uiCategoryLabel")
self.gridLayout.addWidget(self.uiCategoryLabel, 3, 0, 1, 2)
self.gridLayout.addWidget(self.uiCategoryLabel, 8, 0, 1, 1)
self.uiCategoryComboBox = QtWidgets.QComboBox(self.MiscTab)
self.uiCategoryComboBox.setObjectName("uiCategoryComboBox")
self.gridLayout.addWidget(self.uiCategoryComboBox, 3, 2, 1, 1)
spacerItem2 = QtWidgets.QSpacerItem(20, 399, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem2, 4, 1, 1, 2)
self.gridLayout.addWidget(self.uiCategoryComboBox, 8, 2, 1, 1)
self.uiConsoleTypeLabel = QtWidgets.QLabel(self.MiscTab)
self.uiConsoleTypeLabel.setObjectName("uiConsoleTypeLabel")
self.gridLayout.addWidget(self.uiConsoleTypeLabel, 1, 0, 1, 1)
self.uiConsoleTypeComboBox = QtWidgets.QComboBox(self.MiscTab)
self.uiConsoleTypeComboBox.setObjectName("uiConsoleTypeComboBox")
self.uiConsoleTypeComboBox.addItem("")
self.uiConsoleTypeComboBox.addItem("")
self.uiConsoleTypeComboBox.addItem("")
self.uiConsoleTypeComboBox.addItem("")
self.uiConsoleTypeComboBox.addItem("")
self.uiConsoleTypeComboBox.addItem("")
self.gridLayout.addWidget(self.uiConsoleTypeComboBox, 1, 2, 1, 1)
self.uiNameLabel.raise_()
self.uiNameLineEdit.raise_()
self.uiDefaultNameFormatLabel.raise_()
self.uiDefaultNameFormatLineEdit.raise_()
self.uiSymbolLabel.raise_()
self.uiCategoryLabel.raise_()
self.uiCategoryComboBox.raise_()
self.labeluiConsolePortLabel.raise_()
self.uiConsoleHostLabel.raise_()
self.uiConsoleHostLineEdit.raise_()
self.uiConsolePortSpinBox.raise_()
self.uiConsoleHttpPathLabel.raise_()
self.uiConsoleHttpPathLineEdit.raise_()
self.uiConsoleTypeLabel.raise_()
self.uiConsoleTypeComboBox.raise_()
self.uiTabWidget.addTab(self.MiscTab, "")
self.verticalLayout.addWidget(self.uiTabWidget)
@@ -236,18 +289,20 @@ class Ui_cloudConfigPageWidget(object):
def retranslateUi(self, cloudConfigPageWidget):
_translate = QtCore.QCoreApplication.translate
cloudConfigPageWidget.setWindowTitle(_translate("cloudConfigPageWidget", "Cloud configuration"))
cloudConfigPageWidget.setWindowTitle(_translate("cloudConfigPageWidget", "Cloud template configuration"))
cloudConfigPageWidget.setWhatsThis(_translate("cloudConfigPageWidget", "<html><head/><body><p>A cloud node allows you to connect your project to the &quot;real world&quot; (a network or host) using either an Ethernet interface, a TAP interface (Linux only) or even an UDP tunnel. <span style=\" font-weight:600;\">Please be aware that Wifi interfaces may not work properly.</span></p></body></html>"))
self.uiAddEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiAddAllEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
self.uiDeleteEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiEthernetListWidget.setSortingEnabled(True)
self.uiShowSpecialInterfacesCheckBox.setText(_translate("cloudConfigPageWidget", "&Show special Ethernet interfaces"))
self.uiRefreshEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Refresh"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.EthernetTab), _translate("cloudConfigPageWidget", "Ethernet interfaces"))
self.uiDeleteTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiTAPListWidget.setSortingEnabled(True)
self.uiAddTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiAddAllTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
self.uiRefreshTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Refresh"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.TAPTab), _translate("cloudConfigPageWidget", "TAP interfaces"))
self.uiUDPTunnelSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "UDP tunnel settings"))
self.uiRemoteHostLineEdit.setText(_translate("cloudConfigPageWidget", "127.0.0.1"))
@@ -265,9 +320,19 @@ class Ui_cloudConfigPageWidget(object):
self.uiUDPTreeWidget.headerItem().setText(3, _translate("cloudConfigPageWidget", "Remote port"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.UDPTab), _translate("cloudConfigPageWidget", "UDP tunnels"))
self.uiNameLabel.setText(_translate("cloudConfigPageWidget", "Name:"))
self.uiConsoleHostLabel.setText(_translate("cloudConfigPageWidget", "Console host:"))
self.labeluiConsolePortLabel.setText(_translate("cloudConfigPageWidget", "Console port:"))
self.uiConsoleHttpPathLabel.setText(_translate("cloudConfigPageWidget", "Console HTTP path:"))
self.uiDefaultNameFormatLabel.setText(_translate("cloudConfigPageWidget", "Default name format:"))
self.uiSymbolLabel.setText(_translate("cloudConfigPageWidget", "Symbol:"))
self.uiSymbolToolButton.setText(_translate("cloudConfigPageWidget", "&Browse..."))
self.uiCategoryLabel.setText(_translate("cloudConfigPageWidget", "Category:"))
self.uiConsoleTypeLabel.setText(_translate("cloudConfigPageWidget", "Console type:"))
self.uiConsoleTypeComboBox.setItemText(0, _translate("cloudConfigPageWidget", "telnet"))
self.uiConsoleTypeComboBox.setItemText(1, _translate("cloudConfigPageWidget", "vnc"))
self.uiConsoleTypeComboBox.setItemText(2, _translate("cloudConfigPageWidget", "spice"))
self.uiConsoleTypeComboBox.setItemText(3, _translate("cloudConfigPageWidget", "http"))
self.uiConsoleTypeComboBox.setItemText(4, _translate("cloudConfigPageWidget", "https"))
self.uiConsoleTypeComboBox.setItemText(5, _translate("cloudConfigPageWidget", "none"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.MiscTab), _translate("cloudConfigPageWidget", "Misc."))

View File

@@ -2,8 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_preferences_page.ui'
#
# Created: Fri Jun 10 14:39:19 2016
# by: PyQt5 UI code generator 5.2.1
# Created by: PyQt5 UI code generator 5.9
#
# WARNING! All changes made in this file will be lost!

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