Compare commits

...

220 Commits

Author SHA1 Message Date
grossmj
bb89fe2275 Release v2.2.3 2019-11-12 15:29:54 +08:00
grossmj
7eb2a923b2 Fix issue when binding on 0.0.0.0. Fixes #2892 2019-11-11 17:49:09 +08:00
grossmj
58052e3cce Allow double click on cloud with configured console to open session. Fixes #2894 2019-11-11 14:31:35 +08:00
grossmj
e431104f6b Officially support Python 3.8. Ref https://github.com/GNS3/gns3-gui/issues/2895 2019-11-11 12:56:11 +08:00
grossmj
a4e9d6b8ce Set psutil to version 5.6.3 in requirements.txt 2019-11-08 10:44:17 +08:00
grossmj
f58f5c7b95 Developement version on 2.2.3dev1 2019-11-04 19:45:18 +08:00
grossmj
37faa39309 Release v2.2.2 2019-11-04 18:33:29 +08:00
grossmj
4d64598ed2 Fix KeyError: 'spice+agent'. Fixes #2890 2019-11-03 15:19:18 +08:00
grossmj
5132c4e172 Fix wrong log.error() call when exporting file. 2019-11-02 23:19:04 +08:00
grossmj
3c3fdd9ffd Revert "Explicitly cleanup the cache directory."
This reverts commit 8095fef228.
2019-11-02 15:47:24 +08:00
grossmj
efa50571c6 Fix "UnboundLocalError: local variable 'pywintypes' referenced before assignment" 2019-11-02 15:45:16 +08:00
grossmj
ca9b10fcca Fix version string. 2019-11-02 15:27:28 +08:00
grossmj
8660161b10 Fix GUI uses only telnet console. Fixes #2885 2019-11-02 02:23:13 +08:00
grossmj
50ebfb9c06 Fix missing sys module in sudo.py Fixes #2886 2019-11-02 02:06:33 +08:00
grossmj
26df59d6b6 Development on 2.2.2dev1 2019-11-01 18:43:36 +08:00
grossmj
b903e2ad73 Release v2.2.1 2019-11-01 17:53:20 +08:00
grossmj
b2fe7eb643 Check if console_type is None. 2019-11-01 17:47:26 +08:00
grossmj
8095fef228 Explicitly cleanup the cache directory. 2019-11-01 17:46:53 +08:00
grossmj
b8da5440f5 Get Windows interface from registry if cannot load win32com module. 2019-11-01 17:44:47 +08:00
grossmj
6cea094e4e Ignore OSError returned by psutil when bringing console to front. 2019-11-01 17:44:06 +08:00
grossmj
9ac46c9d50 Catch error if NPF or NPCAP service cannot be detected. Ref https://github.com/GNS3/gns3-server/issues/1670 2019-10-30 17:59:19 +08:00
grossmj
6dc44d5108 Better handling for reading synchronous JSON response from server. Ref #2874 2019-10-17 12:45:40 +08:00
grossmj
9c6be0341b Fix tests. 2019-10-17 12:34:34 +08:00
grossmj
011a49e998 Fix JSONDecodeError when getting server version. Fixes #2874 2019-10-17 12:31:15 +08:00
grossmj
e18c2df5f5 Fix FileNotFoundError exceptions when launching SPICE or VNC clients. 2019-10-17 12:19:55 +08:00
grossmj
1794b8389f Fix UnboundLocalError local variable 'win32serviceutil' referenced before assignment 2019-10-17 12:12:46 +08:00
grossmj
0379c370eb Merge remote-tracking branch 'remotes/origin/master' into 2.2 2019-10-15 23:36:49 +08:00
Jeremy Grossmann
e03550a89b Merge pull request #2872 from fatoms/Tab_Order
Tab Order in Preferences and Project Dialog
2019-10-15 22:35:53 +07:00
Dominic
c6ea775e81 'Fix' tab order in preferences dialog so it follows the layout 2019-10-12 22:06:30 +02:00
Dominic
38233ba5e9 'Fix' tab order in edit project dialog so it follows the layout 2019-10-12 22:05:52 +02:00
grossmj
89c1272bc1 Use compatible shlex_quote to handle case where Windows needs double quotes around file names, not single quotes. Ref https://github.com/GNS3/gns3-gui/issues/2866 2019-10-09 17:02:30 +08:00
grossmj
8bbb46c599 Use 0.0.0.0 by default for server host. Fixes https://github.com/GNS3/gns3-server/issues/1663 2019-10-09 16:35:42 +08:00
grossmj
74fca3d736 Set default host to "localhost". Fixes https://github.com/GNS3/gns3-server/issues/1663 2019-10-08 18:28:11 +08:00
grossmj
7aeed7aa59 Catch IndexError when configuring port names. Fixes #2865 2019-10-08 16:15:36 +08:00
grossmj
aa15ace887 Bump version to 2.2.1dev1 2019-10-08 16:05:37 +08:00
grossmj
2d0a7b5f58 Release v2.2.0 2019-09-30 16:24:26 +08:00
grossmj
20a09b56c1 Merge branch 'master' into 2.2
# Conflicts:
#	gns3/version.py
2019-09-24 14:07:38 +08:00
grossmj
1938cdabae Bump version to 2.2.0dev18 2019-09-24 14:02:36 +08:00
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
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
143 changed files with 3107 additions and 2155 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.

193
CHANGELOG
View File

@@ -1,5 +1,182 @@
# Change Log
## 2.2.3 12/11/2019
* Fix issue when binding on 0.0.0.0. Fixes #2892
* Allow double click on cloud with configured console to open session. Fixes #2894
* Officially support Python 3.8. Ref https://github.com/GNS3/gns3-gui/issues/2895
* Set psutil to version 5.6.3 in requirements.txt
## 2.2.2 04/11/2019
* Fix KeyError: 'spice+agent'. Fixes #2890
* Fix wrong log.error() call when exporting file.
* Revert "Explicitly cleanup the cache directory."
* Fix "UnboundLocalError: local variable 'pywintypes' referenced before assignment"
* Fix GUI uses only telnet console. Fixes #2885
* Fix missing sys module in sudo.py Fixes #2886
## 2.2.1 01/11/2019
* Check if console_type is None.
* Explicitly cleanup the cache directory.
* Get Windows interface from registry if cannot load win32com module.
* Ignore OSError returned by psutil when bringing console to front.
* Catch error if NPF or NPCAP service cannot be detected. Ref https://github.com/GNS3/gns3-server/issues/1670
* Better handling for reading synchronous JSON response from server. Ref #2874
* Fix JSONDecodeError when getting server version. Fixes #2874
* Fix FileNotFoundError exceptions when launching SPICE or VNC clients.
* Fix UnboundLocalError local variable 'win32serviceutil' referenced before assignment
* 'Fix' tab order in preferences dialog so it follows the layout
* 'Fix' tab order in edit project dialog so it follows the layout
* Use compatible shlex_quote to handle case where Windows needs double quotes around file names, not single quotes. Ref https://github.com/GNS3/gns3-gui/issues/2866
* Use 0.0.0.0 by default for server host. Fixes https://github.com/GNS3/gns3-server/issues/1663
* Catch IndexError when configuring port names. Fixes #2865
## 2.2.0 30/09/2019
* No changes
## 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
@@ -99,6 +276,22 @@
* 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.

View File

@@ -1,5 +1,5 @@
# Run tests inside a container
FROM ubuntu:17.10
FROM ubuntu:18.04
MAINTAINER GNS3 Team

View File

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

View File

@@ -17,6 +17,9 @@
from .qt import QtCore
from .controller import Controller
from .local_config import LocalConfig
from .settings import GENERAL_SETTINGS
import logging
log = logging.getLogger(__name__)
@@ -43,10 +46,12 @@ class ApplianceManager(QtCore.QObject):
"""
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", self._listAppliancesCallback, progressText="Downloading appliances from online registry...")
self._controller.get("/appliances?update=yes&symbol_theme={}".format(symbol_theme), self._listAppliancesCallback, progressText="Downloading appliances from online registry...")
else:
self._controller.get("/appliances", self._listAppliancesCallback)
self._controller.get("/appliances?symbol_theme={}".format(symbol_theme), self._listAppliancesCallback)
def _controllerDisconnectedSlot(self):
"""

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ import os
import hashlib
import tempfile
import json
import pathlib
from .qt import QtCore, QtNetwork, QtGui, QtWidgets, QtWebSockets, qpartial, qslot
from .symbol import Symbol
@@ -45,15 +46,15 @@ class Controller(QtCore.QObject):
super().__init__()
self._connected = False
self._connecting = False
self._notification_stream = None
self._version = None
self._cache_directory = tempfile.mkdtemp()
self._cache_directory = tempfile.TemporaryDirectory(suffix="-gns3")
self._http_client = None
self._first_error = True
self._error_dialog = None
self._display_error = True
self._projects = []
self._controller_websocket = QtWebSockets.QWebSocket()
self._project_websocket = QtWebSockets.QWebSocket()
self._websocket = QtWebSockets.QWebSocket()
# If we do multiple call in order to download the same symbol we queue them
self._static_asset_download_queue = {}
@@ -245,12 +246,6 @@ class Controller(QtCore.QObject):
if self._http_client:
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
def getSynchronous(self, endpoint, timeout=2):
return self._http_client.getSynchronous(endpoint, timeout)
def connectProjectWebSocket(self, path, *args):
return self._http_client.connectWebSocket(self._project_websocket, path)
@staticmethod
def instance():
"""
@@ -274,7 +269,6 @@ class Controller(QtCore.QObject):
if not self._http_client:
return
path = self.getStaticCachedPath(url)
if os.path.exists(path):
@@ -313,8 +307,8 @@ class Controller(QtCore.QObject):
def getStaticCachedPath(self, url):
"""
Returns static cached (hashed) path
:param url:
:return:
"""
m = hashlib.md5()
m.update(url.encode())
@@ -322,9 +316,22 @@ class Controller(QtCore.QObject):
extension = ".svg"
else:
extension = ".png"
path = os.path.join(self._cache_directory, m.hexdigest() + extension)
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
@@ -341,10 +348,31 @@ class Controller(QtCore.QObject):
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)
@@ -392,7 +420,7 @@ class Controller(QtCore.QObject):
ignoreErrors=True)
else:
self._notification_stream = self._http_client.connectWebSocket(self._controller_websocket, "/notifications/ws")
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)
@@ -415,7 +443,7 @@ class Controller(QtCore.QObject):
@qslot
def _websocket_error(self, error):
if self._notification_stream:
log.error(self._notification_stream.errorString())
log.error("Websocket notification stream error: {}".format(self._notification_stream.errorString()))
self._notification_stream = None
self._startListenNotifications()

View File

@@ -52,7 +52,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "https://aefc0ee91527434c838dd8bac4abf623:f917ff7284594db68b880e64ae794ebf@sentry.io/38506"
DSN = "https://9090482e1e444838b80e5aa94596b9d8:65ab81b67c534e349550ae3766241cda@sentry.io/38506"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):
@@ -133,7 +133,7 @@ class CrashReport:
def _add_qt_information(self, context):
try:
from .qt import QtCore
import sip
from .qt import sip
except ImportError:
return context
context["psutil:version"] = psutil.__version__

View File

@@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sip
from ..qt import sip
import shutil
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
@@ -36,6 +36,7 @@ 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__)
@@ -76,6 +77,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
# 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):
@@ -190,7 +197,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
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':
@@ -221,8 +231,15 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
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):
"""
@@ -339,6 +356,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
else:
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)
@@ -398,9 +416,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
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
@@ -495,9 +518,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
return
image_upload_manger = ImageUploadManager(
image, Controller.instance(), self._compute_id,
self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger = ImageUploadManager(image, Controller.instance(), self._compute_id, self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger.upload()
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
@@ -533,7 +554,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if version is None:
appliance_configuration = self._appliance.copy()
if not "docker" in appliance_configuration:
if "docker" not in appliance_configuration:
# only Docker do not have version
return False
else:
@@ -554,7 +575,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if "qemu" in appliance_configuration:
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, self._symbols)
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
@@ -583,24 +604,35 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self._template_created = True
self.done(True)
def _uploadImages(self, version):
def _uploadImages(self, name, version):
"""
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_manger = ImageUploadManager(
image, Controller.instance(), self._compute_id,
self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger.upload()
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:
@@ -629,14 +661,17 @@ 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 the '{}' appliance (version {}) with missing files".format(appliance["name"], version["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:
# validate the usage page

View File

@@ -44,6 +44,9 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
"""
super().__init__(parent)
self.setupUi(self)
if console_type == "spice+agent":
# special case for spice+agent, use the spice console type
console_type = "spice"
self._console_type = console_type
self._current = current
@@ -63,7 +66,7 @@ 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"):
elif self._console_type == "spice":
self._consoles = copy.copy(PRECONFIGURED_SPICE_CONSOLE_COMMANDS)
self._consoles.update(self._settings[self._console_type])
@@ -121,8 +124,8 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
dialog = ConsoleCommandDialog(parent, console_type=console_type, current=current)
dialog.show()
if dialog.exec_():
return (True, dialog.uiCommandPlainTextEdit.toPlainText().replace("\n", " "))
return (False, None)
return True, dialog.uiCommandPlainTextEdit.toPlainText().replace("\n", " ")
return False, None
if __name__ == '__main__':

View File

@@ -18,6 +18,9 @@
from ..qt import QtGui, QtWidgets, qslot
from ..ui.filter_dialog_ui import Ui_FilterDialog
import logging
log = logging.getLogger(__name__)
class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
@@ -41,7 +44,7 @@ class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
def _listAvailableFiltersCallback(self, result, error=False, *args, **kwargs):
if error:
QtWidgets.QMessageBox.warning(None, "Link", "Error while listing information about the link: {}".format(result["message"]))
log.warning("Error while listing information about the link: {}".format(result["message"]))
return
self._filters = result
self._initialized = True

View File

@@ -62,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.
"""
@@ -78,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):
"""
@@ -89,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

@@ -19,6 +19,7 @@ import sys
import tempfile
import json
import sip
import os
from gns3.qt import QtCore, QtWidgets, qpartial
from gns3.controller import Controller
@@ -45,9 +46,6 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
# we want to see the cancel button on OSX
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
self.uiCreateTemplateManuallyRadioButton.toggled.connect(self._addTemplateToggledSlot)
self.uiImportApplianceFromFileRadioButton.toggled.connect(self._addTemplateToggledSlot)
# add a custom button to show appliance information
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Update from online registry")
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
@@ -56,21 +54,13 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
self.uiFilterLineEdit.textChanged.connect(self._filterTextChangedSlot)
ApplianceManager.instance().appliances_changed_signal.connect(self._appliancesChangedSlot)
def _addTemplateToggledSlot(self, checked):
if checked:
self.button(QtWidgets.QWizard.FinishButton).setEnabled(True)
self.button(QtWidgets.QWizard.NextButton).setEnabled(False)
else:
self.button(QtWidgets.QWizard.FinishButton).setEnabled(False)
self.button(QtWidgets.QWizard.NextButton).setEnabled(True)
def _downloadAppliancesSlot(self):
"""
Request server to update appliances from online registry.
"""
ApplianceManager.instance().refresh(update=True)
Controller.instance().clearStaticCache()
def _appliancesChangedSlot(self):
"""
@@ -162,33 +152,54 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
"""
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
item = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
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("_", " "))
#item.setText(1, appliance["category"].capitalize().replace("_", " "))
if "qemu" in appliance:
item.setText(2, "Qemu")
item.setText(1, "Qemu")
elif "iou" in appliance:
item.setText(2, "IOU")
item.setText(1, "IOU")
elif "dynamips" in appliance:
item.setText(2, "Dynamips")
item.setText(1, "Dynamips")
elif "docker" in appliance:
item.setText(2, "Docker")
item.setText(1, "Docker")
else:
item.setText(2, "N/A")
item.setText(1, "N/A")
item.setText(3, appliance["vendor_name"])
item.setText(2, appliance["vendor_name"])
item.setData(0, QtCore.Qt.UserRole, appliance)
#item.setSizeHint(0, QtCore.QSize(32, 32))
@@ -198,6 +209,8 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
self.uiAppliancesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiAppliancesTreeWidget.resizeColumnToContents(0)
if not appliance_filter:
self.uiAppliancesTreeWidget.collapseAll()
def initializePage(self, page_id):
"""
@@ -237,24 +250,39 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
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.
:param result: ignored
"""
super().done(result)
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)
elif self.uiCreateTemplateManuallyRadioButton.isChecked():
MainWindow.instance().preferencesActionSlot()
elif self.uiImportApplianceFromFileRadioButton.isChecked():
if result:
#ApplianceManager.instance().appliances_changed_signal.disconnect(self._appliancesChangedSlot)
from gns3.main_window import MainWindow
MainWindow.instance().openApplianceActionSlot()
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

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

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

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

@@ -43,6 +43,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
super().__init__(parent)
self.setupUi(self)
self.adjustSize()
self._gns3_vm_settings = {
"enable": True,
@@ -83,10 +84,15 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
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
# 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)
self.uiLocalServerHostComboBox.addItem("localhost", "localhost") # local host
self.uiLocalServerHostComboBox.addItem("::", "::") # all IPv6 addresses
self.uiLocalServerHostComboBox.addItem("0.0.0.0", "0.0.0.0") # all IPv4 addresses
if sys.platform.startswith("darwin"):
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.png"))
else:
@@ -141,7 +147,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)
@@ -414,11 +419,8 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
settings["hide_setup_wizard"] = True
else:
local_server_settings = LocalServer.instance().localServerSettings()
if local_server_settings["host"] is None:
local_server_settings["host"] = DEFAULT_LOCAL_SERVER_HOST
LocalServer.instance().updateLocalServerSettings(local_server_settings)
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
LocalServer.instance().updateLocalServerSettings(local_server_settings)
settings["hide_setup_wizard"] = not self.uiShowCheckBox.isChecked()
self.parentWidget().setSettings(settings)
super().done(result)

View File

@@ -25,7 +25,6 @@ import pathlib
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,17 +77,24 @@ 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):
@@ -99,14 +105,11 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
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()
@@ -116,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)
@@ -158,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

View File

@@ -21,7 +21,7 @@ Graphical view on the scene where items are drawn.
import logging
import os
import sip
from .qt import sip
import sys
from .qt import QtCore, QtGui, QtNetwork, QtWidgets, qpartial, qslot
@@ -49,7 +49,7 @@ 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
@@ -466,13 +466,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
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
@@ -632,7 +627,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
if not self._adding_link:
if isinstance(item, NodeItem) and item.node().initialized():
item.setSelected(True)
if item.node().status() == Node.stopped or item.node().isAlwaysOn():
if item.node().status() == Node.stopped or item.node().consoleType() == "none":
self.configureSlot()
return
else:
@@ -1059,8 +1054,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
# 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.")
QtWidgets.QMessageBox.warning(self, "TightVNC", "TightVNC (vncviewer) may not start because of lack of IPv6 support.")
try:
node.openConsole(aux=aux)
@@ -1121,24 +1115,20 @@ class GraphicsView(QtWidgets.QGraphicsView):
Allow user to use a custom console for this VM
"""
current_cmd = None
console_type = "telnet"
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and item.node().console() is not None 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():
if item.node().consoleType() not in ("telnet", "serial", "vnc", "spice", "spice+agent"):
continue
current_cmd = item.node().consoleCommand()
console_type = item.node().consoleType()
(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 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", "spice", "spice+agent"):
continue
(ok, cmd) = ConsoleCommandDialog.getCommand(self, console_type=console_type, current=current_cmd)
if ok:
try:
node.openConsole(command=cmd)
if item.node().status() != Node.started:
QtWidgets.QMessageBox.warning(self, "Console", "This node must be started before a console can be opened")
continue
item.node().openConsole(command=cmd)
except (OSError, ValueError) as e:
QtWidgets.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
@@ -1204,12 +1194,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
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)")
if not path:
continue
self._import_config_dir = os.path.dirname(path)
self._import_config_directory = os.path.dirname(path)
item.node().importFile(config_file, path)
def editConfigActionSlot(self):
@@ -1351,9 +1341,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):
"""
@@ -1461,11 +1451,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
for item in self.scene().selectedItems():
if item.parentItem() is None:
current_zvalue = item.zValue()
if not (item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable):
log.error("Cannot move object to a upper layer because it is locked")
continue
item.setZValue(current_zvalue + 1)
item.setZValue(item.zValue() + 1)
item.updateNode()
item.update()
@@ -1477,17 +1466,13 @@ class GraphicsView(QtWidgets.QGraphicsView):
for item in self.scene().selectedItems():
if item.parentItem() is None:
current_zvalue = item.zValue()
if not (item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable):
log.error("Cannot move object to a lower layer because it is locked")
continue
item.setZValue(current_zvalue - 1)
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
@@ -1495,8 +1480,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
for item in self.scene().selectedItems():
if not isinstance(item, LinkItem):
item.setZValue(-(item.zValue() + 1))
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()
@@ -1510,7 +1498,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))
@@ -1557,9 +1549,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
pos = self.mapToScene(pos)
return TemplateManager().instance().createNodeFromTemplateId(self._topology.project(), template_id, pos.x(), pos.y())
def createNodeItem(self, node, symbol, x, y, z):
def createNodeItem(self, node, symbol, x, y):
node.setSymbol(symbol)
node.setPos(x, y, z)
node.setPos(x, y)
node_item = NodeItem(node)
self.scene().addItem(node_item)
@@ -1582,18 +1574,18 @@ class GraphicsView(QtWidgets.QGraphicsView):
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, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
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()
@@ -1605,10 +1597,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
def drawBackground(self, painter, rect):
super().drawBackground(painter, rect)
if self._main_window.uiShowGridAction.isChecked():
grids = [(self.drawingGridSize(),QtGui.QColor(208, 208, 208)),
(self.nodeGridSize(),QtGui.QColor(190, 190, 190))]
grids = [(self.drawingGridSize(), QtGui.QColor(208, 208, 208)),
(self.nodeGridSize(), QtGui.QColor(190, 190, 190))]
painter.save()
for (grid,colour) in grids:
for (grid, colour) in grids:
if not grid:
continue
painter.setPen(QtGui.QPen(colour))
left = int(rect.left()) - (int(rect.left()) % grid)

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
@@ -207,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, str(sent), str(total))
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
def _notify_progress_download(self, query_id, sent, total):
"""
Called when a query download progress
"""
if not sip_is_deleted(HTTPClient._progress_callback):
# abs() for maxium because sometimes the system send negative
# abs() for maximum because sometimes the system send negative
# values
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(abs(total)))
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
@classmethod
def setProgressCallback(cls, progress_callback):
@@ -301,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,
@@ -469,7 +470,9 @@ class HTTPClient(QtCore.QObject):
"""
host = self._getHostForQuery()
request = websocket.request()
request.setUrl(QtCore.QUrl("ws://{host}:{port}{prefix}{path}".format(host=host, port=self._port, path=path, prefix=prefix)))
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
@@ -489,7 +492,7 @@ class HTTPClient(QtCore.QObject):
def _paramsToQueryString(self, params):
"""
:param params: Dictionnary of query string parameters
:param params: Dictionary of query string parameters
:returns: String of the query string
"""
if params == {}:
@@ -617,7 +620,7 @@ class HTTPClient(QtCore.QObject):
# We check if we received HTTP headers
if not sip.isdeleted(response) and response.isRunning() and not len(response.rawHeaderList()) > 0:
if not response.error() != QtNetwork.QNetworkReply.NoError:
log.warning("Timeout after {} seconds for request {}".format(timeout, response.url().toString()))
log.warning("Timeout after {} seconds for request {}. Please check the connection is not blocked by a firewall or an anti-virus.".format(timeout, response.url().toString()))
response.abort()
def disconnect(self):
@@ -637,7 +640,7 @@ class HTTPClient(QtCore.QObject):
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)
@@ -724,47 +727,54 @@ class HTTPClient(QtCore.QObject):
e = HttpBadRequest(body)
raise e
def getSynchronous(self, endpoint, timeout=2):
def getSynchronous(self, method, endpoint, prefix="/v2", timeout=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:
if content_type == "application/json":
content = response.read()
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()))
else:
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
if status == 200 and content_type == "application/json":
content = bytes(response.readAll())
try:
json_data = json.loads(content.decode("utf-8"))
return response.status, json_data
else:
return response.status, None
except http.client.InvalidURL as e:
log.warning("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 0, None
except (UnicodeEncodeError, ValueError) as e:
log.warning("Could not read JSON data returned from {}: {}".format(url, e))
else:
return status, json_data
return status, None
@classmethod
def fromUrl(cls, url, network_manager=None, base_settings=None):

View File

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

@@ -15,6 +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 os
import pathlib
import urllib.parse
@@ -37,6 +38,9 @@ class ImageUploadManager(object):
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)
@@ -71,19 +75,16 @@ class ImageUploadManager(object):
self._callback(result, error, **kwargs)
def _fileUploadToCompute(self, endpoint):
log.info("Uploading file to compute: {}".format(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),
progressText="Uploading {}".format(self._image.filename), timeout=None, prefix="")
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.info("Uploading file to controller: {}".format(self._getComputePath()))
self._controller.postCompute(
self._getComputePath(), self._compute_id, self._callback, body=pathlib.Path(self._image.path),
progressText="Uploading {}".format(self._image.filename), timeout=None)
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

@@ -41,9 +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)
@@ -65,6 +66,8 @@ class DrawingItem:
if rotation:
self.setRotation(rotation)
self.setLocked(locked)
def drawing_id(self):
return self._id
@@ -106,6 +109,7 @@ class DrawingItem:
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"])
@@ -149,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()
@@ -158,23 +163,29 @@ 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:
if locked is True:
self.setFlag(self.ItemIsMovable, False)
else:
self.setFlag(self.ItemIsMovable, True)
self._locked = locked
def deleting(self):
"""
Is the link being deleted
Is the drawing being deleted
"""
return self._deleting

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

View File

@@ -106,7 +106,7 @@ class EthernetLinkItem(LinkItem):
"""
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
if not self._adding_flag and self._settings["draw_link_status_points"]:
if not self._adding_flag:
# points disappears if nodes are too close to each others.
if self.length < 100:
@@ -147,11 +147,13 @@ class EthernetLinkItem(LinkItem):
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._link.suspended() or self._destination_port.status() == Port.suspended:
# link or port is suspended
@@ -188,10 +190,12 @@ class EthernetLinkItem(LinkItem):
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._drawSymbol()

View File

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

View File

@@ -170,19 +170,6 @@ class LabelItem(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.ItemIsMovable, False)
else:
self.setFlag(self.ItemIsMovable, True)
def setStyle(self, new_style):
"""
Set text style using a SVG style

View File

@@ -63,13 +63,6 @@ class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def setZValue(self, value):
"""
Sets Z value of the item
:param value: z layer
"""
return DrawingItem.setZValue(self, value)
def toSvg(self):
"""
Return an SVG version of the shape
@@ -126,8 +119,8 @@ class LineItem(QtWidgets.QGraphicsLineItem, 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 self._isHorizontalLine():
if event.pos().x() > (self.line().x2() - self._border):
@@ -218,6 +211,6 @@ class LineItem(QtWidgets.QGraphicsLineItem, 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)

View File

@@ -36,7 +36,8 @@ class SvgIconItem(QtSvg.QGraphicsSvgItem):
def mousePressEvent(self, event):
self.parentItem().mousePressEvent(event)
if self.parentItem():
self.parentItem().mousePressEvent(event)
event.accept()

View File

@@ -19,7 +19,7 @@
Graphical representation of a node on the QGraphicsScene.
"""
import sip
from ..qt import sip
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
from ..qt.qimage_svg_renderer import QImageSvgRenderer
@@ -50,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.
@@ -78,7 +79,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)
# update z value and set proper flags - not movable in case z < 0
# update z value and locked state
self.setLocked(self._node.locked())
self.setZValue(self._node.z())
# connect signals to know about some events
@@ -101,22 +103,11 @@ 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):
grid_size = self._main_window.uiGraphicsView.nodeGridSize()
mid_x = self.boundingRect().width() / 2
x = (grid_size * round((self.x() + mid_x) / grid_size)) - mid_x
mid_y = self.boundingRect().height() / 2
y = (grid_size * round((self.y() + mid_y) / grid_size)) - mid_y
self.setPos(x, y)
def updateNode(self):
"""
Sync change to the node
@@ -272,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
@@ -388,7 +379,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self._node_label.setStyle(style)
self._node_label.setRotation(label_data.get("rotation", 0))
if self._node.z() < 0:
if self._node.locked():
self._node_label.setFlag(self.ItemIsMovable, False)
if label_data["x"] is None:
@@ -531,7 +522,21 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
"""
super().setZValue(value)
if self.zValue() < 0:
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.ItemIsMovable, False)
@@ -541,6 +546,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self._node_label.setFlag(self.ItemIsMovable, True)
for link in self._links:
link.adjust()
self._locked = locked
def hoverEnterEvent(self, event):
"""

View File

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

View File

@@ -107,7 +107,7 @@ class SerialLinkItem(LinkItem):
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
if not self._adding_flag and self._settings["draw_link_status_points"]:
if not self._adding_flag:
# points disappears if nodes are too close to each others.
if self.length < 80:
@@ -136,11 +136,13 @@ class SerialLinkItem(LinkItem):
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._link.suspended() or self._destination_port.status() == Port.suspended:
@@ -166,10 +168,12 @@ class SerialLinkItem(LinkItem):
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._drawSymbol()

View File

@@ -147,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):
@@ -167,8 +167,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():
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
def fromSvg(self, svg):

View File

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

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
@@ -79,6 +79,7 @@ class Link(QtCore.QObject):
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
@@ -103,24 +104,28 @@ 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
self._capturing = result.get("capturing", False)
if self._capturing:
if Controller.instance().isRemote():
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
self._capture_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()
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"]
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"]
@@ -169,7 +174,7 @@ class Link(QtCore.QObject):
def updateLinkCallback(self, result, error=False, *args, **kwargs):
if error:
QtWidgets.QMessageBox.warning(None, "Update link", "Error while updating link: {}".format(result["message"]))
log.warning("Error while updating link: {}".format(result["message"]))
return
self._parseResponse(result)
@@ -219,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
@@ -353,7 +358,7 @@ class Link(QtCore.QObject):
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):
"""
@@ -366,15 +371,16 @@ class Link(QtCore.QObject):
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 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))
# 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),
@@ -384,7 +390,7 @@ class Link(QtCore.QObject):
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):
"""

View File

@@ -24,8 +24,10 @@ import copy
import psutil
from .qt import QtCore, QtWidgets
from .version import __version__
from .version import __version__, __version_info__
from .utils import parse_version
from .local_server_config import LocalServerConfig
from .settings import LOCAL_SERVER_SETTINGS
import logging
log = logging.getLogger(__name__)
@@ -87,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))
@@ -114,17 +133,18 @@ class LocalConfig(QtCore.QObject):
self._config_file = None
self._resetLoadConfig()
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)
@@ -146,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)
@@ -156,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
@@ -164,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. If you want to reset delete the settings in {}".format(self._settings["version"], self.configDirectory()))
app = QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
error_message = "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
@@ -198,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
@@ -209,7 +233,7 @@ 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:

View File

@@ -31,12 +31,11 @@ import subprocess
from gns3.qt import QtWidgets, QtCore, qslot
from gns3.settings import LOCAL_SERVER_SETTINGS
from gns3.settings import LOCAL_SERVER_SETTINGS, DEFAULT_LOCAL_SERVER_HOST
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
@@ -131,6 +130,7 @@ class LocalServer(QtCore.QObject):
import win32serviceutil
except ImportError as e:
log.error("Could not check if the {} service is running: {}".format(service_name, e))
return
try:
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
@@ -246,6 +246,9 @@ class LocalServer(QtCore.QObject):
"""
Update the local server settings. Keep the key not in new_settings
"""
if "host" in new_settings and new_settings["host"] is None:
new_settings["host"] = DEFAULT_LOCAL_SERVER_HOST
old_settings = copy.copy(self._settings)
if not self._settings:
self._settings = new_settings
@@ -369,13 +372,15 @@ class LocalServer(QtCore.QObject):
self._checkUbridgePermissions()
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
if sys.platform.startswith("win"):
import pywintypes
try:
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
log.warning("The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
except pywintypes.error as e:
log.warning("Could not check if the NPF or Npcap service is running: {}".format(e.strerror))
self._port = self._settings["port"]
# check the local server path
local_server_path = self.localServerPath()
if not local_server_path:
@@ -516,9 +521,7 @@ class LocalServer(QtCore.QObject):
:returns: boolean
"""
status, json_data = getSynchronous(self._settings["protocol"], self._settings["host"], self._port, "version",
timeout=2, user=self._settings["user"], password=self._settings["password"])
status, json_data = HTTPClient(self._settings).getSynchronous("GET", "/version")
if status == 401: # Auth issue that need to be solved later
return True
elif json_data is None:

View File

@@ -137,10 +137,7 @@ 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,
@@ -268,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
@@ -292,7 +292,6 @@ def main():
mainwindow.show()
exit_code = app.exec_()
signal.signal(signal.SIGINT, orig_sigint)
signal.signal(signal.SIGTERM, orig_sigterm)
@@ -301,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,8 +42,9 @@ 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
@@ -189,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)
@@ -258,7 +260,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiDrawRectangleAction.triggered.connect(self._drawRectangleActionSlot)
self.uiDrawEllipseAction.triggered.connect(self._drawEllipseActionSlot)
self.uiDrawLineAction.triggered.connect(self._drawLineActionSlot)
self.uiEditReadmeAction.triggered.connect(self._editReadmeActionSlot)
# help menu connections
self.uiOnlineHelpAction.triggered.connect(self._onlineHelpActionSlot)
@@ -328,7 +329,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def _openWebInterfaceActionSlot(self):
if Controller.instance().connected():
base_url = Controller.instance().httpClient().fullUrl()
webui_url = "{}/static/web-ui/local".format(base_url)
webui_url = "{}/static/web-ui/bundled".format(base_url)
QtGui.QDesktopServices.openUrl(QtCore.QUrl(webui_url))
def _showGridActionSlot(self):
@@ -362,15 +363,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
:return: None
"""
for item in self.uiGraphicsView.items():
if not isinstance(item, LinkItem):
if self.uiLockAllAction.isChecked() and item.zValue() >= 0:
item.setZValue(-(item.zValue() + 1))
if item.parentItem() is None:
item.updateNode()
item.update()
elif not self.uiLockAllAction.isChecked() and item.zValue() < 0:
item.setZValue(-(item.zValue() + 1))
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()
@@ -394,9 +393,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
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())
@@ -438,7 +434,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._newProjectActionSlot()
else:
directory = self._project_dir
if self._project_dir and not os.path.exists(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)",
@@ -1068,12 +1064,6 @@ 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)
@@ -1132,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)

View File

@@ -38,7 +38,7 @@ class EthernetSwitch(Node):
# this is an always-on node
self.setStatus(Node.started)
self._always_on = True
self.settings().update({"ports_mapping": [], "console_type": "telnet"})
self.settings().update({"ports_mapping": [], "console_type": "none"})
def info(self):
"""

View File

@@ -59,7 +59,7 @@ ETHERNET_SWITCH_SETTINGS = {
"default_name_format": "Switch{0}",
"symbol": ":/symbols/ethernet_switch.svg",
"category": Node.switches,
"console_type": "telnet",
"console_type": "none",
"ports_mapping": [],
"node_type": "ethernet_switch"
}

View File

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

View File

@@ -103,7 +103,8 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
self.uiConsoleResolutionComboBox.setCurrentIndex(self.uiConsoleResolutionComboBox.findText(settings["console_resolution"]))
self.uiConsoleHttpPortSpinBox.setValue(settings["console_http_port"])
self.uiHttpConsolePathLineEdit.setText(settings["console_http_path"])
self.uiExtraHostsTextEdit.setText(settings["extra_hosts"])
self.uiExtraHostsTextEdit.setPlainText(settings["extra_hosts"])
self.uiExtraVolumeTextEdit.setPlainText("\n".join(settings["extra_volumes"]))
if not group:
self.uiNameLineEdit.setText(settings["name"])
@@ -175,6 +176,8 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
settings["console_http_port"] = self.uiConsoleHttpPortSpinBox.value()
settings["console_http_path"] = self.uiHttpConsolePathLineEdit.text()
settings["extra_hosts"] = self.uiExtraHostsTextEdit.toPlainText()
# only tidy input here, validation is performed server side
settings["extra_volumes"] = [ y for x in self.uiExtraVolumeTextEdit.toPlainText().split("\n") for y in [ x.strip() ] if y ]
if not group:
adapters = self.uiAdapterSpinBox.value()

View File

@@ -95,6 +95,9 @@ class DockerVMPreferencesPage(QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidge
if docker_container["extra_hosts"]:
QtWidgets.QTreeWidgetItem(section_item, ["Extra hosts:", str(docker_container["extra_hosts"])])
if docker_container["extra_volumes"]:
QtWidgets.QTreeWidgetItem(section_item, ["Extra volumes:", "\n".join(docker_container["extra_volumes"])])
self.uiDockerVMInfoTreeWidget.expandAll()
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(0)
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(1)

View File

@@ -43,5 +43,6 @@ DOCKER_CONTAINER_SETTINGS = {
"console_http_port": 80,
"console_http_path": "/",
"extra_hosts": "",
"extra_volumes": [],
"node_type": "docker"
}

View File

@@ -297,9 +297,34 @@ one per line)</string>
</widget>
</item>
<item row="0" column="1">
<widget class="QTextEdit" name="uiExtraHostsTextEdit"/>
<widget class="QPlainTextEdit" name="uiExtraHostsTextEdit">
<property name="placeholderText">
<string>e.g. router:192.168.0.1</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="uiExtraVolumeLabel">
<property name="text">
<string>Additional directories to
make persistent that are
not included in the image
VOLUMES config. One
directory per line.</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPlainTextEdit" name="uiExtraVolumeTextEdit">
<property name="plainText">
<string/>
</property>
<property name="placeholderText">
<string>e.g. /etc/sysctl.d</string>
</property>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>

View File

@@ -143,11 +143,18 @@ class Ui_dockerVMConfigPageWidget(object):
self.uiExtraHostsLabel.setWordWrap(True)
self.uiExtraHostsLabel.setObjectName("uiExtraHostsLabel")
self.gridLayout_2.addWidget(self.uiExtraHostsLabel, 0, 0, 1, 1)
self.uiExtraHostsTextEdit = QtWidgets.QTextEdit(self.tab_2)
self.uiExtraHostsTextEdit = QtWidgets.QPlainTextEdit(self.tab_2)
self.uiExtraHostsTextEdit.setObjectName("uiExtraHostsTextEdit")
self.gridLayout_2.addWidget(self.uiExtraHostsTextEdit, 0, 1, 1, 1)
self.uiExtraVolumeLabel = QtWidgets.QLabel(self.tab_2)
self.uiExtraVolumeLabel.setObjectName("uiExtraVolumeLabel")
self.gridLayout_2.addWidget(self.uiExtraVolumeLabel, 1, 0, 1, 1)
self.uiExtraVolumeTextEdit = QtWidgets.QPlainTextEdit(self.tab_2)
self.uiExtraVolumeTextEdit.setPlainText("")
self.uiExtraVolumeTextEdit.setObjectName("uiExtraVolumeTextEdit")
self.gridLayout_2.addWidget(self.uiExtraVolumeTextEdit, 1, 1, 1, 1)
spacerItem = QtWidgets.QSpacerItem(20, 388, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout_2.addItem(spacerItem, 1, 1, 1, 1)
self.gridLayout_2.addItem(spacerItem, 2, 1, 1, 1)
self.uiTabWidget.addTab(self.tab_2, "")
self.tab_3 = QtWidgets.QWidget()
self.tab_3.setObjectName("tab_3")
@@ -201,6 +208,13 @@ class Ui_dockerVMConfigPageWidget(object):
"to the /etc/hosts file.\n"
"(hostname:IP\n"
"one per line)"))
self.uiExtraHostsTextEdit.setPlaceholderText(_translate("dockerVMConfigPageWidget", "e.g. router:192.168.0.1"))
self.uiExtraVolumeLabel.setText(_translate("dockerVMConfigPageWidget", "Additional directories to\n"
"make persistent that are\n"
"not included in the image\n"
"VOLUMES config. One\n"
"directory per line."))
self.uiExtraVolumeTextEdit.setPlaceholderText(_translate("dockerVMConfigPageWidget", "e.g. /etc/sysctl.d"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab_2), _translate("dockerVMConfigPageWidget", "Advanced"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab_3), _translate("dockerVMConfigPageWidget", "Usage"))

View File

@@ -19,122 +19,127 @@
<property name="accessibleDescription">
<string/>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeWidget" name="uiDockerVMInfoTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QTreeWidget" name="uiDockerVMsTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>160</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
</property>
<property name="indentation">
<number>10</number>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>1</string>
</property>
</column>
<column>
<property name="text">
<string>2</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
</column>
</widget>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="uiNewDockerVMPushButton">
<property name="text">
<string>&amp;New</string>
<widget class="QTreeWidget" name="uiDockerVMInfoTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="indentation">
<number>10</number>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>1</string>
</property>
</column>
<column>
<property name="text">
<string>2</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiCopyDockerVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Copy</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiEditDockerVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Edit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDeleteDockerVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete</string>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="uiNewDockerVMPushButton">
<property name="text">
<string>&amp;New</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiCopyDockerVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Copy</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiEditDockerVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Edit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDeleteDockerVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QTreeWidget" name="uiDockerVMsTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>160</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</widget>
</item>
</layout>

View File

@@ -13,41 +13,12 @@ class Ui_DockerVMPreferencesPageWidget(object):
DockerVMPreferencesPageWidget.setObjectName("DockerVMPreferencesPageWidget")
DockerVMPreferencesPageWidget.resize(575, 435)
DockerVMPreferencesPageWidget.setAccessibleDescription("")
self.gridLayout = QtWidgets.QGridLayout(DockerVMPreferencesPageWidget)
self.gridLayout.setObjectName("gridLayout")
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.uiDockerVMInfoTreeWidget = QtWidgets.QTreeWidget(DockerVMPreferencesPageWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiDockerVMInfoTreeWidget.sizePolicy().hasHeightForWidth())
self.uiDockerVMInfoTreeWidget.setSizePolicy(sizePolicy)
self.uiDockerVMInfoTreeWidget.setIndentation(10)
self.uiDockerVMInfoTreeWidget.setAllColumnsShowFocus(True)
self.uiDockerVMInfoTreeWidget.setObjectName("uiDockerVMInfoTreeWidget")
self.uiDockerVMInfoTreeWidget.header().setVisible(False)
self.verticalLayout.addWidget(self.uiDockerVMInfoTreeWidget)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.uiNewDockerVMPushButton = QtWidgets.QPushButton(DockerVMPreferencesPageWidget)
self.uiNewDockerVMPushButton.setObjectName("uiNewDockerVMPushButton")
self.horizontalLayout.addWidget(self.uiNewDockerVMPushButton)
self.uiCopyDockerVMPushButton = QtWidgets.QPushButton(DockerVMPreferencesPageWidget)
self.uiCopyDockerVMPushButton.setEnabled(False)
self.uiCopyDockerVMPushButton.setObjectName("uiCopyDockerVMPushButton")
self.horizontalLayout.addWidget(self.uiCopyDockerVMPushButton)
self.uiEditDockerVMPushButton = QtWidgets.QPushButton(DockerVMPreferencesPageWidget)
self.uiEditDockerVMPushButton.setEnabled(False)
self.uiEditDockerVMPushButton.setObjectName("uiEditDockerVMPushButton")
self.horizontalLayout.addWidget(self.uiEditDockerVMPushButton)
self.uiDeleteDockerVMPushButton = QtWidgets.QPushButton(DockerVMPreferencesPageWidget)
self.uiDeleteDockerVMPushButton.setEnabled(False)
self.uiDeleteDockerVMPushButton.setObjectName("uiDeleteDockerVMPushButton")
self.horizontalLayout.addWidget(self.uiDeleteDockerVMPushButton)
self.verticalLayout.addLayout(self.horizontalLayout)
self.gridLayout.addLayout(self.verticalLayout, 0, 1, 1, 1)
self.uiDockerVMsTreeWidget = QtWidgets.QTreeWidget(DockerVMPreferencesPageWidget)
self.verticalLayout_2 = QtWidgets.QVBoxLayout(DockerVMPreferencesPageWidget)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.splitter = QtWidgets.QSplitter(DockerVMPreferencesPageWidget)
self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName("splitter")
self.uiDockerVMsTreeWidget = QtWidgets.QTreeWidget(self.splitter)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -65,7 +36,41 @@ class Ui_DockerVMPreferencesPageWidget(object):
self.uiDockerVMsTreeWidget.setObjectName("uiDockerVMsTreeWidget")
self.uiDockerVMsTreeWidget.headerItem().setText(0, "1")
self.uiDockerVMsTreeWidget.header().setVisible(False)
self.gridLayout.addWidget(self.uiDockerVMsTreeWidget, 0, 0, 1, 1)
self.widget = QtWidgets.QWidget(self.splitter)
self.widget.setObjectName("widget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.widget)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.uiDockerVMInfoTreeWidget = QtWidgets.QTreeWidget(self.widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiDockerVMInfoTreeWidget.sizePolicy().hasHeightForWidth())
self.uiDockerVMInfoTreeWidget.setSizePolicy(sizePolicy)
self.uiDockerVMInfoTreeWidget.setIndentation(10)
self.uiDockerVMInfoTreeWidget.setAllColumnsShowFocus(True)
self.uiDockerVMInfoTreeWidget.setObjectName("uiDockerVMInfoTreeWidget")
self.uiDockerVMInfoTreeWidget.header().setVisible(False)
self.verticalLayout.addWidget(self.uiDockerVMInfoTreeWidget)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.uiNewDockerVMPushButton = QtWidgets.QPushButton(self.widget)
self.uiNewDockerVMPushButton.setObjectName("uiNewDockerVMPushButton")
self.horizontalLayout.addWidget(self.uiNewDockerVMPushButton)
self.uiCopyDockerVMPushButton = QtWidgets.QPushButton(self.widget)
self.uiCopyDockerVMPushButton.setEnabled(False)
self.uiCopyDockerVMPushButton.setObjectName("uiCopyDockerVMPushButton")
self.horizontalLayout.addWidget(self.uiCopyDockerVMPushButton)
self.uiEditDockerVMPushButton = QtWidgets.QPushButton(self.widget)
self.uiEditDockerVMPushButton.setEnabled(False)
self.uiEditDockerVMPushButton.setObjectName("uiEditDockerVMPushButton")
self.horizontalLayout.addWidget(self.uiEditDockerVMPushButton)
self.uiDeleteDockerVMPushButton = QtWidgets.QPushButton(self.widget)
self.uiDeleteDockerVMPushButton.setEnabled(False)
self.uiDeleteDockerVMPushButton.setObjectName("uiDeleteDockerVMPushButton")
self.horizontalLayout.addWidget(self.uiDeleteDockerVMPushButton)
self.verticalLayout.addLayout(self.horizontalLayout)
self.verticalLayout_2.addWidget(self.splitter)
self.retranslateUi(DockerVMPreferencesPageWidget)
QtCore.QMetaObject.connectSlotsByName(DockerVMPreferencesPageWidget)

View File

@@ -130,7 +130,7 @@ class Dynamips(Module):
for router in self._settings.get("routers"):
router_settings = IOS_ROUTER_SETTINGS.copy()
router_settings.update(router)
if not router_settings.get("chassis"):
if router_settings.get("chassis"):
del router_settings["chassis"]
templates.append(Template(router_settings))
TemplateManager.instance().updateList(templates)
@@ -166,12 +166,16 @@ class Dynamips(Module):
:param idlepc: Idle-PC value
"""
for ios_router in self._ios_routers.values():
if os.path.basename(ios_router["image"]) == image_path:
if ios_router["idlepc"] != idlepc:
ios_router["idlepc"] = idlepc
log.debug("Idle-PC value {} saved into '{}' template".format(idlepc, ios_router["name"]))
self._saveIOSRouters()
for template in TemplateManager.instance().templates().values():
if template.template_type() == "dynamips":
template_settings = template.settings()
router_image = template_settings.get("image")
old_idlepc = template_settings.get("idlepc")
if os.path.basename(router_image) == image_path and old_idlepc != idlepc:
template_settings["idlepc"] = idlepc
template.setSettings(template_settings)
log.debug("Idle-PC value {} saved into '{}' template".format(idlepc, template.name()))
TemplateManager.instance().updateTemplate(template)
@staticmethod
def configurationPage():

View File

@@ -123,7 +123,7 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
# try to guess the platform
image = os.path.basename(self.uiIOSImageLineEdit.text())
match = re.match(r"^(c[0-9]+)p?\\-\w+", image.lower())
match = re.match(r"^(c[0-9]+)p?-\w+", image.lower())
if not match:
QtWidgets.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
return

View File

@@ -55,7 +55,7 @@ class DynamipsPreferencesPage(QtWidgets.QWidget, Ui_DynamipsPreferencesPageWidge
dynamips_path = shutil.which("dynamips")
if sys.platform.startswith("darwin") and dynamips_path is None:
dynamips_path = "/Applications/GNS3.app/Contents/Resources/dynamips"
dynamips_path = "/Applications/GNS3.app/Contents/MacOS/dynamips"
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select Dynamips", dynamips_path, file_filter)
if not path:
return

View File

@@ -111,7 +111,7 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
# try to guess the platform
image = os.path.basename(path)
match = re.match(r"^(c[0-9]+)\\-\w+", image)
match = re.match(r"^(c[0-9]+)p?-\w+", image)
if not match:
QtWidgets.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
return

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>700</width>
<height>511</height>
<width>715</width>
<height>440</height>
</rect>
</property>
<property name="windowTitle">
@@ -16,133 +16,138 @@
<property name="accessibleName">
<string>IOS router templates</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTreeWidget" name="uiIOSRoutersTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="maximumSize">
<size>
<width>160</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
<widget class="QTreeWidget" name="uiIOSRoutersTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</column>
</widget>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeWidget" name="uiIOSRouterInfoTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<property name="maximumSize">
<size>
<width>160</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
</property>
<property name="indentation">
<number>10</number>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>1</string>
</property>
</column>
<column>
<property name="text">
<string>2</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
</column>
</widget>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="uiNewIOSRouterPushButton">
<property name="text">
<string>&amp;New</string>
<widget class="QTreeWidget" name="uiIOSRouterInfoTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="indentation">
<number>10</number>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>1</string>
</property>
</column>
<column>
<property name="text">
<string>2</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDecompressIOSPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Decompress</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiCopyIOSRouterPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Copy</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiEditIOSRouterPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Edit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDeleteIOSRouterPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete</string>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="uiNewIOSRouterPushButton">
<property name="text">
<string>&amp;New</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDecompressIOSPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Decompress</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiCopyIOSRouterPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Copy</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiEditIOSRouterPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Edit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDeleteIOSRouterPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>

View File

@@ -11,10 +11,13 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_IOSRouterPreferencesPageWidget(object):
def setupUi(self, IOSRouterPreferencesPageWidget):
IOSRouterPreferencesPageWidget.setObjectName("IOSRouterPreferencesPageWidget")
IOSRouterPreferencesPageWidget.resize(700, 511)
self.gridLayout = QtWidgets.QGridLayout(IOSRouterPreferencesPageWidget)
self.gridLayout.setObjectName("gridLayout")
self.uiIOSRoutersTreeWidget = QtWidgets.QTreeWidget(IOSRouterPreferencesPageWidget)
IOSRouterPreferencesPageWidget.resize(715, 440)
self.verticalLayout_2 = QtWidgets.QVBoxLayout(IOSRouterPreferencesPageWidget)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.splitter = QtWidgets.QSplitter(IOSRouterPreferencesPageWidget)
self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName("splitter")
self.uiIOSRoutersTreeWidget = QtWidgets.QTreeWidget(self.splitter)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -32,10 +35,12 @@ class Ui_IOSRouterPreferencesPageWidget(object):
self.uiIOSRoutersTreeWidget.setObjectName("uiIOSRoutersTreeWidget")
self.uiIOSRoutersTreeWidget.headerItem().setText(0, "1")
self.uiIOSRoutersTreeWidget.header().setVisible(False)
self.gridLayout.addWidget(self.uiIOSRoutersTreeWidget, 0, 0, 1, 1)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.widget = QtWidgets.QWidget(self.splitter)
self.widget.setObjectName("widget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.widget)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.uiIOSRouterInfoTreeWidget = QtWidgets.QTreeWidget(IOSRouterPreferencesPageWidget)
self.uiIOSRouterInfoTreeWidget = QtWidgets.QTreeWidget(self.widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -48,27 +53,27 @@ class Ui_IOSRouterPreferencesPageWidget(object):
self.verticalLayout.addWidget(self.uiIOSRouterInfoTreeWidget)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.uiNewIOSRouterPushButton = QtWidgets.QPushButton(IOSRouterPreferencesPageWidget)
self.uiNewIOSRouterPushButton = QtWidgets.QPushButton(self.widget)
self.uiNewIOSRouterPushButton.setObjectName("uiNewIOSRouterPushButton")
self.horizontalLayout.addWidget(self.uiNewIOSRouterPushButton)
self.uiDecompressIOSPushButton = QtWidgets.QPushButton(IOSRouterPreferencesPageWidget)
self.uiDecompressIOSPushButton = QtWidgets.QPushButton(self.widget)
self.uiDecompressIOSPushButton.setEnabled(False)
self.uiDecompressIOSPushButton.setObjectName("uiDecompressIOSPushButton")
self.horizontalLayout.addWidget(self.uiDecompressIOSPushButton)
self.uiCopyIOSRouterPushButton = QtWidgets.QPushButton(IOSRouterPreferencesPageWidget)
self.uiCopyIOSRouterPushButton = QtWidgets.QPushButton(self.widget)
self.uiCopyIOSRouterPushButton.setEnabled(False)
self.uiCopyIOSRouterPushButton.setObjectName("uiCopyIOSRouterPushButton")
self.horizontalLayout.addWidget(self.uiCopyIOSRouterPushButton)
self.uiEditIOSRouterPushButton = QtWidgets.QPushButton(IOSRouterPreferencesPageWidget)
self.uiEditIOSRouterPushButton = QtWidgets.QPushButton(self.widget)
self.uiEditIOSRouterPushButton.setEnabled(False)
self.uiEditIOSRouterPushButton.setObjectName("uiEditIOSRouterPushButton")
self.horizontalLayout.addWidget(self.uiEditIOSRouterPushButton)
self.uiDeleteIOSRouterPushButton = QtWidgets.QPushButton(IOSRouterPreferencesPageWidget)
self.uiDeleteIOSRouterPushButton = QtWidgets.QPushButton(self.widget)
self.uiDeleteIOSRouterPushButton.setEnabled(False)
self.uiDeleteIOSRouterPushButton.setObjectName("uiDeleteIOSRouterPushButton")
self.horizontalLayout.addWidget(self.uiDeleteIOSRouterPushButton)
self.verticalLayout.addLayout(self.horizontalLayout)
self.gridLayout.addLayout(self.verticalLayout, 0, 1, 1, 1)
self.verticalLayout_2.addWidget(self.splitter)
self.retranslateUi(IOSRouterPreferencesPageWidget)
QtCore.QMetaObject.connectSlotsByName(IOSRouterPreferencesPageWidget)

View File

@@ -98,7 +98,10 @@ class IOUPreferencesPage(QtWidgets.QWidget, Ui_IOUPreferencesPageWidget):
:param settings: IOU settings
"""
self.IOULicenceTextEdit.setPlainText(settings["iourc_content"])
if settings["iourc_content"]:
self.IOULicenceTextEdit.blockSignals(True)
self.IOULicenceTextEdit.setPlainText(settings["iourc_content"])
self.IOULicenceTextEdit.blockSignals(False)
self.uiLicensecheckBox.setChecked(settings["license_check"])
def loadPreferences(self):
@@ -106,7 +109,10 @@ class IOUPreferencesPage(QtWidgets.QWidget, Ui_IOUPreferencesPageWidget):
Loads IOU preferences.
"""
Controller.instance().get("/iou_license", self._getSettingsCallback)
if Controller.instance().connected():
Controller.instance().get("/iou_license", self._getSettingsCallback)
else:
log.error("Cannot load the IOU license in the preferences dialog: not connected to the controller")
@qslot
def _getSettingsCallback(self, result, error=False, **kwargs):
@@ -115,7 +121,7 @@ class IOUPreferencesPage(QtWidgets.QWidget, Ui_IOUPreferencesPageWidget):
return
if error:
if "message" in result:
log.error("Error while getting settings : {}".format(result["message"]))
log.error("Error while getting the IOU license information: {}".format(result["message"]))
return
self._old_settings = copy.copy(result)
self._populateWidgets(result)

View File

@@ -16,123 +16,128 @@
<property name="accessibleName">
<string>IOU device templates</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTreeWidget" name="uiIOUDevicesTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="maximumSize">
<size>
<width>160</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
<widget class="QTreeWidget" name="uiIOUDevicesTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</column>
</widget>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeWidget" name="uiIOUDeviceInfoTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<property name="maximumSize">
<size>
<width>160</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
</property>
<property name="indentation">
<number>10</number>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>1</string>
</property>
</column>
<column>
<property name="text">
<string>2</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
</column>
</widget>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="uiNewIOUDevicePushButton">
<property name="text">
<string>&amp;New</string>
<widget class="QTreeWidget" name="uiIOUDeviceInfoTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="indentation">
<number>10</number>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>1</string>
</property>
</column>
<column>
<property name="text">
<string>2</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiCopyIOUDevicePushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Copy</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiEditIOUDevicePushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Edit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDeleteIOUDevicePushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete</string>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="uiNewIOUDevicePushButton">
<property name="text">
<string>&amp;New</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiCopyIOUDevicePushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Copy</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiEditIOUDevicePushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Edit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDeleteIOUDevicePushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>

View File

@@ -12,9 +12,12 @@ class Ui_IOUDevicePreferencesPageWidget(object):
def setupUi(self, IOUDevicePreferencesPageWidget):
IOUDevicePreferencesPageWidget.setObjectName("IOUDevicePreferencesPageWidget")
IOUDevicePreferencesPageWidget.resize(542, 449)
self.gridLayout = QtWidgets.QGridLayout(IOUDevicePreferencesPageWidget)
self.gridLayout.setObjectName("gridLayout")
self.uiIOUDevicesTreeWidget = QtWidgets.QTreeWidget(IOUDevicePreferencesPageWidget)
self.verticalLayout_2 = QtWidgets.QVBoxLayout(IOUDevicePreferencesPageWidget)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.splitter = QtWidgets.QSplitter(IOUDevicePreferencesPageWidget)
self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName("splitter")
self.uiIOUDevicesTreeWidget = QtWidgets.QTreeWidget(self.splitter)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -32,10 +35,12 @@ class Ui_IOUDevicePreferencesPageWidget(object):
self.uiIOUDevicesTreeWidget.setObjectName("uiIOUDevicesTreeWidget")
self.uiIOUDevicesTreeWidget.headerItem().setText(0, "1")
self.uiIOUDevicesTreeWidget.header().setVisible(False)
self.gridLayout.addWidget(self.uiIOUDevicesTreeWidget, 0, 0, 1, 1)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.widget = QtWidgets.QWidget(self.splitter)
self.widget.setObjectName("widget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.widget)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.uiIOUDeviceInfoTreeWidget = QtWidgets.QTreeWidget(IOUDevicePreferencesPageWidget)
self.uiIOUDeviceInfoTreeWidget = QtWidgets.QTreeWidget(self.widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -48,23 +53,23 @@ class Ui_IOUDevicePreferencesPageWidget(object):
self.verticalLayout.addWidget(self.uiIOUDeviceInfoTreeWidget)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.uiNewIOUDevicePushButton = QtWidgets.QPushButton(IOUDevicePreferencesPageWidget)
self.uiNewIOUDevicePushButton = QtWidgets.QPushButton(self.widget)
self.uiNewIOUDevicePushButton.setObjectName("uiNewIOUDevicePushButton")
self.horizontalLayout.addWidget(self.uiNewIOUDevicePushButton)
self.uiCopyIOUDevicePushButton = QtWidgets.QPushButton(IOUDevicePreferencesPageWidget)
self.uiCopyIOUDevicePushButton = QtWidgets.QPushButton(self.widget)
self.uiCopyIOUDevicePushButton.setEnabled(False)
self.uiCopyIOUDevicePushButton.setObjectName("uiCopyIOUDevicePushButton")
self.horizontalLayout.addWidget(self.uiCopyIOUDevicePushButton)
self.uiEditIOUDevicePushButton = QtWidgets.QPushButton(IOUDevicePreferencesPageWidget)
self.uiEditIOUDevicePushButton = QtWidgets.QPushButton(self.widget)
self.uiEditIOUDevicePushButton.setEnabled(False)
self.uiEditIOUDevicePushButton.setObjectName("uiEditIOUDevicePushButton")
self.horizontalLayout.addWidget(self.uiEditIOUDevicePushButton)
self.uiDeleteIOUDevicePushButton = QtWidgets.QPushButton(IOUDevicePreferencesPageWidget)
self.uiDeleteIOUDevicePushButton = QtWidgets.QPushButton(self.widget)
self.uiDeleteIOUDevicePushButton.setEnabled(False)
self.uiDeleteIOUDevicePushButton.setObjectName("uiDeleteIOUDevicePushButton")
self.horizontalLayout.addWidget(self.uiDeleteIOUDevicePushButton)
self.verticalLayout.addLayout(self.horizontalLayout)
self.gridLayout.addLayout(self.verticalLayout, 0, 1, 1, 1)
self.verticalLayout_2.addWidget(self.splitter)
self.retranslateUi(IOUDevicePreferencesPageWidget)
QtCore.QMetaObject.connectSlotsByName(IOUDevicePreferencesPageWidget)

View File

@@ -14,6 +14,13 @@
<string>IOS on UNIX</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>IOU licence (iourc file):</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>

View File

@@ -1,21 +1,22 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/iou/ui/iou_preferences_page.ui'
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/iou/ui/iou_preferences_page.ui'
#
# Created by: PyQt5 UI code generator 5.7.1
# Created by: PyQt5 UI code generator 5.9
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_IOUPreferencesPageWidget(object):
def setupUi(self, IOUPreferencesPageWidget):
IOUPreferencesPageWidget.setObjectName("IOUPreferencesPageWidget")
IOUPreferencesPageWidget.resize(490, 532)
self.vboxlayout = QtWidgets.QVBoxLayout(IOUPreferencesPageWidget)
self.vboxlayout.setObjectName("vboxlayout")
self.label = QtWidgets.QLabel(IOUPreferencesPageWidget)
self.label.setObjectName("label")
self.vboxlayout.addWidget(self.label)
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.IOULicenceTextEdit = QtWidgets.QPlainTextEdit(IOUPreferencesPageWidget)
@@ -47,7 +48,9 @@ class Ui_IOUPreferencesPageWidget(object):
def retranslateUi(self, IOUPreferencesPageWidget):
_translate = QtCore.QCoreApplication.translate
IOUPreferencesPageWidget.setWindowTitle(_translate("IOUPreferencesPageWidget", "IOS on UNIX"))
self.label.setText(_translate("IOUPreferencesPageWidget", "IOU licence (iourc file):"))
self.IOULicenceTextEdit.setToolTip(_translate("IOUPreferencesPageWidget", "A license is required to run IOU. Copy & paste the content of your iourc file here or use the browse button to select a file. The license will be pushed to remote servers."))
self.uiIOURCPathToolButton.setText(_translate("IOUPreferencesPageWidget", "&Browse..."))
self.uiLicensecheckBox.setText(_translate("IOUPreferencesPageWidget", "Check for a valid IOU license key"))
self.uiRestoreDefaultsPushButton.setText(_translate("IOUPreferencesPageWidget", "Restore defaults"))

View File

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

View File

@@ -126,7 +126,7 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
# default is qemu-system-i386w.exe on Windows 32-bit with a remote server
search_string = "i386w.exe"
elif ComputeManager.instance().localPlatform().startswith("darwin") and hasattr(sys, "frozen") and self.uiLocalRadioButton.isChecked():
search_string = "GNS3.app/Contents/Resources/qemu/bin/qemu-system-x86_64"
search_string = "GNS3.app/Contents/MacOS/qemu/bin/qemu-system-x86_64"
elif is_64bit:
# default is qemu-system-x86_64 on other 64-bit platforms
search_string = "x86_64"
@@ -163,7 +163,7 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
settings["initrd"] = self.uiInitrdImageLineEdit.text()
settings["kernel_image"] = self.uiKernelImageLineEdit.text()
settings["kernel_command_line"] = "ide_generic.probe_mask=0x01 ide_core.chs=0.0:980,16,32 auto nousb console=ttyS0,9600 bigphysarea=65536 ide1=noprobe no-hlt -net nic"
settings["options"] = "-no-kvm -icount auto -hdachs 980,16,32"
settings["options"] = "-no-kvm -icount auto"
if not sys.platform.startswith("darwin"):
settings["cpu_throttling"] = 80 # limit to 80% CPU usage
settings["process_priority"] = "low"

View File

@@ -391,7 +391,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
try:
ports = StandardPortNameFactory(adapters, first_port_name, port_name_format, port_segment_size)
except (ValueError, KeyError):
except (IndexError, ValueError, KeyError):
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
return

View File

@@ -860,6 +860,8 @@
&lt;li&gt;%vm-id% =VM ID&lt;/li&gt;
&lt;li&gt;%project-id% = project ID&lt;/li&gt;
&lt;li&gt;%project-path% = project path&lt;/li&gt;
&lt;li&gt;%console-port% = console port number&lt;/li&gt;
&lt;li&gt;%guest-cid% = unique ID from 3 to 65535&lt;/li&gt;
&lt;/ul&gt;
&lt;/body&gt;&lt;/html&gt;</string>
</property>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
# Created by: PyQt5 UI code generator 5.9
#
# WARNING! All changes made in this file will be lost!
@@ -541,6 +541,8 @@ class Ui_QemuVMConfigPageWidget(object):
"<li>%vm-id% =VM ID</li>\n"
"<li>%project-id% = project ID</li>\n"
"<li>%project-path% = project path</li>\n"
"<li>%console-port% = console port number</li>\n"
"<li>%guest-cid% = unique ID from 3 to 65535</li>\n"
"</ul>\n"
"</body></html>"))
self.uiBaseVMCheckBox.setText(_translate("QemuVMConfigPageWidget", "Use as a linked base VM"))

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>526</width>
<height>400</height>
<width>535</width>
<height>355</height>
</rect>
</property>
<property name="windowTitle">
@@ -16,123 +16,128 @@
<property name="accessibleName">
<string>Qemu VM templates</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTreeWidget" name="uiQemuVMsTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="maximumSize">
<size>
<width>160</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
<widget class="QTreeWidget" name="uiQemuVMsTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</column>
</widget>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeWidget" name="uiQemuVMInfoTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<property name="maximumSize">
<size>
<width>160</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
</property>
<property name="indentation">
<number>10</number>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>1</string>
</property>
</column>
<column>
<property name="text">
<string>2</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
</column>
</widget>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="uiNewQemuVMPushButton">
<property name="text">
<string>&amp;New</string>
<widget class="QTreeWidget" name="uiQemuVMInfoTreeWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="indentation">
<number>10</number>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>1</string>
</property>
</column>
<column>
<property name="text">
<string>2</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiCopyQemuVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Copy</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiEditQemuVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Edit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDeleteQemuVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete</string>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="uiNewQemuVMPushButton">
<property name="text">
<string>&amp;New</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiCopyQemuVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Copy</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiEditQemuVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Edit</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDeleteQemuVMPushButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>

View File

@@ -11,10 +11,13 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_QemuVMPreferencesPageWidget(object):
def setupUi(self, QemuVMPreferencesPageWidget):
QemuVMPreferencesPageWidget.setObjectName("QemuVMPreferencesPageWidget")
QemuVMPreferencesPageWidget.resize(526, 400)
self.gridLayout = QtWidgets.QGridLayout(QemuVMPreferencesPageWidget)
self.gridLayout.setObjectName("gridLayout")
self.uiQemuVMsTreeWidget = QtWidgets.QTreeWidget(QemuVMPreferencesPageWidget)
QemuVMPreferencesPageWidget.resize(535, 355)
self.verticalLayout_2 = QtWidgets.QVBoxLayout(QemuVMPreferencesPageWidget)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.splitter = QtWidgets.QSplitter(QemuVMPreferencesPageWidget)
self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName("splitter")
self.uiQemuVMsTreeWidget = QtWidgets.QTreeWidget(self.splitter)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -32,10 +35,12 @@ class Ui_QemuVMPreferencesPageWidget(object):
self.uiQemuVMsTreeWidget.setObjectName("uiQemuVMsTreeWidget")
self.uiQemuVMsTreeWidget.headerItem().setText(0, "1")
self.uiQemuVMsTreeWidget.header().setVisible(False)
self.gridLayout.addWidget(self.uiQemuVMsTreeWidget, 0, 0, 1, 1)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.widget = QtWidgets.QWidget(self.splitter)
self.widget.setObjectName("widget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.widget)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.uiQemuVMInfoTreeWidget = QtWidgets.QTreeWidget(QemuVMPreferencesPageWidget)
self.uiQemuVMInfoTreeWidget = QtWidgets.QTreeWidget(self.widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -48,23 +53,23 @@ class Ui_QemuVMPreferencesPageWidget(object):
self.verticalLayout.addWidget(self.uiQemuVMInfoTreeWidget)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.uiNewQemuVMPushButton = QtWidgets.QPushButton(QemuVMPreferencesPageWidget)
self.uiNewQemuVMPushButton = QtWidgets.QPushButton(self.widget)
self.uiNewQemuVMPushButton.setObjectName("uiNewQemuVMPushButton")
self.horizontalLayout.addWidget(self.uiNewQemuVMPushButton)
self.uiCopyQemuVMPushButton = QtWidgets.QPushButton(QemuVMPreferencesPageWidget)
self.uiCopyQemuVMPushButton = QtWidgets.QPushButton(self.widget)
self.uiCopyQemuVMPushButton.setEnabled(False)
self.uiCopyQemuVMPushButton.setObjectName("uiCopyQemuVMPushButton")
self.horizontalLayout.addWidget(self.uiCopyQemuVMPushButton)
self.uiEditQemuVMPushButton = QtWidgets.QPushButton(QemuVMPreferencesPageWidget)
self.uiEditQemuVMPushButton = QtWidgets.QPushButton(self.widget)
self.uiEditQemuVMPushButton.setEnabled(False)
self.uiEditQemuVMPushButton.setObjectName("uiEditQemuVMPushButton")
self.horizontalLayout.addWidget(self.uiEditQemuVMPushButton)
self.uiDeleteQemuVMPushButton = QtWidgets.QPushButton(QemuVMPreferencesPageWidget)
self.uiDeleteQemuVMPushButton = QtWidgets.QPushButton(self.widget)
self.uiDeleteQemuVMPushButton.setEnabled(False)
self.uiDeleteQemuVMPushButton.setObjectName("uiDeleteQemuVMPushButton")
self.horizontalLayout.addWidget(self.uiDeleteQemuVMPushButton)
self.verticalLayout.addLayout(self.horizontalLayout)
self.gridLayout.addLayout(self.verticalLayout, 0, 1, 1, 1)
self.verticalLayout_2.addWidget(self.splitter)
self.retranslateUi(QemuVMPreferencesPageWidget)
QtCore.QMetaObject.connectSlotsByName(QemuVMPreferencesPageWidget)

View File

@@ -72,7 +72,7 @@ class TraceNGNode(Node):
self._last_destination = destination
params = {"destination": destination}
log.debug("{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, body=params, timeout=None, progressText="{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, body=params, timeout=None, showProgress=False)
def info(self):
"""

View File

@@ -99,7 +99,7 @@ class VirtualBoxVMConfigurationPage(QtWidgets.QWidget, Ui_virtualBoxVMConfigPage
try:
ports = StandardPortNameFactory(adapters, first_port_name, port_name_format, port_segment_size)
except (ValueError, KeyError):
except (IndexError, ValueError, KeyError):
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
return

View File

@@ -99,7 +99,7 @@ class VMwareVMConfigurationPage(QtWidgets.QWidget, Ui_VMwareVMConfigPageWidget):
try:
ports = StandardPortNameFactory(adapters, first_port_name, port_name_format, port_segment_size)
except (ValueError, KeyError):
except (IndexError, ValueError, KeyError):
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
return

View File

@@ -83,7 +83,6 @@ class VPCS(Module):
"""
# save the settings
print(self._settings)
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
server_settings = {}

View File

@@ -46,7 +46,8 @@ class Node(BaseNode):
self._settings = {"name": "",
"x": None,
"y": None,
"z": 1}
"z": 1,
"locked": False}
def settings(self):
"""
@@ -148,6 +149,13 @@ class Node(BaseNode):
return self._settings["z"]
def locked(self):
"""
Is the node locked
"""
return self._settings["locked"]
def setSymbol(self, symbol):
"""
Sets the symbol for this node.
@@ -215,7 +223,7 @@ class Node(BaseNode):
return
log.debug("{} is starting".format(self.name()))
self.post("/start", self._startCallback, timeout=None, progressText="{} is starting".format(self.name()))
self.post("/start", self._startCallback, timeout=None, showProgress=False)
def _startCallback(self, result, error=False, **kwargs):
"""
@@ -241,7 +249,7 @@ class Node(BaseNode):
return
log.debug("{} is stopping".format(self.name()))
self.post("/stop", self._stopCallback, timeout=None, progressText="{} is stopping".format(self.name()))
self.post("/stop", self._stopCallback, timeout=None, showProgress=False)
def _stopCallback(self, result, error=False, **kwargs):
"""
@@ -271,7 +279,7 @@ class Node(BaseNode):
return
log.debug("{} is being suspended".format(self.name()))
self.post("/suspend", self._suspendCallback, timeout=None, progressText="{} is suspending".format(self.name()))
self.post("/suspend", self._suspendCallback, timeout=None, showProgress=False)
def _suspendCallback(self, result, error=False, **kwargs):
"""
@@ -293,7 +301,7 @@ class Node(BaseNode):
"""
log.debug("{} is being reloaded".format(self.name()))
self.post("/reload", self._reloadCallback, timeout=None, progressText="{} is reloading".format(self.name()))
self.post("/reload", self._reloadCallback, timeout=None, showProgress=False)
def _reloadCallback(self, result, error=False, **kwargs):
"""
@@ -373,6 +381,7 @@ class Node(BaseNode):
"x",
"y",
"z",
"locked",
"symbol",
"label",
"port_name_format",
@@ -446,7 +455,7 @@ class Node(BaseNode):
if not skip_controller:
for link in self.links():
link.setDeleting()
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback)
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback, showProgress=False)
else:
self.deleted_signal.emit()
self._module.removeNode(self)
@@ -476,7 +485,7 @@ class Node(BaseNode):
"y": int(y),
"z": int(z)}
self.post("/duplicate", self._duplicateCallback, body=params, timeout=None, progressText="{} is being duplicated".format(self.name()))
self.post("/duplicate", self._duplicateCallback, body=params, timeout=None, showProgress=False)
def _duplicateCallback(self, result, error=False, **kwargs):
"""
@@ -529,7 +538,7 @@ class Node(BaseNode):
del result["properties"]
# Update common element of all nodes
for key in ["x", "y", "z", "symbol", "label", "console_host", "console", "console_type", "console_auto_start", "custom_adapters"]:
for key in ["x", "y", "z", "locked", "symbol", "label", "console_host", "console", "console_type", "console_auto_start", "custom_adapters"]:
if key in result:
self._settings[key] = result[key]
@@ -578,7 +587,8 @@ class Node(BaseNode):
data = {"x": int(node_item.pos().x()),
"y": int(node_item.pos().y()),
"z": int(node_item.zValue()),
"symbol": node_item.symbol()}
"symbol": node_item.symbol(),
"locked": node_item.locked()}
if node_item.label() is not None:
data["label"] = node_item.label().dump()
@@ -618,12 +628,12 @@ class Node(BaseNode):
from .main_window import MainWindow
general_settings = MainWindow.instance().settings()
if console_type != "telnet":
if not console_type:
console_type = self.consoleType()
if console_type == "vnc":
return general_settings["vnc_console_command"]
if console_type.startswith("spice"):
return general_settings["spice_console_command"]
if console_type == "vnc":
return general_settings["vnc_console_command"]
elif console_type.startswith("spice"):
return general_settings["spice_console_command"]
return general_settings["telnet_console_command"]
def consoleType(self):
@@ -631,7 +641,7 @@ class Node(BaseNode):
Get the console type (serial, telnet or VNC)
"""
console_type = "telnet"
console_type = "none"
if "console_type" in self.settings():
return self.settings()["console_type"]
return console_type
@@ -755,7 +765,7 @@ class Node(BaseNode):
with open(context["path"], "wb+") as f:
f.write(raw_body)
except OSError as e:
log.erro("Can't write %s: %s", context["path"], str(e))
log.error("Cannot export file '{}': {}".format(context["path"], e))
def exportConfigsToDirectory(self, directory):
"""
@@ -765,12 +775,13 @@ class Node(BaseNode):
"""
if not hasattr(self, "configFiles"):
return
return False
for file in self.configFiles():
self.get("/files/{file}".format(file=file),
self._exportConfigsToDirectoryCallback,
context={"directory": directory, "file": file},
raw=True)
return True
def _exportConfigsToDirectoryCallback(self, result, error=False, raw_body=None, context={}, **kwargs):
"""

View File

@@ -20,7 +20,7 @@ Nodes view that list all the available nodes to be dragged and dropped
on the QGraphics scene.
"""
import sip
from .qt import sip
from .qt import QtCore, QtGui, QtWidgets, qpartial
from .controller import Controller

View File

@@ -27,7 +27,7 @@ from gns3.qt import QtGui, QtCore, QtWidgets
from gns3.local_config import LocalConfig
from ..ui.general_preferences_page_ui import Ui_GeneralPreferencesPageWidget
from gns3.local_server import LocalServer
from ..settings import GRAPHICS_VIEW_SETTINGS, GENERAL_SETTINGS, STYLES
from ..settings import GRAPHICS_VIEW_SETTINGS, GENERAL_SETTINGS, STYLES, SYMBOL_THEMES
from ..dialogs.console_command_dialog import ConsoleCommandDialog
@@ -66,6 +66,7 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
self.uiBrowseConfigurationPushButton.clicked.connect(self._browseConfigurationDirectorySlot)
self._default_label_color = QtGui.QColor(QtCore.Qt.black)
self.uiStyleComboBox.addItems(STYLES)
self.uiSymbolThemeComboBox.addItems(SYMBOL_THEMES)
self.uiImageDirectoriesAddPushButton.clicked.connect(self._imageDirectoriesAddPushButtonSlot)
self.uiImageDirectoriesDeletePushButton.clicked.connect(self._imageDirectoriesDeletePushButtonSlot)
@@ -308,9 +309,15 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
self.uiHdpiCheckBox.setChecked(settings["hdpi"])
self.uiTelnetConsoleCommandLineEdit.setText(settings["telnet_console_command"])
self.uiTelnetConsoleCommandLineEdit.setCursorPosition(0)
index = self.uiStyleComboBox.findText(settings["style"])
if index != -1:
self.uiStyleComboBox.setCurrentIndex(index)
index = self.uiSymbolThemeComboBox.findText(settings["symbol_theme"])
if index != -1:
self.uiSymbolThemeComboBox.setCurrentIndex(index)
self.uiDelayConsoleAllSpinBox.setValue(settings["delay_console_all"])
self.uiVNCConsoleCommandLineEdit.setText(settings["vnc_console_command"])
@@ -395,6 +402,7 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
new_general_settings = {
"style": self.uiStyleComboBox.currentText(),
"symbol_theme": self.uiSymbolThemeComboBox.currentText(),
"experimental_features": self.uiExperimentalFeaturesCheckBox.isChecked(),
"hdpi": self.uiHdpiCheckBox.isChecked(),
"check_for_update": self.uiCheckForUpdateCheckBox.isChecked(),

View File

@@ -72,7 +72,11 @@ class GNS3VMPreferencesPage(QtWidgets.QWidget, Ui_GNS3VMPreferencesPageWidget):
"""
Loads the preference from controller.
"""
Controller.instance().get("/gns3vm", self._getSettingsCallback)
if Controller.instance().connected():
Controller.instance().get("/gns3vm", self._getSettingsCallback)
else:
log.error("Cannot load the GNS3 VM settings in the preferences dialog: not connected to the controller")
@qslot
def _getSettingsCallback(self, result, error=False, **kwargs):
@@ -81,7 +85,7 @@ class GNS3VMPreferencesPage(QtWidgets.QWidget, Ui_GNS3VMPreferencesPageWidget):
return
if error:
if "message" in result:
log.error("Error while getting settings : {}".format(result["message"]))
log.error("Error while getting the GNS3 VM settings : {}".format(result["message"]))
return
self._old_settings = copy.copy(result)
self._settings = result

View File

@@ -67,6 +67,7 @@ class ServerPreferencesPage(QtWidgets.QWidget, Ui_ServerPreferencesPageWidget):
# ignore link-local addresses
continue
self.uiLocalServerHostComboBox.addItem(address_string, address_string)
self.uiLocalServerHostComboBox.addItem("localhost", "localhost") # local host
self.uiLocalServerHostComboBox.addItem("::", "::") # all IPv6 addresses
self.uiLocalServerHostComboBox.addItem("0.0.0.0", "0.0.0.0") # all IPv4 addresses

View File

@@ -19,7 +19,7 @@
Base class for port objects.
"""
import sip
from ..qt import sip
from ..qt import qslot

View File

@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sip
from .qt import sip
import time
from contextlib import contextmanager
@@ -189,7 +189,10 @@ class Progress(QtCore.QObject):
# Due to Qt limitations for large numbers (above 32bit int) we calculate "progress" ourselves
current, maximum = self._normalize(query['current'], query['maximum'])
progress_dialog.setMaximum(maximum)
progress_dialog.setValue(current)
try:
progress_dialog.setValue(current)
except OverflowError:
progress_dialog.setValue(100)
if text and query["maximum"] > 1000:
text += "\n{} / {}".format(human_filesize(query["current"]), human_filesize(query["maximum"]))

View File

@@ -17,14 +17,12 @@
import os
import json
from .qt import QtCore, qpartial, QtWidgets, QtNetwork, qslot
from .qt import QtCore, qpartial, QtNetwork, QtWebSockets, qslot
from gns3.controller import Controller
from gns3.compute_manager import ComputeManager
from gns3.topology import Topology
from gns3.local_config import LocalConfig
from gns3.settings import GRAPHICS_VIEW_SETTINGS
from gns3.template_manager import TemplateManager
from gns3.utils import parse_version
import logging
@@ -49,7 +47,6 @@ class Project(QtCore.QObject):
# Called when project is fully loaded
project_loaded_signal = QtCore.Signal()
def __init__(self):
self._id = None
@@ -62,8 +59,7 @@ class Project(QtCore.QObject):
self._auto_close = False
config = LocalConfig.instance()
graphic_settings = LocalConfig.instance().loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS)
graphic_settings = config.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS)
self._scene_width = graphic_settings["scene_width"]
self._scene_height = graphic_settings["scene_height"]
self._zoom = graphic_settings.get("zoom", None)
@@ -85,6 +81,7 @@ class Project(QtCore.QObject):
# Due to bug in Qt on some version we need a dedicated network manager
self._notification_network_manager = QtNetwork.QNetworkAccessManager()
self._notification_stream = None
self._websocket = QtWebSockets.QWebSocket()
super().__init__()
@@ -336,7 +333,7 @@ class Project(QtCore.QObject):
def _duplicateCallback(self, callback, result, error=False, **kwargs):
if error:
if "message" in result:
QtWidgets.QMessageBox.critical(None, "Duplicate project", "Error while duplicating: {}".format(result["message"]))
log.error("Error while duplicating project: {}".format(result["message"]))
return
if callback:
callback(result["project_id"])
@@ -486,7 +483,8 @@ class Project(QtCore.QObject):
if self._closed:
self._closed = False
self._closing = False
self._startListenNotifications()
if not self._notification_stream:
self._startListenNotifications()
self.project_updated_signal.emit()
self.project_loaded_signal.emit()
@@ -538,7 +536,8 @@ class Project(QtCore.QObject):
if self._closed:
self._closed = False
self._closing = False
self._startListenNotifications()
if not self._notification_stream:
self._startListenNotifications()
self.project_updated_signal.emit()
self.get("/nodes", self._listNodesCallback)
@@ -610,7 +609,7 @@ class Project(QtCore.QObject):
log.debug("Stop listening for notifications from project %s", self._id)
stream = self._notification_stream
self._notification_stream = None
stream.abort()
stream.close()
def _startListenNotifications(self):
if not Controller.instance().connected():
@@ -628,7 +627,7 @@ class Project(QtCore.QObject):
else:
path = "/projects/{project_id}/notifications/ws".format(project_id=self._id)
self._notification_stream = Controller.instance().connectProjectWebSocket(path)
self._notification_stream = Controller.instance().httpClient().connectWebSocket(self._websocket, path)
self._notification_stream.textMessageReceived.connect(self._websocket_event_received)
self._notification_stream.error.connect(self._websocket_error)
@@ -701,7 +700,7 @@ class Project(QtCore.QObject):
elif result["action"] == "project.updated":
self._projectUpdatedCallback(result["event"])
elif result["action"] == "snapshot.restored":
Topology.instance().createLoadProject({"project_id": result["event"]["project_id"]})
Topology.instance().restoreSnapshot(result["event"]["project_id"])
elif result["action"] == "log.error":
log.error(result["event"]["message"])
elif result["action"] == "log.warning":

View File

@@ -24,7 +24,6 @@ Compatibility layer for Qt bindings, so it is easier to switch to PySide if need
import sys
import sip
import os
import re
import inspect
@@ -33,12 +32,17 @@ import functools
import logging
log = logging.getLogger("qt/__init__.py")
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, Qt
try:
from PyQt5 import sip
except ImportError:
import sip
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets
sys.modules[__name__ + '.QtCore'] = QtCore
sys.modules[__name__ + '.QtGui'] = QtGui
sys.modules[__name__ + '.QtNetwork'] = QtNetwork
sys.modules[__name__ + '.QtWidgets'] = QtWidgets
sys.modules[__name__ + '.Qt'] = Qt
sys.modules[__name__ + '.sip'] = sip
try:
from PyQt5 import QtSvg
@@ -115,14 +119,20 @@ class LogQMessageBox(QtWidgets.QMessageBox):
if message.startswith("QXcbConnection"): # Qt noise not relevant
return
LogQMessageBox._get_logger().critical(re.sub(r"<[^<]+?>", "", message), stack_info=LogQMessageBox.stack_info())
if sip_is_deleted(parent):
if parent is False:
# special case to display a QMessageBox before the main window is created.
parent = None
elif sip_is_deleted(parent):
return
return super(QtWidgets.QMessageBox, QtWidgets.QMessageBox).critical(parent, title, message, *args)
@staticmethod
def warning(parent, title, message, *args):
LogQMessageBox._get_logger().warning(re.sub(r"<[^<]+?>", "", message))
if sip_is_deleted(parent):
if parent is False:
# special case to display a QMessageBox before the main window is created.
parent = None
elif sip_is_deleted(parent):
return
return super(QtWidgets.QMessageBox, QtWidgets.QMessageBox).warning(parent, title, message, *args)
@@ -130,7 +140,7 @@ class LogQMessageBox(QtWidgets.QMessageBox):
def _get_logger():
"""
Return a logger in the context of the caller
in order to have the correct informations in the log
in order to have the correct information in the log
"""
if sys.version_info < (3, 5):
return logging.getLogger('qt')

View File

@@ -150,9 +150,7 @@ class Appliance(collections.Mapping):
for image_type, image in version["images"].items():
image["type"] = image_type
img = self._registry.search_image_file(
self.emulator(), image["filename"], image.get("md5sum"), image.get("filesize")
)
img = self._registry.search_image_file(self.emulator(), image["filename"], image.get("md5sum"), image.get("filesize"))
if img is None:
if "md5sum" in image:
raise ApplianceError("File {} with checksum {} not found for {}".format(image["filename"], image["md5sum"], appliance["name"]))

View File

@@ -17,10 +17,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import urllib.request
import shutil
from ssl import CertificateError
from ..qt import QtCore, QtWidgets, QtNetwork
from ..controller import Controller
from .config import Config, ConfigException
@@ -33,7 +33,7 @@ class ApplianceToTemplate:
Appliance installation.
"""
def new_template(self, appliance_config, server, controller_symbols=None):
def new_template(self, appliance_config, server, controller_symbols=None, parent=None):
"""
Creates a new template from an appliance.
@@ -41,6 +41,7 @@ class ApplianceToTemplate:
:param server
"""
self._parent = parent
new_template = {
"compute_id": server,
"name": appliance_config["name"]
@@ -99,8 +100,6 @@ class ApplianceToTemplate:
new_config.pop("arch", None)
options = appliance_config["qemu"].get("options", "")
if "-nographic" not in options:
options += " -nographic"
if appliance_config["qemu"].get("kvm", "allow") == "disable" and "-no-kvm" not in options:
options += " -no-kvm"
new_config["options"] = options.strip()
@@ -170,37 +169,66 @@ class ApplianceToTemplate:
return os.path.basename(path)
def _set_symbol(self, symbol, controller_symbols):
def _set_symbol(self, symbol_id, controller_symbols):
"""
Check if exists on controller or download symbol from the web if needed
"""
# GNS3 builtin symbol
if symbol.startswith(":/symbols/"):
return symbol
if symbol_id.startswith(":/symbols/"):
return symbol_id
path = os.path.join(Config().symbols_dir, symbol)
path = os.path.join(Config().symbols_dir, symbol_id)
if os.path.exists(path):
return os.path.basename(path)
if controller_symbols:
is_symbol_on_controller = len([s for s in controller_symbols
if s['symbol_id'] == symbol]) > 0
is_symbol_on_controller = len([s for s in controller_symbols if s['symbol_id'] == symbol_id]) > 0
if is_symbol_on_controller:
cached = Controller.instance().getStaticCachedPath(symbol)
cached = Controller.instance().getStaticCachedPath(symbol_id)
if os.path.exists(cached):
try:
shutil.copy(cached, path)
except IOError as e:
log.warning("Cannot copy cached symbol from `{}` to `{}` due `{}`".format(
cached, path, str(e)
))
return symbol
log.warning("Cannot copy cached symbol from `{}` to `{}` due `{}`".format(cached, path, e))
return symbol_id
url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol)
url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol_id)
try:
urllib.request.urlretrieve(url, path)
self._downloadApplianceSymbol(url, path)
controller = Controller.instance()
controller.clearStaticCache()
if controller.isRemote():
controller.uploadSymbol(symbol_id, path)
return os.path.basename(path)
except (OSError, CertificateError):
return None
def _downloadApplianceSymbol(self, url, path, timeout=30):
"""
Download an appliance symbol in a synchronous way.
"""
network_manager = QtNetwork.QNetworkAccessManager()
request = QtNetwork.QNetworkRequest(QtCore.QUrl(url))
request.setRawHeader(b'User-Agent', b'GNS3 symbol downloader')
reply = network_manager.get(request)
progress_dialog = QtWidgets.QProgressDialog("Downloading '{}' appliance symbol...".format(os.path.basename(path)), "Cancel", 0, 0, self._parent)
progress_dialog.setMinimumDuration(0)
reply.finished.connect(progress_dialog.close)
QtCore.QTimer.singleShot(timeout * 1000, progress_dialog.close)
log.debug("Downloading appliance symbol from '{}'".format(url))
progress_dialog.show()
progress_dialog.exec_()
status = reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
if reply.error() == QtNetwork.QNetworkReply.NoError and status == 200:
try:
with open(path, 'wb+') as f:
f.write(reply.readAll())
except OSError as e:
log.debug("Error while saving appliance symbol to '{}': {}".format(path, e))
raise
log.debug("Appliance symbol downloaded and saved to '{}'".format(path))
else:
log.warning("Error when downloading appliance symbol from '{}': {}".format(url, reply.errorString()))

View File

@@ -38,7 +38,7 @@ class Image:
self._location = "local"
self._emulator = emulator
self.path = path
self._path = path
if filename is None:
self._filename = os.path.basename(self.path)
else:
@@ -66,6 +66,13 @@ class Image:
"""
return self._filename
@property
def path(self):
"""
:returns: Image path
"""
return self._path
@property
def version(self):
"""
@@ -90,12 +97,12 @@ class Image:
"""
if self._md5sum is None:
from_cache = Image._cache.get(self.path)
from_cache = Image._cache.get(self._path)
if from_cache:
self._md5sum = from_cache
return self._md5sum
md5_file = self.path + ".md5sum"
md5_file = self._path + ".md5sum"
if os.path.exists(md5_file):
try:
with open(md5_file) as f:
@@ -105,20 +112,20 @@ class Image:
log.debug("Could not read '{}': {}".format(md5_file, e))
try:
if not os.path.isfile(self.path):
if not os.path.isfile(self._path):
return None
m = hashlib.md5()
with open(self._path, "rb") as f:
while True:
buf = f.read(4096)
if not buf:
break
m.update(buf)
except (OSError, PermissionError) as e:
log.debug("Cannot access '{}': {}".format(self.path, e))
log.debug("Cannot access '{}': {}".format(self._path, e))
return None
m = hashlib.md5()
with open(self.path, "rb") as f:
while True:
buf = f.read(4096)
if not buf:
break
m.update(buf)
self._md5sum = m.hexdigest()
Image._cache[self.path] = self._md5sum
Image._cache[self._path] = self._md5sum
return self._md5sum
@md5sum.setter
@@ -133,7 +140,7 @@ class Image:
if self._filesize is not None:
return self._filesize
try:
self._filesize = os.path.getsize(self.path)
self._filesize = os.path.getsize(self._path)
return self._filesize
except OSError:
return 0

View File

@@ -87,12 +87,12 @@ class Registry(QtCore.QObject):
return remote_image
for directory in self._images_dirs:
log.debug("Search images %s (%s) in %s", filename, md5sum, directory)
log.debug("Search image {} (MD5={} SIZE={}) in '{}'".format(filename, md5sum, size, directory))
if os.path.exists(directory):
for file in os.listdir(directory):
if not file.endswith(".md5sum") and not file.startswith("."):
path = os.path.join(directory, file)
try:
try:
for file in os.listdir(directory):
if not file.endswith(".md5sum") and not file.startswith("."):
path = os.path.join(directory, file)
if os.path.isfile(path):
if md5sum is None or strict_md5_check is False:
if filename == os.path.basename(path):
@@ -104,9 +104,9 @@ class Registry(QtCore.QObject):
if size is None or (file_size - 10 < size and file_size + 10 > size):
image = Image(emulator, path)
if image.md5sum == md5sum:
log.debug("Found images %s (%s) in %s", filename, md5sum, image.path)
log.debug("Found image {} (MD5={}) in {}".format(filename, md5sum, image.path))
return image
except (OSError, PermissionError) as e:
log.error("Cannot scan {}: {}".format(path, e))
except (OSError, PermissionError) as e:
log.error("Cannot scan {}: {}".format(path, e))
return None

View File

@@ -166,6 +166,13 @@
"extra_hosts": {
"description": "Hosts which will be written to /etc/hosts into container" ,
"type": "string"
},
"extra_volumes": {
"description": "Additional directories to make persistent that are not included in the images VOLUME directive" ,
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [

View File

@@ -40,7 +40,7 @@ DEFAULT_CONFIGS_PATH = os.path.normpath(os.path.expanduser("~/GNS3/configs"))
# Default appliances location
DEFAULT_APPLIANCES_PATH = os.path.normpath(os.path.expanduser("~/GNS3/appliances"))
DEFAULT_LOCAL_SERVER_HOST = "127.0.0.1"
DEFAULT_LOCAL_SERVER_HOST = "localhost"
DEFAULT_LOCAL_SERVER_PORT = 3080
DEFAULT_DELAY_CONSOLE_ALL = 500
@@ -55,7 +55,8 @@ if sys.platform.startswith("win"):
# windows 32-bit
program_files_x86 = program_files = os.environ["PROGRAMFILES"]
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {'Putty (included with GNS3)': 'putty.exe -telnet %h %p -wt "%d" -gns3 5 -skin 4',
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {'Putty (normal standalone version)': 'putty_standalone.exe -telnet %h %p -loghost "%d"',
'Putty (custom deprecated version)': 'putty.exe -telnet %h %p -wt "%d" -gns3 5 -skin 4',
'MobaXterm': r'"{}\Mobatek\MobaXterm Personal Edition\MobaXterm.exe" -newtab "telnet %h %p"'.format(program_files_x86),
'Royal TS': r'{}\code4ward.net\Royal TS V3\RTS3App.exe /connectadhoc:%h /adhoctype:terminal /p:IsTelnetConnection="true" /p:ConnectionType="telnet;Telnet Connection" /p:Port="%p" /p:Name="%d"'.format(program_files),
'SuperPutty': r'SuperPutty.exe -telnet "%h -P %p -wt \"%d\""',
@@ -75,7 +76,7 @@ if sys.platform.startswith("win"):
DEFAULT_DELAY_CONSOLE_ALL = 1500
else:
PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Solar-Putty (included with GNS3 downloaded from gns3.com)"] = 'Solar-PuTTY.exe --telnet --hostname %h --port %p --name "%d"'
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Putty (included with GNS3)"]
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Putty (normal standalone version)"]
elif sys.platform.startswith("darwin"):
# Mac OS X
@@ -260,6 +261,14 @@ if sys.platform.startswith("win"):
STYLES = ["Charcoal", "Classic", "Legacy"]
SYMBOL_THEMES = ["Classic",
"Affinity-square-blue",
"Affinity-square-gray",
"Affinity-square-red",
"Affinity-circle-blue",
"Affinity-circle-gray",
"Affinity-circle-red"]
if sys.platform.startswith("win"):
DEFAULT_STYLE = "Classic"
else:
@@ -288,7 +297,8 @@ GENERAL_SETTINGS = {
"debug_level": 0,
"multi_profiles": False,
"hdpi": not sys.platform.startswith("linux"),
"direct_file_upload": False
"direct_file_upload": False,
"symbol_theme": "Classic"
}
NODES_VIEW_SETTINGS = {
@@ -320,7 +330,7 @@ GRAPHICS_VIEW_SETTINGS = {
LOCAL_SERVER_SETTINGS = {
"path": "gns3server",
"ubridge_path": "ubridge",
"host": None,
"host": "localhost",
"port": DEFAULT_LOCAL_SERVER_PORT,
"images_path": DEFAULT_IMAGES_PATH,
"projects_path": DEFAULT_PROJECTS_PATH,

View File

@@ -38,7 +38,7 @@ def spiceConsole(host, port, command):
"""
if len(command.strip(' ')) == 0:
log.warning('SPICE client is not configured')
log.error("SPICE client is not configured")
return
# ipv6 support
@@ -50,7 +50,7 @@ def spiceConsole(host, port, command):
command = command.replace("%p", str(port))
try:
log.info('starting SPICE program "{}"'.format(command))
log.debug('starting SPICE program "{}"'.format(command))
if sys.platform.startswith("win"):
# use the string on Windows
subprocess.Popen(command)
@@ -59,5 +59,4 @@ def spiceConsole(host, port, command):
args = shlex.split(command)
subprocess.Popen(args, env=os.environ)
except (OSError, ValueError, subprocess.SubprocessError) as e:
log.warning('could not start SPICE program "{}": {}'.format(command, e))
raise
log.error("Could not start SPICE program with command '{}': {}".format(command, e))

View File

@@ -76,7 +76,6 @@ class Style:
self._mw.uiDrawRectangleAction.setIcon(self._getStyleIcon(":/icons/rectangle.svg", ":/icons/rectangle-hover.svg"))
self._mw.uiDrawEllipseAction.setIcon(self._getStyleIcon(":/icons/ellipse.svg", ":/icons/ellipse-hover.svg"))
self._mw.uiDrawLineAction.setIcon(QtGui.QIcon(":/icons/vertically.svg"))
self._mw.uiEditReadmeAction.setIcon(QtGui.QIcon(":/icons/edit.svg"))
self._mw.uiOnlineHelpAction.setIcon(QtGui.QIcon(":/icons/help.svg"))
self._mw.uiBrowseRoutersAction.setIcon(self._getStyleIcon(":/icons/router.png", ":/icons/router-hover.png"))
self._mw.uiBrowseSwitchesAction.setIcon(self._getStyleIcon(":/icons/switch.png", ":/icons/switch-hover.png"))
@@ -128,7 +127,6 @@ class Style:
self._mw.uiDrawRectangleAction.setIcon(self._getStyleIcon(":/classic_icons/rectangle.svg", ":/classic_icons/rectangle-hover.svg"))
self._mw.uiDrawEllipseAction.setIcon(self._getStyleIcon(":/classic_icons/ellipse.svg", ":/classic_icons/ellipse-hover.svg"))
self._mw.uiDrawLineAction.setIcon(self._getStyleIcon(":/classic_icons/line.svg", ":/classic_icons/line-hover.svg"))
self._mw.uiEditReadmeAction.setIcon(self._getStyleIcon(":/classic_icons/edit.svg", ":/classic_icons/edit-hover.svg"))
self._mw.uiOnlineHelpAction.setIcon(self._getStyleIcon(":/classic_icons/help.svg", ":/classic_icons/help-hover.svg"))
self._mw.uiBrowseRoutersAction.setIcon(self._getStyleIcon(":/classic_icons/router.svg", ":/classic_icons/router-hover.svg"))
self._mw.uiBrowseSwitchesAction.setIcon(self._getStyleIcon(":/classic_icons/switch.svg", ":/classic_icons/switch-hover.svg"))
@@ -190,7 +188,6 @@ class Style:
self._mw.uiDrawRectangleAction.setIcon(self._getStyleIcon(":/charcoal_icons/rectangle.svg", ":/charcoal_icons/rectangle-hover.svg"))
self._mw.uiDrawEllipseAction.setIcon(self._getStyleIcon(":/charcoal_icons/ellipse.svg", ":/charcoal_icons/ellipse-hover.svg"))
self._mw.uiDrawLineAction.setIcon(self._getStyleIcon(":/charcoal_icons/line.svg", ":/charcoal_icons/line-hover.svg"))
self._mw.uiEditReadmeAction.setIcon(self._getStyleIcon(":/charcoal_icons/edit.svg", ":/charcoal_icons/edit-hover.svg"))
self._mw.uiOnlineHelpAction.setIcon(self._getStyleIcon(":/charcoal_icons/help.svg", ":/charcoal_icons/help-hover.svg"))
self._mw.uiBrowseRoutersAction.setIcon(self._getStyleIcon(":/charcoal_icons/router.svg", ":/charcoal_icons/router-hover.svg"))
self._mw.uiBrowseSwitchesAction.setIcon(self._getStyleIcon(":/charcoal_icons/switch.svg", ":/charcoal_icons/switch-hover.svg"))

View File

@@ -19,10 +19,11 @@ import urllib.parse
class Symbol:
def __init__(self, symbol_id=None, builtin=False, filename=None):
def __init__(self, symbol_id=None, builtin=False, filename=None, theme=None):
self._id = symbol_id
self._builtin = builtin
self._filename = filename
self._theme = theme
def id(self):
return self._id
@@ -33,6 +34,9 @@ class Symbol:
def builtin(self):
return self._builtin
def theme(self):
return self._theme
def url(self):
return urllib.parse.quote("/symbols/" + self._id + "/raw")

View File

@@ -30,6 +30,10 @@ class Template:
settings["template_id"] = str(uuid.uuid4())
self._settings = copy.deepcopy(settings)
# The "appliance_id" setting has been replaced by "template_id" setting in version 2.2
if "appliance_id" in self._settings:
self._settings["template_id"] = self._settings.pop("appliance_id")
# The "node_type" setting has been replaced by "template_type" setting in version 2.2
if "node_type" in self._settings:
self._settings["template_type"] = self._settings.pop("node_type")
@@ -38,6 +42,11 @@ class Template:
if "server" in self._settings:
self._settings["compute_id"] = self._settings.pop("server")
for setting in self._settings.copy():
# remove deprecated settings
if setting in ["enable_remote_console", "use_ubridge", "acpi_shutdown", "default_symbol", "hover_symbol"]:
del self._settings[setting]
def id(self):
"""
Returns the template ID.

View File

@@ -213,6 +213,7 @@ class TemplateManager(QtCore.QObject):
self._controller.post("/projects/{project_id}/templates/{template_id}".format(project_id=project.id(), template_id=template_id),
self._createNodeFromTemplateCallback,
params,
showProgress=False,
timeout=None)
return True

View File

@@ -27,9 +27,8 @@ from .local_server import LocalServer
from .qt import QtCore, QtWidgets
from .utils.progress_dialog import ProgressDialog
from .utils.export_project_worker import ExportProjectWorker
from .utils.import_project_worker import ImportProjectWorker
from .dialogs.file_editor_dialog import FileEditorDialog
from .dialogs.project_export_wizard import ExportProjectWizard
from .dialogs.project_welcome_dialog import ProjectWelcomeDialog
from .modules import MODULES
@@ -110,14 +109,14 @@ class Topology(QtCore.QObject):
return self._project
def setProject(self, project):
def setProject(self, project, snapshot=False):
"""
Set current project
:param project: Project instance
"""
if self._project:
if self._project and snapshot is False:
# Assert to detect when we create a new project object for the same project
assert project is None or (project != self._project and project.id != self._project.id)
self._project.stopListenNotifications()
@@ -135,7 +134,6 @@ class Topology(QtCore.QObject):
self.project_changed_signal.emit()
def _projectUpdatedSlot(self):
if not self._project or not self._project.filesDir() or not self._project.filename():
return
@@ -194,6 +192,7 @@ class Topology(QtCore.QObject):
"""
Create load a project based on settings, not on the .gns3
"""
self.setProject(None)
from .project import Project
project = Project()
@@ -216,6 +215,17 @@ class Topology(QtCore.QObject):
self._main_window.uiStatusBar.showMessage("Project created", 2000)
return project
def restoreSnapshot(self, project_id):
"""
Restore a snapshot for a given project.
"""
assert self._project.id() == project_id
project = self._project
self.setProject(project, snapshot=True)
project.load()
self._main_window.uiStatusBar.showMessage("Snapshot restored", 2000)
def loadProject(self, path):
"""
Loads a project into GNS3.
@@ -233,13 +243,6 @@ class Topology(QtCore.QObject):
self._main_window.uiStatusBar.showMessage("Project loaded {}".format(path), 2000)
return True
def editReadme(self):
if self.project() is None:
return
dialog = FileEditorDialog(self.project(), "/README.txt", parent=self._main_window, default="Project title\n\nAuthor: Grace Hopper <grace@example.org>\n\nThis project is about...")
dialog.show()
dialog.exec_()
def _projectCreationErrorSlot(self, message):
if self._project:
self._project.project_creation_error_signal.disconnect(self._projectCreationErrorSlot)
@@ -247,41 +250,12 @@ class Topology(QtCore.QObject):
QtWidgets.QMessageBox.critical(self._main_window, "New project", message)
def exportProject(self):
include_image_question = """Would you like to include any base image?
The project will not require additional images to run on another host, however the resulting file will be much bigger.
It is your responsability to check if you have the right to distribute the image(s) as part of the project.
"""
reply = QtWidgets.QMessageBox.question(self._main_window, "Export project", include_image_question,
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No)
include_images = int(reply == QtWidgets.QMessageBox.Yes)
directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
if len(directory) == 0:
directory = self.projectsDirPath()
path, _ = QtWidgets.QFileDialog.getSaveFileName(self._main_window, "Export portable project", directory,
"GNS3 Portable Project (*.gns3project *.gns3p)",
"GNS3 Portable Project (*.gns3project *.gns3p)")
if path is None or len(path) == 0:
if self._project is None:
QtWidgets.QMessageBox.critical(self._main_window, "Export project", "No project has been opened")
return
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._main_window, "Export project", "Could not write {}: {}".format(path, e))
return
self.editReadme()
export_worker = ExportProjectWorker(self._project, path, include_images)
progress_dialog = ProgressDialog(export_worker, "Exporting project", "Exporting portable project files...", "Cancel", parent=self._main_window, create_thread=False)
progress_dialog.show()
progress_dialog.exec_()
export_wizard = ExportProjectWizard(self.project(), parent=self._main_window)
export_wizard.show()
export_wizard.exec_()
def importProject(self, project_file):
from .dialogs.project_dialog import ProjectDialog
@@ -541,7 +515,7 @@ It is your responsability to check if you have the right to distribute the image
node = node_module.instantiateNode(node_class, ComputeManager.instance().getCompute(node_data["compute_id"]), self._project)
node.createNodeCallback(node_data)
self._main_window.uiGraphicsView.createNodeItem(node, node_data["symbol"], node_data["x"], node_data["y"], node_data.get("z", 1))
self._main_window.uiGraphicsView.createNodeItem(node, node_data["symbol"], node_data["x"], node_data["y"])
def createLink(self, link_data):
source_port = None
@@ -597,7 +571,7 @@ It is your responsability to check if you have the right to distribute the image
except IndexError:
# If unknow we render it as a raw SVG image
type = "image"
self._main_window.uiGraphicsView.createDrawingItem(type, drawing_data["x"], drawing_data["y"], drawing_data["z"], rotation=drawing_data["rotation"], drawing_id=drawing_data["drawing_id"], svg=drawing_data["svg"])
self._main_window.uiGraphicsView.createDrawingItem(type, drawing_data["x"], drawing_data["y"], drawing_data["z"], locked=drawing_data["locked"], rotation=drawing_data["rotation"], drawing_id=drawing_data["drawing_id"], svg=drawing_data["svg"])
@staticmethod
def instance():

View File

@@ -67,13 +67,13 @@
<item row="4" column="1">
<widget class="QSpinBox" name="uiNodeGridSizeSpinBox">
<property name="minimum">
<number>10</number>
<number>5</number>
</property>
<property name="maximum">
<number>150</number>
</property>
<property name="singleStep">
<number>10</number>
<number>5</number>
</property>
<property name="value">
<number>75</number>
@@ -83,13 +83,13 @@
<item row="5" column="1">
<widget class="QSpinBox" name="uiDrawingGridSizeSpinBox">
<property name="minimum">
<number>10</number>
<number>5</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="singleStep">
<number>10</number>
<number>5</number>
</property>
<property name="value">
<number>25</number>
@@ -179,6 +179,17 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>uiProjectNameLineEdit</tabstop>
<tabstop>uiSceneWidthSpinBox</tabstop>
<tabstop>uiSceneHeightSpinBox</tabstop>
<tabstop>uiNodeGridSizeSpinBox</tabstop>
<tabstop>uiDrawingGridSizeSpinBox</tabstop>
<tabstop>uiProjectAutoOpenCheckBox</tabstop>
<tabstop>uiProjectAutoStartCheckBox</tabstop>
<tabstop>uiProjectAutoCloseCheckBox</tabstop>
</tabstops>
<resources/>
<connections>
<connection>

View File

@@ -37,16 +37,16 @@ class Ui_EditProjectDialog(object):
self.uiSceneWidthSpinBox.setObjectName("uiSceneWidthSpinBox")
self.uiGeneralGrid.addWidget(self.uiSceneWidthSpinBox, 2, 1, 1, 1)
self.uiNodeGridSizeSpinBox = QtWidgets.QSpinBox(self.uiGeneralTab)
self.uiNodeGridSizeSpinBox.setMinimum(10)
self.uiNodeGridSizeSpinBox.setMinimum(5)
self.uiNodeGridSizeSpinBox.setMaximum(150)
self.uiNodeGridSizeSpinBox.setSingleStep(10)
self.uiNodeGridSizeSpinBox.setSingleStep(5)
self.uiNodeGridSizeSpinBox.setProperty("value", 75)
self.uiNodeGridSizeSpinBox.setObjectName("uiNodeGridSizeSpinBox")
self.uiGeneralGrid.addWidget(self.uiNodeGridSizeSpinBox, 4, 1, 1, 1)
self.uiDrawingGridSizeSpinBox = QtWidgets.QSpinBox(self.uiGeneralTab)
self.uiDrawingGridSizeSpinBox.setMinimum(10)
self.uiDrawingGridSizeSpinBox.setMinimum(5)
self.uiDrawingGridSizeSpinBox.setMaximum(100)
self.uiDrawingGridSizeSpinBox.setSingleStep(10)
self.uiDrawingGridSizeSpinBox.setSingleStep(5)
self.uiDrawingGridSizeSpinBox.setProperty("value", 25)
self.uiDrawingGridSizeSpinBox.setObjectName("uiDrawingGridSizeSpinBox")
self.uiGeneralGrid.addWidget(self.uiDrawingGridSizeSpinBox, 5, 1, 1, 1)

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ExportProjectWizard</class>
<widget class="QWizard" name="ExportProjectWizard">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Export project</string>
</property>
<property name="options">
<set>QWizard::HaveHelpButton</set>
</property>
<widget class="QWizardPage" name="uiExportOptionsWizardPage">
<property name="title">
<string>Export project</string>
</property>
<property name="subTitle">
<string>Please select the location, whether to include base images or not and the compression type.</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="uiPathLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Path:</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="uiPathLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="uiPathBrowserToolButton">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="uiCompressionLabel">
<property name="text">
<string>Compression:</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QComboBox" name="uiCompressionComboBox"/>
</item>
<item row="2" column="0" colspan="3">
<widget class="QCheckBox" name="uiIncludeImagesCheckBox">
<property name="text">
<string>Include base images</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="uiIncludeSnapshotsCheckBox">
<property name="text">
<string>Include snapshots</string>
</property>
</widget>
</item>
<item row="4" column="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>247</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWizardPage" name="uiProjectReadmeWizardPage">
<property name="title">
<string>Readme file</string>
</property>
<property name="subTitle">
<string>Write a summary of the project.</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTextEdit" name="uiReadmeTextEdit">
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'.SF NS Text'; font-size:13pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/export_project_wizard.ui'
#
# Created by: PyQt5 UI code generator 5.9
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_ExportProjectWizard(object):
def setupUi(self, ExportProjectWizard):
ExportProjectWizard.setObjectName("ExportProjectWizard")
ExportProjectWizard.setWindowModality(QtCore.Qt.ApplicationModal)
ExportProjectWizard.resize(900, 600)
ExportProjectWizard.setOptions(QtWidgets.QWizard.HaveHelpButton)
self.uiExportOptionsWizardPage = QtWidgets.QWizardPage()
self.uiExportOptionsWizardPage.setObjectName("uiExportOptionsWizardPage")
self.gridLayout = QtWidgets.QGridLayout(self.uiExportOptionsWizardPage)
self.gridLayout.setObjectName("gridLayout")
self.uiPathLabel = QtWidgets.QLabel(self.uiExportOptionsWizardPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiPathLabel.sizePolicy().hasHeightForWidth())
self.uiPathLabel.setSizePolicy(sizePolicy)
self.uiPathLabel.setObjectName("uiPathLabel")
self.gridLayout.addWidget(self.uiPathLabel, 0, 0, 1, 1)
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.uiPathLineEdit = QtWidgets.QLineEdit(self.uiExportOptionsWizardPage)
self.uiPathLineEdit.setObjectName("uiPathLineEdit")
self.horizontalLayout_3.addWidget(self.uiPathLineEdit)
self.uiPathBrowserToolButton = QtWidgets.QToolButton(self.uiExportOptionsWizardPage)
self.uiPathBrowserToolButton.setObjectName("uiPathBrowserToolButton")
self.horizontalLayout_3.addWidget(self.uiPathBrowserToolButton)
self.gridLayout.addLayout(self.horizontalLayout_3, 0, 1, 1, 2)
self.uiCompressionLabel = QtWidgets.QLabel(self.uiExportOptionsWizardPage)
self.uiCompressionLabel.setObjectName("uiCompressionLabel")
self.gridLayout.addWidget(self.uiCompressionLabel, 1, 0, 1, 1)
self.uiCompressionComboBox = QtWidgets.QComboBox(self.uiExportOptionsWizardPage)
self.uiCompressionComboBox.setObjectName("uiCompressionComboBox")
self.gridLayout.addWidget(self.uiCompressionComboBox, 1, 1, 1, 2)
self.uiIncludeImagesCheckBox = QtWidgets.QCheckBox(self.uiExportOptionsWizardPage)
self.uiIncludeImagesCheckBox.setObjectName("uiIncludeImagesCheckBox")
self.gridLayout.addWidget(self.uiIncludeImagesCheckBox, 2, 0, 1, 3)
self.uiIncludeSnapshotsCheckBox = QtWidgets.QCheckBox(self.uiExportOptionsWizardPage)
self.uiIncludeSnapshotsCheckBox.setObjectName("uiIncludeSnapshotsCheckBox")
self.gridLayout.addWidget(self.uiIncludeSnapshotsCheckBox, 3, 0, 1, 2)
spacerItem = QtWidgets.QSpacerItem(20, 247, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem, 4, 2, 1, 1)
ExportProjectWizard.addPage(self.uiExportOptionsWizardPage)
self.uiProjectReadmeWizardPage = QtWidgets.QWizardPage()
self.uiProjectReadmeWizardPage.setObjectName("uiProjectReadmeWizardPage")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.uiProjectReadmeWizardPage)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.uiReadmeTextEdit = QtWidgets.QTextEdit(self.uiProjectReadmeWizardPage)
self.uiReadmeTextEdit.setObjectName("uiReadmeTextEdit")
self.verticalLayout_2.addWidget(self.uiReadmeTextEdit)
ExportProjectWizard.addPage(self.uiProjectReadmeWizardPage)
self.retranslateUi(ExportProjectWizard)
QtCore.QMetaObject.connectSlotsByName(ExportProjectWizard)
def retranslateUi(self, ExportProjectWizard):
_translate = QtCore.QCoreApplication.translate
ExportProjectWizard.setWindowTitle(_translate("ExportProjectWizard", "Export project"))
self.uiExportOptionsWizardPage.setTitle(_translate("ExportProjectWizard", "Export project"))
self.uiExportOptionsWizardPage.setSubTitle(_translate("ExportProjectWizard", "Please select the location, whether to include base images or not and the compression type."))
self.uiPathLabel.setText(_translate("ExportProjectWizard", "Path:"))
self.uiPathBrowserToolButton.setText(_translate("ExportProjectWizard", "Browse..."))
self.uiCompressionLabel.setText(_translate("ExportProjectWizard", "Compression:"))
self.uiIncludeImagesCheckBox.setText(_translate("ExportProjectWizard", "Include base images"))
self.uiIncludeSnapshotsCheckBox.setText(_translate("ExportProjectWizard", "Include snapshots"))
self.uiProjectReadmeWizardPage.setTitle(_translate("ExportProjectWizard", "Readme file"))
self.uiProjectReadmeWizardPage.setSubTitle(_translate("ExportProjectWizard", "Write a summary of the project."))
self.uiReadmeTextEdit.setHtml(_translate("ExportProjectWizard", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Ubuntu\'; font-size:11pt; font-weight:400; font-style:normal;\">\n"
"<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:\'.SF NS Text\'; font-size:13pt;\"><br /></p></body></html>"))

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1324</width>
<height>1038</height>
<width>931</width>
<height>878</height>
</rect>
</property>
<property name="windowTitle">
@@ -30,124 +30,93 @@
<string>General</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="margin">
<number>10</number>
</property>
<item>
<widget class="QGroupBox" name="uiLocalPathsGroupBox">
<property name="title">
<string>Local paths</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="uiProjectsPathLabel">
<property name="text">
<string>My projects:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="uiProjectsPathLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Directory where your GNS3 projects are stored</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="uiProjectsPathToolButton">
<property name="text">
<string>&amp;Browse...</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum>
</property>
</widget>
</item>
</layout>
<item row="1" column="0">
<widget class="QLineEdit" name="uiProjectsPathLineEdit">
<property name="toolTip">
<string>Directory where your GNS3 projects are stored</string>
</property>
</widget>
</item>
<item>
<item row="1" column="1">
<widget class="QToolButton" name="uiProjectsPathToolButton">
<property name="text">
<string>&amp;Browse...</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>My symbols:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="uiSymbolsPathLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="uiSymbolsPathToolButton">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
<item row="3" column="0">
<widget class="QLineEdit" name="uiSymbolsPathLineEdit"/>
</item>
<item>
<item row="3" column="1">
<widget class="QToolButton" name="uiSymbolsPathToolButton">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="uiConfigsPathLabel">
<property name="text">
<string>My configs:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLineEdit" name="uiConfigsPathLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Directory where your binary images (e.g. IOS) are stored</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="uiConfigsPathToolButton">
<property name="text">
<string>&amp;Browse...</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum>
</property>
</widget>
</item>
</layout>
<item row="5" column="0">
<widget class="QLineEdit" name="uiConfigsPathLineEdit">
<property name="toolTip">
<string>Directory where your binary images (e.g. IOS) are stored</string>
</property>
</widget>
</item>
<item>
<item row="5" column="1">
<widget class="QToolButton" name="uiConfigsPathToolButton">
<property name="text">
<string>&amp;Browse...</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>My custom appliances:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QLineEdit" name="uiAppliancesPathLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="uiAppliancesPathToolButton">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
<item row="7" column="0">
<widget class="QLineEdit" name="uiAppliancesPathLineEdit"/>
</item>
<item row="7" column="1">
<widget class="QToolButton" name="uiAppliancesPathToolButton">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</widget>
@@ -155,7 +124,7 @@
<item>
<widget class="QGroupBox" name="uiStyleGroupBox">
<property name="title">
<string>Style</string>
<string>Interface style</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
@@ -164,6 +133,22 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="uiSymbolThemeGroupBox">
<property name="title">
<string>Symbol theme for new templates</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QComboBox" name="uiSymbolThemeComboBox">
<property name="toolTip">
<string>Symbol theme support only works when adding a new template using the recommended method in the template wizard.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="uiConfigurationFileGroupBox">
<property name="title">
@@ -219,14 +204,14 @@
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<spacer name="verticalSpacer_8">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
<height>40</height>
</size>
</property>
</spacer>
@@ -238,7 +223,16 @@
<string>Binary images</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -290,6 +284,9 @@
</item>
<item>
<widget class="QListWidget" name="uiImageDirectoriesListWidget">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
@@ -365,6 +362,19 @@
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_7">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="uiConsoleTab">
@@ -372,7 +382,16 @@
<string>Console applications</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -483,7 +502,16 @@
<string>VNC</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -651,143 +679,28 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="uiSceneHeightLabel">
<property name="text">
<string>Default height:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="uiNodeGridSizeLabel">
<property name="text">
<string>Default node grid size:</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QLabel" name="uiDrawingGridSizeLabel">
<property name="text">
<string>Default drawing (e.g. labels and rectangles) grid size:</string>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="uiRectangleSelectedItemCheckBox">
<property name="text">
<string>Draw a rectangle when an item is selected</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QCheckBox" name="uiDrawLinkStatusPointsCheckBox">
<property name="text">
<string>Draw link status points</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QCheckBox" name="uiShowInterfaceLabelsOnNewProject">
<property name="text">
<string>Show interface labels on new project</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QCheckBox" name="uiShowGridOnNewProject">
<property name="text">
<string>Show grid on new project</string>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QCheckBox" name="uiSnapToGridOnNewProject">
<property name="text">
<string>Snap to grid on new project</string>
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QCheckBox" name="uiLimitSizeNodeSymbolCheckBox">
<property name="text">
<string>Limit the size of node symbols</string>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel" name="uiLabelPreviewLabel">
<property name="text">
<string>Default label style:</string>
</property>
</widget>
</item>
<item row="15" column="0" colspan="2">
<widget class="QPlainTextEdit" name="uiDefaultLabelStylePlainTextEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>50</height>
</size>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="plainText">
<string>AaBbYyZz</string>
</property>
</widget>
</item>
<item row="16" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QPushButton" name="uiDefaultLabelFontPushButton">
<property name="text">
<string>&amp;Select default font</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDefaultLabelColorPushButton">
<property name="text">
<string>&amp;Select default color</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="17" column="0">
<widget class="QLabel" name="uiNotePreviewLabel">
<property name="text">
<string>Default note style:</string>
</property>
</widget>
</item>
<item row="18" column="0" colspan="2">
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="uiShowInterfaceLabelsOnNewProject">
<property name="text">
<string>Show interface labels on new project</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="uiSceneHeightLabel">
<property name="text">
<string>Default height:</string>
</property>
</widget>
</item>
<item row="14" column="0" colspan="3">
<widget class="QPlainTextEdit" name="uiDefaultNoteStylePlainTextEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
@@ -801,6 +714,9 @@
<height>50</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
@@ -809,7 +725,7 @@
</property>
</widget>
</item>
<item row="19" column="0" colspan="2">
<item row="15" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout_14">
<item>
<widget class="QPushButton" name="uiDefaultNoteFontPushButton">
@@ -840,40 +756,80 @@
</item>
</layout>
</item>
<item row="20" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0" colspan="2">
<widget class="QSpinBox" name="uiSceneWidthSpinBox">
<property name="suffix">
<string> pixels</string>
</property>
<property name="minimum">
<number>500</number>
</property>
<property name="maximum">
<number>1000000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>2000</number>
<item row="9" column="0" colspan="2">
<widget class="QCheckBox" name="uiLimitSizeNodeSymbolCheckBox">
<property name="text">
<string>Limit the size of node symbols</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="12" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QPushButton" name="uiDefaultLabelFontPushButton">
<property name="text">
<string>&amp;Select default font</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiDefaultLabelColorPushButton">
<property name="text">
<string>&amp;Select default color</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="11" column="0" colspan="3">
<widget class="QPlainTextEdit" name="uiDefaultLabelStylePlainTextEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>50</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="plainText">
<string>AaBbYyZz</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="uiSceneHeightSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="suffix">
<string> pixels</string>
</property>
@@ -891,16 +847,106 @@
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QSpinBox" name="uiNodeGridSizeSpinBox">
<item row="0" column="1">
<widget class="QSpinBox" name="uiSceneWidthSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="suffix">
<string> pixels</string>
</property>
<property name="minimum">
<number>10</number>
<number>500</number>
</property>
<property name="maximum">
<number>1000000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>2000</number>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="uiSnapToGridOnNewProject">
<property name="text">
<string>Snap to grid on new project</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="uiDrawingGridSizeLabel">
<property name="text">
<string>Default drawing grid size:</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="uiLabelPreviewLabel">
<property name="text">
<string>Default label style:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="uiNodeGridSizeLabel">
<property name="text">
<string>Default node grid size:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="uiDrawingGridSizeSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="minimum">
<number>5</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="singleStep">
<number>5</number>
</property>
<property name="value">
<number>25</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="uiNodeGridSizeSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="minimum">
<number>5</number>
</property>
<property name="maximum">
<number>150</number>
</property>
<property name="singleStep">
<number>10</number>
<number>5</number>
</property>
<property name="value">
<number>75</number>
@@ -908,18 +954,42 @@
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QSpinBox" name="uiDrawingGridSizeSpinBox">
<property name="minimum">
<number>10</number>
<widget class="QCheckBox" name="uiShowGridOnNewProject">
<property name="text">
<string>Show grid on new project</string>
</property>
<property name="maximum">
<number>100</number>
</widget>
</item>
<item row="4" column="0" colspan="3">
<widget class="QCheckBox" name="uiRectangleSelectedItemCheckBox">
<property name="text">
<string>Draw a rectangle when an item is selected</string>
</property>
<property name="singleStep">
<number>10</number>
<property name="checked">
<bool>true</bool>
</property>
<property name="value">
<number>25</number>
</widget>
</item>
<item row="16" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="uiDrawLinkStatusPointsCheckBox">
<property name="text">
<string>Draw link status points</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
@@ -930,7 +1000,16 @@
<string>Miscellaneous</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -966,7 +1045,7 @@
<item>
<widget class="QCheckBox" name="uiOverlayNotificationsCheckBox">
<property name="text">
<string>Display error, warning and info in a overlay popup</string>
<string>Display error, warning and info in an overlay popup</string>
</property>
</widget>
</item>
@@ -980,21 +1059,24 @@
<item>
<widget class="QCheckBox" name="uiHdpiCheckBox">
<property name="text">
<string>Enable HDPI mode (this may crash the application on Linux, restart required)</string>
<string>Enable HDPI mode (this may crash on Linux, restart required)</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="uiMultiProfilesCheckBox">
<property name="text">
<string>Request for profile settings at application startup (work profile / home profile)</string>
<string>Request for profile settings at application startup</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="uiDirectFileUpload">
<property name="toolTip">
<string>Experimental, requires computes visibility from GUI network</string>
</property>
<property name="text">
<string>Upload files directly to computes (experimental, requires computes visibility from GUI network)</string>
<string>Upload files directly to computes (experimental)</string>
</property>
</widget>
</item>
@@ -1041,6 +1123,56 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>uiProjectsPathLineEdit</tabstop>
<tabstop>uiProjectsPathToolButton</tabstop>
<tabstop>uiSymbolsPathLineEdit</tabstop>
<tabstop>uiSymbolsPathToolButton</tabstop>
<tabstop>uiConfigsPathLineEdit</tabstop>
<tabstop>uiConfigsPathToolButton</tabstop>
<tabstop>uiAppliancesPathLineEdit</tabstop>
<tabstop>uiAppliancesPathToolButton</tabstop>
<tabstop>uiStyleComboBox</tabstop>
<tabstop>uiSymbolThemeComboBox</tabstop>
<tabstop>uiImportConfigurationFilePushButton</tabstop>
<tabstop>uiExportConfigurationFilePushButton</tabstop>
<tabstop>uiBrowseConfigurationPushButton</tabstop>
<tabstop>uiImagesPathLineEdit</tabstop>
<tabstop>uiImagesPathToolButton</tabstop>
<tabstop>uiImageDirectoriesAddPushButton</tabstop>
<tabstop>uiImageDirectoriesDeletePushButton</tabstop>
<tabstop>uiTelnetConsoleCommandLineEdit</tabstop>
<tabstop>uiTelnetConsolePreconfiguredCommandPushButton</tabstop>
<tabstop>uiDelayConsoleAllSpinBox</tabstop>
<tabstop>uiVNCConsoleCommandLineEdit</tabstop>
<tabstop>uiVNCConsolePreconfiguredCommandPushButton</tabstop>
<tabstop>uiSPICEConsoleCommandLineEdit</tabstop>
<tabstop>uiSPICEConsolePreconfiguredCommandPushButton</tabstop>
<tabstop>uiSceneWidthSpinBox</tabstop>
<tabstop>uiSceneHeightSpinBox</tabstop>
<tabstop>uiNodeGridSizeSpinBox</tabstop>
<tabstop>uiDrawingGridSizeSpinBox</tabstop>
<tabstop>uiRectangleSelectedItemCheckBox</tabstop>
<tabstop>uiDrawLinkStatusPointsCheckBox</tabstop>
<tabstop>uiShowInterfaceLabelsOnNewProject</tabstop>
<tabstop>uiShowGridOnNewProject</tabstop>
<tabstop>uiSnapToGridOnNewProject</tabstop>
<tabstop>uiLimitSizeNodeSymbolCheckBox</tabstop>
<tabstop>uiDefaultLabelFontPushButton</tabstop>
<tabstop>uiDefaultLabelColorPushButton</tabstop>
<tabstop>uiDefaultNoteFontPushButton</tabstop>
<tabstop>uiDefaultNoteColorPushButton</tabstop>
<tabstop>uiCheckForUpdateCheckBox</tabstop>
<tabstop>uiCrashReportCheckBox</tabstop>
<tabstop>uiStatsCheckBox</tabstop>
<tabstop>uiOverlayNotificationsCheckBox</tabstop>
<tabstop>uiExperimentalFeaturesCheckBox</tabstop>
<tabstop>uiHdpiCheckBox</tabstop>
<tabstop>uiMultiProfilesCheckBox</tabstop>
<tabstop>uiDirectFileUpload</tabstop>
<tabstop>uiRestoreDefaultsPushButton</tabstop>
<tabstop>uiMiscTabWidget</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

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