Compare commits

...

920 Commits

Author SHA1 Message Date
Julien Duponchelle
4e172fc7e3 2.0.2 2017-05-30 09:02:02 +02:00
Julien Duponchelle
8ed8a2c115 Show a default symbol in case of corrupted file
Fix https://github.com/GNS3/gns3-server/issues/1043
2017-05-24 12:26:18 +02:00
Julien Duponchelle
073665a75d When another gui is already running exit instead of proper close to avoid any issue
Fix #2059
2017-05-23 18:00:02 +02:00
Julien Duponchelle
4ccc67aa46 Fix duplicate on remote server use wrong location
Fix https://github.com/GNS3/gns3-server/issues/1040
2017-05-23 17:25:29 +02:00
Julien Duponchelle
6e2632e91f Display the location of settings when we disallow opening due to old release 2017-05-23 15:54:35 +02:00
Julien Duponchelle
38ddcde902 Improve search for dynamips in development on OSX 2017-05-23 14:24:54 +02:00
Julien Duponchelle
436563afcb Merge pull request #2079 from GNS3/pyup-update-pytest-3.0.7-to-3.1.0
Update pytest to 3.1.0
2017-05-23 08:29:55 +02:00
pyup-bot
7eaab3e38b Update pytest from 3.0.7 to 3.1.0 2017-05-23 02:25:48 +02:00
Julien Duponchelle
0927a2a8c9 Fix error display when loading a .png custom symbol 2017-05-22 13:38:04 +02:00
Julien Duponchelle
87e6159ff6 Fix a crash in the progress dialog
Fix #2064
2017-05-22 09:56:31 +02:00
Julien Duponchelle
effdcf5e24 Fix a race condition when exporting a closed project
Fix #2078
2017-05-22 09:54:17 +02:00
Julien Duponchelle
021cdd2e65 Fix RuntimeError: wrapped C/C++ object of type NodeItem has been deleted
Fix #2070
2017-05-19 17:39:34 +02:00
Julien Duponchelle
363c4a9966 2.0.2dev1 2017-05-16 09:15:18 +02:00
Julien Duponchelle
7082c75511 2.0.1 2017-05-16 08:48:56 +02:00
grossmj
16c4a837d7 Improve inline help. Fixes #1999.
Add a warning about wifi interfaces in the cloud. Fixes #1902.
2017-05-14 22:18:35 +02:00
grossmj
fd42ac410c Copy remote directory path into clipboard in "Show in FileManager". Fixes #1966. 2017-05-12 16:49:26 +08:00
Jeremy Grossmann
6efc177804 Merge pull request #2054 from GNS3/export_thread
Do not run import / export of project in seperate thread
2017-05-12 10:23:25 +08:00
Julien Duponchelle
6f9e6c9b92 Fix display of error in progress dialog when we don't have thread 2017-05-11 17:48:38 +02:00
Jeremy Grossmann
0411c68150 Merge pull request #2057 from GNS3/lost_slot_and_port
Fix lost slot and port in dynamips settings
2017-05-11 23:37:39 +08:00
Julien Duponchelle
4246e731e5 Fix lost slot and port in dynamips settings
When you reopen a project you no longer have the
wic and slot, until you move the node and retrieve an
update. The settings is just lost in the GUI but is fine
on server.

Fix #2053
2017-05-11 09:49:47 +02:00
Julien Duponchelle
50cca71279 Do not run import / export of project in seperate thread
This trigger warning because you need to do the HTTP request
to the API from the main thread.

Fix #2008
2017-05-10 18:13:45 +02:00
Julien Duponchelle
c78ef8f348 Assert when running an HTTP query outside the main thread
Ref #2008
2017-05-10 17:57:02 +02:00
Julien Duponchelle
c03a5a9e0a Proper error when you try to load the pid file as config file
Fix #2044
2017-05-09 17:27:38 +02:00
Julien Duponchelle
c93b7836d8 Log malformed svg text item
Fix #2045
2017-05-09 17:14:02 +02:00
Julien Duponchelle
690b22cc24 Fix a race condition when right click and delete a node at the same time
Fix #2043
2017-05-09 17:03:23 +02:00
Julien Duponchelle
3560251816 Fix a race condition when snapshoting a closed project
Fix #2046
2017-05-09 16:37:42 +02:00
Jeremy Grossmann
90e861289f Merge pull request #2016 from GNS3/missing_xattr
Catch missing function listxattr on some linux host
2017-05-08 21:04:47 +07:00
Jeremy Grossmann
6e144d6122 Update doctor_dialog.py 2017-05-08 21:04:01 +07:00
grossmj
2f168193d1 Catch remaining missing function listxattr on some Linux host. 2017-05-08 21:01:27 +07:00
Jeremy Grossmann
0fe5559564 Merge pull request #2007 from GNS3/fix_project_close
Fix project closing when we have multiple client connected
2017-05-08 20:42:35 +07:00
Julien Duponchelle
37f0744d7c Fix a race condition when creating node and closing project
Fix #2018
2017-05-05 19:04:06 +02:00
Julien Duponchelle
000f4a4790 Fix Bug with python before 3.4.3
Fix #2035
2017-05-05 18:58:40 +02:00
Julien Duponchelle
a09b7d6738 Fix error if you put a path in a .gns3a file for qemu
Fix #2038
2017-05-05 18:31:13 +02:00
Julien Duponchelle
a1fa8f9ec2 Fix AttributeError: 'NoneType' object has no attribute '_refreshVisibleWidgets'
Fix #2034
2017-05-05 18:29:36 +02:00
Jeremy Grossmann
cd6b0b793e Merge pull request #2009 from GNS3/dissalow_gns3_on_remote
Disallow opening a .gns3 on a remote server
2017-05-05 20:53:29 +07:00
Jeremy Grossmann
6453932421 Update main_window.py 2017-05-05 20:52:03 +07:00
Julien Duponchelle
37a23d9682 Do not crash if the logging code raise an exception
Fix #2017
2017-05-04 12:18:26 +02:00
Julien Duponchelle
e18e10c701 Fix some crash in dynamips device preference page
Fix #2029
2017-05-04 12:02:49 +02:00
Julien Duponchelle
a9240e2e46 Fix warning when loading IOU images on Windows
Fix #2012
2017-05-04 10:43:13 +02:00
Julien Duponchelle
4df0c33013 Do not crash if you don't have configure a packet capture program on Windows
Fix #2026
2017-05-04 10:38:35 +02:00
Julien Duponchelle
4096316ceb Ignore error when we can't kill the packet capture
The user probalby manually kill it.

Fix #2013
2017-05-04 10:30:41 +02:00
Julien Duponchelle
e09292c647 Fix AttributeError: 'NoneType' object has no attribute 'wasCanceled'
Fix #2023
2017-05-04 10:27:45 +02:00
Julien Duponchelle
31c37161fa Fix RuntimeError: wrapped C/C++ object of type QComboBox has been deleted
Fix #2027
2017-05-04 10:25:46 +02:00
Julien Duponchelle
a047cd7f4c Fix RuntimeError: wrapped C/C++ object of type QTreeWidgetItem has been deleted
Fix #2028
2017-05-04 10:13:33 +02:00
Julien Duponchelle
87cde665a8 Fix detection of https when use for the local server
Ref #995
2017-05-03 17:23:03 +02:00
Julien Duponchelle
6361c94bbe Silent the _COMPIZ_TOOLKIT_ACTION warning
This a Qt bug.

Fix #2020
2017-05-03 15:54:55 +02:00
Julien Duponchelle
8a0aeff0bb Cacth TypeError: native Qt signal is not callable
Fix #2011
2017-05-03 10:58:38 +02:00
Julien Duponchelle
508f8b3ad5 Fix AttributeError: 'C7200' object has no attribute 'warning_signal'
Fix #2014
2017-05-03 10:51:12 +02:00
Julien Duponchelle
bfe942c029 Catch missing function listxattr on some linux host
Fix #2010
2017-05-03 10:47:22 +02:00
Julien Duponchelle
b3a86594ff 2.0.1dev1 2017-05-03 10:19:25 +02:00
Julien Duponchelle
debe88bd37 2.0.0 2017-05-02 10:14:01 +02:00
Julien Duponchelle
a948fd07b1 Disallow opening a .gns3 on a remote server
This prevent opening a local .gns3 on a remote server.
Because this is not working you need to import the
project on the server using portable project.

Fix https://github.com/GNS3/gns3-server/issues/984
2017-05-02 09:27:40 +02:00
Julien Duponchelle
072f714e21 Fix project closing when we have multiple client connected
In all case when we close the main window we let the server
manage if he need to close or not the project.

Fix https://github.com/GNS3/gns3-server/issues/991
2017-05-02 09:07:14 +02:00
Julien Duponchelle
a2059d3e7c Clarify that we don't override vmware custom adapters 2017-04-27 10:06:52 +02:00
Julien Duponchelle
c4cc819d50 Strip space from path at project creation
Fix #1997
2017-04-21 14:49:52 +02:00
Julien Duponchelle
0796d9aae5 2.0.0dev13 2017-04-20 10:57:28 +02:00
Julien Duponchelle
c8148c1877 2.0.0rc4 2017-04-20 10:56:55 +02:00
Julien Duponchelle
d6021afa0f Catch all error during the generation of log messages.
This prevent error loop is something goes wrong.

Fix #1993
2017-04-20 09:26:57 +02:00
Julien Duponchelle
9018bdac07 Remove no longer working capturelog plugin for pytest 2017-04-19 09:41:56 +02:00
Julien Duponchelle
a48b898d92 Catch a rare node creation error
Fix #1991
2017-04-19 09:39:32 +02:00
Julien Duponchelle
949c1bbe37 Fix missing menu text at application startup
Fix #1990
2017-04-19 09:35:31 +02:00
Julien Duponchelle
0089edecc0 Run test on windows also 2017-04-18 16:48:45 +02:00
Julien Duponchelle
91da1d492b Fix a race condition in the drawing item
Fix #1987
2017-04-18 11:32:33 +02:00
Julien Duponchelle
0242f53c17 Catch system error when connecting to local server
Fix #1988
2017-04-18 11:29:42 +02:00
Julien Duponchelle
bab7a1016f Catch a rare error when killing the capture
Fix #1985
2017-04-14 10:39:43 +02:00
Julien Duponchelle
2ec91e1ef7 Improve pcap streaming speed
Fix #1982
2017-04-14 10:30:49 +02:00
Julien Duponchelle
38af30ec15 Merge branch 'master' into 2.0 2017-04-14 09:04:17 +02:00
Julien Duponchelle
3f8a0cb527 Merge master 2017-04-14 09:03:08 +02:00
Julien Duponchelle
fd1a86df03 1.5.5dev1 2017-04-14 09:00:57 +02:00
Julien Duponchelle
c954a16ead 1.5.4 2017-04-13 17:27:37 +02:00
Julien Duponchelle
9418b997eb Mac Qt wheel is not working in our build envirronement 2017-04-13 16:07:52 +02:00
Julien Duponchelle
9682cad4f4 Add security issues section 2017-04-13 12:37:34 +02:00
Julien Duponchelle
60ee9e1374 Limit ubridge permission to the admin group on OSX 2017-04-13 10:04:08 +02:00
Julien Duponchelle
dbd5b9366a Upgrade to 5.7.1 2017-04-13 10:01:40 +02:00
Julien Duponchelle
f63314d4d0 Fix version number 2017-04-13 09:56:17 +02:00
Julien Duponchelle
96eaec0100 Recent projects list bug
Fix #1976
2017-04-10 17:59:17 +02:00
Julien Duponchelle
5d554976e2 Fix a race condition in the preferences dialog
Fix #1981
2017-04-10 11:42:13 +02:00
Julien Duponchelle
d1d8390b73 Try to fix some windows Z issues
Fix #1955
2017-04-07 11:06:51 +02:00
Julien Duponchelle
5f3f6462c2 Catch a garbage collection issue in the right click on a link 2017-04-03 16:33:14 +02:00
Julien Duponchelle
821809bf6f Fix a compatibility issue with Python 3.4
Fix #1973
2017-03-31 19:30:32 +02:00
Julien Duponchelle
a712fab7a4 2.0.0dev12 2017-03-31 09:44:27 +02:00
Julien Duponchelle
4ab21ea9f9 2.0.0rc3 2017-03-31 09:41:48 +02:00
Julien Duponchelle
1a4902279b Check hibernation only if it's a remote main server
Fix #1971
2017-03-29 17:19:00 +02:00
Julien Duponchelle
35412171b9 Improve timeout handling
Ref #1959
2017-03-29 12:34:10 +02:00
Julien Duponchelle
73939847b9 Improve logging when we display a qt message box
Ref #1959
2017-03-29 12:12:22 +02:00
Julien Duponchelle
5acfc5bc56 Fix a problem with settings not pushed to the server 2017-03-28 16:22:17 +02:00
Julien Duponchelle
d67d0d146f Try to detect computer hibernation
I'm not really happy with the patch but it the most simple way
I found. If between two request we have more than 120 seconds
of differences we disconnect.

Fix #1968
2017-03-28 14:41:43 +02:00
Julien Duponchelle
23da6a4c31 Fix crash when we send some errors to the user console 2017-03-27 18:16:11 +02:00
Julien Duponchelle
fd5f999756 Use QtFile for managing file capture
This prevent application freeze when we run a capture.

Fix #1959
2017-03-27 14:19:07 +02:00
Julien Duponchelle
c05304b86f Allow to delete a profile from the profile select dialog
Fix #1961
2017-03-27 10:26:29 +02:00
Julien Duponchelle
6361980591 Filter hidden folder in the profil directory
Fix #1960
2017-03-23 12:02:32 +01:00
Julien Duponchelle
d5ea98ba2a Prevent user putting port in the remote host name
Fix #1948
2017-03-22 11:56:27 +01:00
Julien Duponchelle
3ca01384c9 Fix RuntimeError: wrapped C/C++ object of type EllipseItem has been deleted
Fix #1957
2017-03-21 18:19:47 +01:00
Julien Duponchelle
f35bb6a281 Fix a rare error in LinkItem
Fix #1950
2017-03-21 16:56:23 +01:00
Julien Duponchelle
0c4a7693e6 Fix Image field in nodes list is stale after changing an image
Fix #1956
2017-03-21 16:54:10 +01:00
Julien Duponchelle
20506525c5 Fix RuntimeError: Set changed size during iteration
Fix #1951
2017-03-21 15:13:49 +01:00
Julien Duponchelle
bb00a9f64c Better detection of remote server changes
Fix #1949
2017-03-21 14:38:20 +01:00
Julien Duponchelle
ef36792379 Add a notice about the fact you need to apply server settings
Fix #1952
2017-03-21 11:35:03 +01:00
Julien Duponchelle
873fd409bd Check python version only for setup.py install 2017-03-21 10:10:14 +01:00
Julien Duponchelle
03221e8ab7 Fix refresh issue with link in topology summary
Fix #1938
2017-03-20 18:24:16 +01:00
Julien Duponchelle
55f9836dc9 Remove duplicate code 2017-03-20 17:34:17 +01:00
Julien Duponchelle
3ab5144fe2 Catch appliance error when creating an appliance new version
Fix #1941
2017-03-20 17:10:49 +01:00
Julien Duponchelle
c91a22c9f8 If a node can't be deleted do not remove it
Fix #1933
2017-03-16 19:42:38 +01:00
Julien Duponchelle
a8ba909568 If something is wrong during packet capture do not disconnect us from the server
Fix #1520
2017-03-16 18:14:55 +01:00
Julien Duponchelle
d33e9ed833 If something is wrong during packet capture do not disconnect us from the server
Fix #1520
2017-03-16 14:37:32 +01:00
Julien Duponchelle
aa31af1dca Fix saving dynamips
Fix #1939
2017-03-16 14:33:16 +01:00
Julien Duponchelle
4d07a7391f Try to fix the hang dialog on some computers
Ref #1915
2017-03-16 12:19:09 +01:00
Julien Duponchelle
417395718d Fix a rare crash in progress dialog 2017-03-15 16:36:13 +01:00
Julien Duponchelle
212048c4d1 If we pass --profile skip the profile select dialog
Fix #1934
2017-03-15 16:09:35 +01:00
Julien Duponchelle
11c27063b4 Raise an error if the progress dialog is not created from the main thread
Ref #1915
2017-03-15 15:14:12 +01:00
Julien Duponchelle
b8696fa54e Log qt log to python log 2017-03-15 14:50:19 +01:00
Julien Duponchelle
ba9785f83f Revert "Rollback to Qt 5.7.1"
This reverts commit 5038a72610.
2017-03-15 14:46:57 +01:00
Julien Duponchelle
4ae742529c Fix image are not uploaded to remote main server
Fix #1926, #1927, #1925
2017-03-15 14:33:25 +01:00
Julien Duponchelle
92cc335708 Fix race condition when editing a project
Fix #1932
2017-03-15 14:30:00 +01:00
Julien Duponchelle
2099a4ae9a Poll settings each 5 seconds
Fix #1923, #1924
2017-03-15 12:55:14 +01:00
Julien Duponchelle
5038a72610 Rollback to Qt 5.7.1
Ref #1915
2017-03-15 11:51:22 +01:00
Julien Duponchelle
b9e320996b Merge pull request #1935 from GNS3/pyup-update-pytest-3.0.6-to-3.0.7
Update pytest to 3.0.7
2017-03-15 10:20:50 +01:00
pyup-bot
9c86cd71c8 Update pytest from 3.0.6 to 3.0.7 2017-03-14 23:53:42 +01:00
Julien Duponchelle
7fdf42b442 Avoid progress dialog not disapear
Fix #1915
2017-03-14 15:40:04 +01:00
Julien Duponchelle
61d86e919e Remove wrong mention about the fact super putty is include 2017-03-14 12:02:02 +01:00
Julien Duponchelle
bc71985ee3 Avoid a crash when an ios router don't have a chassis
Fix #1920
2017-03-13 17:25:58 +01:00
Julien Duponchelle
6bebf2d14d Fix a potentatial crash in the progress dialog
Fix #1919
2017-03-13 17:16:34 +01:00
Julien Duponchelle
490021aa47 Fix appliance file 2017-03-13 15:48:59 +01:00
Julien Duponchelle
c8db5e7e49 2.0.0 dev 11 2017-03-13 15:42:06 +01:00
Julien Duponchelle
8efaacbc3d Support official docker images in appliances 2017-03-13 15:41:14 +01:00
Julien Duponchelle
c5da24b954 2.0.0rc2 2017-03-10 20:40:10 +01:00
Julien Duponchelle
4456cfd68e Merge branch 'master' into 2.0 2017-03-10 20:39:03 +01:00
Julien Duponchelle
3468909db1 Deploy on pypi when we tag 2017-03-10 20:37:57 +01:00
Julien Duponchelle
212728eb94 Fix rare crash in GNS3 VM preference page
Fix #1912
2017-03-09 10:51:38 +01:00
Julien Duponchelle
cf4f73a7e1 Fix an error on Windows when loading SVG files
Fix #1913
2017-03-09 10:25:18 +01:00
Julien Duponchelle
5948e5c4cb Prevent a potential crash 2017-03-08 18:12:46 +01:00
Julien Duponchelle
791aa27158 Workaround a rare crash when sending analytics
It seem PyQT is confuse between the signal and error function.

Fix #1861
2017-03-08 14:52:47 +01:00
Julien Duponchelle
0b8ab56ffc Catch error when you try to create a node a not existing server
Fix #1905
2017-03-08 14:44:30 +01:00
Julien Duponchelle
12dcfda756 Fix an error when your local server crash and computer return non unicode
Fix #1904
2017-03-08 14:40:29 +01:00
Julien Duponchelle
ef44beac41 Fix KeyError: 'slot1'
Fix #1907
2017-03-08 09:54:17 +01:00
Julien Duponchelle
97a71904f7 Fix a rare crash in import appliance
Fix #1908
2017-03-08 09:53:27 +01:00
Julien Duponchelle
94f4059d67 Rollback to PyQT 5.8 because 5.8.1 seem to have trouble at install 2017-03-07 15:49:26 +01:00
Julien Duponchelle
14ed2546bc Merge pull request #1903 from GNS3/pyup-update-pyqt5-5.8-to-5.8.1
Update pyqt5 to 5.8.1
2017-03-07 13:25:40 +01:00
pyup-bot
908258c163 Update pyqt5 from 5.8 to 5.8.1 2017-03-07 12:09:10 +01:00
Julien Duponchelle
704191ba25 2.0.0 dev 10 2017-03-06 19:42:25 +01:00
Julien Duponchelle
be7fc9abe2 2.0.0rc1 2017-03-06 19:39:46 +01:00
Julien Duponchelle
e0daf1dbb1 Fix syntax error 2017-03-01 09:04:05 +01:00
Julien Duponchelle
a8877d4f8a UltraVNC support 2017-02-28 18:08:48 +01:00
Julien Duponchelle
2db850a3f3 Fix tests 2017-02-28 15:58:25 +01:00
Julien Duponchelle
ec7cdedb86 Display less noisy dialog when we can't connect to the remote server
Fix #1887
2017-02-28 14:06:52 +01:00
Julien Duponchelle
606fa15a55 Prevent the usage of gns3vm as a remote server name 2017-02-28 13:50:31 +01:00
Jeremy Grossmann
3eca9b0e54 Merge pull request #1881 from GNS3/catch_local_process_errors
Monitor and display local server stderr
2017-02-28 02:46:27 +08:00
Julien Duponchelle
3abaf74580 Fix the VMware wizard for not using a remote server by default
Fix #1893
2017-02-27 19:00:02 +01:00
Julien Duponchelle
9713748633 Prevent the GNS3 VM to appear in remote compute in the VM wizard
Fix #1894
2017-02-27 18:08:34 +01:00
Julien Duponchelle
405e86aff4 Remove iouyap settings
Fix #1892
2017-02-27 15:26:05 +01:00
Julien Duponchelle
4da824dc2b Fix missing permission error management
Fix #1888
2017-02-24 09:49:10 +01:00
Julien Duponchelle
0ec177c644 Avoid a crash when create a new dynamips version in the appliance wizard
Fix #1884
2017-02-23 15:13:00 +01:00
Julien Duponchelle
9ef4f86050 Disallow user to add the same server as a remote server and as local server
Fix #1883
2017-02-23 11:13:14 +01:00
Julien Duponchelle
96b830817e Fix 'module' object has no attribute 'run'
Fix #1878
2017-02-23 09:11:31 +01:00
Julien Duponchelle
0db8b00fb9 Monitor and display local server stderr
It's the best solution I found. Streaming in real time
the log will require to start an aditionnal thread

Fix #1880
2017-02-22 17:51:23 +01:00
Julien Duponchelle
005dde6c2b Fix some import errors 2017-02-22 17:23:58 +01:00
Julien Duponchelle
260f9b352e Remove placeholder string from appliance wizard 2017-02-22 11:02:25 +01:00
Julien Duponchelle
852b0bc498 Avoiding calling multiple time /computes at the same time. And reduce timeout
Fix #1848
2017-02-22 10:29:41 +01:00
Julien Duponchelle
3a503a5fc0 Support for appliance v4 2017-02-22 09:04:22 +01:00
Jeremy Grossmann
88b408695a Merge pull request #1875 from GNS3/hdpi_enable
Disable HDPI by default on Linux and allow to configure it
2017-02-20 20:39:19 -08:00
grossmj
776b45363b Some tweaks for enabling/disabling HDPI mode. 2017-02-21 12:33:09 +08:00
Julien Duponchelle
adb270b64c Do not display error at first step of the setup wizard
Fix #1827
2017-02-20 20:04:43 +01:00
Julien Duponchelle
5329b8fd72 Disable HDPI by default on Linux and allow to configure it
Fix #1870
2017-02-20 19:21:02 +01:00
Julien Duponchelle
d6379e4bb9 Fix an issue when you edit a VPCS node from the node view
Fix https://github.com/GNS3/gns3-gui/issues/1874
2017-02-20 18:09:48 +01:00
Julien Duponchelle
6ca18d5b29 Catch a race condition in managing error static assets download
Fix #1872
2017-02-20 15:11:44 +01:00
Julien Duponchelle
50c008ddec Handle error if you try to import an appliance without having the images
Fix #1871
2017-02-20 12:29:37 +01:00
Julien Duponchelle
4f56f100fa Improve crash proof code of the progress dialog
Fix #1873
2017-02-20 12:16:32 +01:00
Julien Duponchelle
0c16a5b0d1 2.0.0dev9 2017-02-16 11:27:49 +01:00
Julien Duponchelle
8f33ad3c70 2.0.0b4 2017-02-16 11:26:32 +01:00
Julien Duponchelle
7dca1b404c Fix a rare crash 2017-02-15 19:28:38 +01:00
Julien Duponchelle
99ff98ff47 Merge pull request #1858 from GNS3/pyup-update-pyqt5-5.7.1-to-5.8
Update pyqt5 to 5.8
2017-02-15 18:55:09 +01:00
pyup-bot
083d6e1298 Update pyqt5 from 5.7.1 to 5.8 2017-02-15 18:44:11 +01:00
Julien Duponchelle
71716c451e Drop from console view the show command not supported by 2.0
Fix #1855
2017-02-15 18:15:09 +01:00
Julien Duponchelle
3c4a244b75 Try to avoid segfault in some PyQT version
Fix #1856
2017-02-15 16:19:13 +01:00
Julien Duponchelle
85892a3bd6 Display git commit version in version number 2017-02-15 12:52:17 +01:00
Julien Duponchelle
1d3d721cf3 Support for strike and underline
Fix #1851
2017-02-14 17:45:48 +01:00
Julien Duponchelle
37a72af75f Do not use native font selector on mac it could crash 2017-02-14 17:01:21 +01:00
Julien Duponchelle
c93713b9e7 Use a dedicated QNetwork manager for notification
I hope this will solve issue for some users where the connection
return random error when they change the project.

Ref #1848
2017-02-14 15:15:29 +01:00
Julien Duponchelle
77b3118cbc Try to workaround unknow error on some user computers
Ref #1848
2017-02-14 12:27:34 +01:00
Julien Duponchelle
bc0cbfd040 Fix a display error in console error message 2017-02-14 11:37:20 +01:00
Julien Duponchelle
0cfc66b4d4 Use signal for writting on console to avoid some potential segfault 2017-02-14 11:23:41 +01:00
Julien Duponchelle
1b9b1cbe3c Fix tests about HTTP errors 2017-02-14 10:19:34 +01:00
Julien Duponchelle
407187c826 Fix a rare warning
Fix https://github.com/GNS3/gns3-server/issues/901
2017-02-14 09:53:12 +01:00
Julien Duponchelle
a6eb1e65ac Add more debug when we have an http error
Ref #1848
2017-02-13 19:15:19 +01:00
Julien Duponchelle
efbb19a862 Disable timeout on project open
Ref #1848
2017-02-13 17:39:13 +01:00
Julien Duponchelle
2a94816b58 Support for gvncviewer
Fix #1845
2017-02-13 15:26:12 +01:00
Julien Duponchelle
aab307a519 Fix a rare crash in the file editor dialog
Fix #1849
2017-02-13 15:22:03 +01:00
Julien Duponchelle
1279e16484 Fix a race condition when we display the error
Ref #1848
2017-02-13 13:08:38 +01:00
Julien Duponchelle
c2e20f9bd6 Fix an issue with invalid hostname detected as an IPV6 2017-02-10 18:13:48 +01:00
Julien Duponchelle
4acdaf6b5a When you update a a node from the node view send settings to controller 2017-02-10 15:51:54 +01:00
Julien Duponchelle
25313cbcde Fix error when permission on the loaded image is broken
Fix #1843
2017-02-08 11:19:41 +01:00
Julien Duponchelle
0de0eb12eb Fix tests 2017-02-07 17:36:21 +01:00
Julien Duponchelle
7d0fe52600 Fix crash with invalid image file in appliance wizard
Fix #1837
2017-02-06 17:27:30 +01:00
Julien Duponchelle
ba1d3b2423 Fix error when loading an handmade appliance file
Fix #1839
2017-02-06 17:10:30 +01:00
Julien Duponchelle
cae6afe85d Fix no error if your VNC client is not configured
Fix #1838
2017-02-06 16:59:06 +01:00
Julien Duponchelle
5865ac267b Avoid high cpu usage when connection is lost
Fix #1840
2017-02-06 11:27:36 +01:00
Julien Duponchelle
162993839c Support {name} in cloud template
Fix #1833
2017-02-03 15:15:47 +01:00
Julien Duponchelle
4359b490cc Fix text of the export dialog
Fix #1834
2017-02-03 14:47:07 +01:00
Julien Duponchelle
e4a8e67229 Fix error message when a project is already open
Fix #1832
2017-02-03 14:45:44 +01:00
Julien Duponchelle
6605270e64 Fix missing info in tooltip of ethernet switch
Fix  https://github.com/GNS3/gns3-gui/issues/1828
2017-01-31 19:28:03 +01:00
Julien Duponchelle
09e2bfeed0 The server manage the vmname when we update the linked virtual box VM
Ref https://github.com/GNS3/gns3-gui/issues/1821
2017-01-31 18:50:52 +01:00
Julien Duponchelle
b24733466d Fix z value for text
Fix #1822
2017-01-30 16:00:43 +01:00
Julien Duponchelle
c31be48f20 Avoid a segfault when display an error 2017-01-30 10:51:31 +01:00
Julien Duponchelle
a9ed27f42c Add sata options in the appliance schema
Fix #1817
2017-01-27 11:01:11 +01:00
Julien Duponchelle
1440caa532 Fix a rare crash when exporting IOU configurations
Fix #1800
2017-01-25 13:55:19 +01:00
Julien Duponchelle
513eb21940 Allow additionnal properties in registry files 2017-01-25 12:14:36 +01:00
Julien Duponchelle
de348d39da Fix a potential crash when a symbol is not found 2017-01-25 12:04:40 +01:00
Julien Duponchelle
0a3697962b Strip unused code for OVA support in the registry 2017-01-24 12:14:25 +01:00
Julien Duponchelle
e0edcf3d23 Increase the timeout for killing local server
Fix #1813
2017-01-24 11:20:50 +01:00
Julien Duponchelle
57dbff6a8e Merge pull request #1804 from GNS3/pyup-update-pytest-3.0.5-to-3.0.6
Update pytest to 3.0.6
2017-01-24 09:57:13 +01:00
Julien Duponchelle
0149bc90f2 Fix error when changing the layer of a drawing item
Fix #1810
2017-01-23 17:30:55 +01:00
Julien Duponchelle
f7292deb0f Fix double click for open file on OSX
Fix #1808
2017-01-23 17:22:17 +01:00
Julien Duponchelle
be01448c36 Add debug to see the arguments use to start the application
Ref #1808
2017-01-23 13:58:19 +01:00
Julien Duponchelle
484617ce25 Put the selected engine in the first position of the listbox
This avoid trigerring unexpected signals

Fix #1803
2017-01-23 11:31:55 +01:00
Julien Duponchelle
8ec53a6004 Fix rare crash with dynamips
Fix #1806
2017-01-23 10:15:29 +01:00
Julien Duponchelle
cfd1bbd9d1 Fix rare crash in the progress dialog
Fix #1802
2017-01-23 10:13:43 +01:00
Julien Duponchelle
25ae214b6b Fix a rare crash in console view
Fix #1807
2017-01-23 10:11:36 +01:00
Julien Duponchelle
456160beb1 Fix rare crash
Fix #1805
2017-01-23 10:09:49 +01:00
Julien Duponchelle
9affe2d9f4 Fix crash when you drag a file inside GNS3
Fix #1798
2017-01-23 10:08:03 +01:00
pyup-bot
3ed2f89b3b Update pytest from 3.0.5 to 3.0.6 2017-01-22 23:58:47 +01:00
Julien Duponchelle
9bb353fdbd 2.0.0dev8 2017-01-19 11:23:19 +01:00
Julien Duponchelle
c414ea28e4 2.0.0b3 2017-01-19 11:20:31 +01:00
Julien Duponchelle
cb0253f7cb Fix error if you already have an image with a different name on remote server
Fix #1794
2017-01-18 17:13:00 +01:00
Julien Duponchelle
f28663c626 Ask pyup to not monitor pywin32 2017-01-18 09:32:39 +01:00
Julien Duponchelle
3ef018f90e Allow any pywin32 version 2017-01-17 10:59:25 +01:00
Julien Duponchelle
e5ae7f77fa Drop gns3 converter from requirements 2017-01-17 10:58:36 +01:00
Julien Duponchelle
b6bac0cd3b Show correct server name in tooltip
Fix #1783
2017-01-16 20:46:49 +01:00
Julien Duponchelle
6a8e435210 Menu item to open controller webpage
Fix #1784
2017-01-16 17:43:06 +01:00
grossmj
442318af49 Fixes potential exception when adding network module to an IOS router. Fixes #1774. 2017-01-16 13:53:25 +08:00
Julien Duponchelle
ad93b46c94 Merge pull request #1776 from GNS3/pyup-update-pypiwin32-219-to-220
Update pypiwin32 to 220
2017-01-13 09:13:40 +01:00
pyup-bot
f7a9cc09ea Update pypiwin32 from 219 to 220 2017-01-12 18:47:41 +01:00
Julien Duponchelle
0d5507cf3d Merge branch '2.0' of github.com:GNS3/gns3-gui into 2.0 2017-01-12 08:55:17 +01:00
Julien Duponchelle
f885e33cbd Merge branch 'master' into 2.0 2017-01-12 08:53:57 +01:00
Julien Duponchelle
4813a8681f 1.5.4dev1 2017-01-12 08:51:46 +01:00
Julien Duponchelle
829f750c76 1.5.3 2017-01-12 08:14:42 +01:00
Julien Duponchelle
08b9f4a6d2 Merge pull request #1772 from GNS3/sata-qemu
Sata disk interface support for Qemu VMs.
2017-01-11 16:53:58 +01:00
Julien Duponchelle
7fdf022b36 Do not export a file config file if empty 2017-01-10 15:56:17 +01:00
Julien Duponchelle
f12935076e Allow to set console type in qemu wizard
/bin/bash: q: command not found
2017-01-09 13:02:12 +01:00
Julien Duponchelle
7a472d1574 Fix overwrite of projects
Fix #1743
2017-01-09 12:35:36 +01:00
Julien Duponchelle
4a82dc8705 Fix creation of new appliance version when filename is different
Fix #1755
2017-01-09 12:08:09 +01:00
Julien Duponchelle
52acaadfce Fix you can't configure port 0 on ethernet switch
Fix #1765
2017-01-09 10:23:06 +01:00
Julien Duponchelle
ef1967ff00 Fix a race condition when saving as a project and closing it
Fix #1770
2017-01-09 10:17:03 +01:00
Julien Duponchelle
6584f3b2d4 Reorder multi link when you delete one
Fix #1750
2017-01-06 11:42:26 +01:00
Julien Duponchelle
8ad55290b1 Ensure we can't connect to occupy port
Fix https://github.com/GNS3/gns3-gui/issues/1759
2017-01-06 10:30:24 +01:00
Julien Duponchelle
8ff34d63c0 Fix AttributeError: 'QImageSvgRenderer' object has no attribute '_svg'
Fix #1760
2017-01-06 09:12:13 +01:00
Julien Duponchelle
da88947028 Fix Unsaved preferences in GNS3 VM warning 2017-01-05 17:40:05 +01:00
Julien Duponchelle
854dc5db71 Fix Unsaved preferences in GNS3 VM warning
Fix #1740
2017-01-05 15:50:41 +01:00
Julien Duponchelle
e8ac144011 Merge pull request #1753 from GNS3/pyup-update-pyqt5-5.7-to-5.7.1
Update pyqt5 to 5.7.1
2017-01-05 08:22:10 +01:00
grossmj
8b20f4d568 Force margins in configuration tabs. 2017-01-05 14:52:39 +08:00
grossmj
cb88a4f6d9 Sata disk interface support for Qemu VMs. 2017-01-05 11:24:49 +08:00
grossmj
270f12dc1e Remove "sata" disk interface. Does not exist in Qemu. Ref #1749 2017-01-02 15:02:19 +08:00
grossmj
46ff586055 Add SATA and none disk interfaces on Qemu VM configuration page. Fixes #1749. 2017-01-01 23:25:31 +08:00
pyup-bot
78b999be57 Update pyqt5 from 5.7 to 5.7.1 2016-12-29 12:06:02 +01:00
Julien Duponchelle
4f702d9339 Fix TypeError: argument of type 'NoneType' is not iterable
Fix #1733
2016-12-21 14:34:58 +01:00
Julien Duponchelle
6f210c0e91 2.0.0 dev7 2016-12-21 09:38:50 +01:00
Julien Duponchelle
a677cff0a2 Fix an error when you edit readme and no projet is opened
Fix #1732
2016-12-21 09:37:23 +01:00
Julien Duponchelle
5929e3b56d 2.0.0 beta 2 2016-12-20 12:01:10 +01:00
Julien Duponchelle
5b1050e427 1.5.3 dev2 2016-12-20 11:48:07 +01:00
Julien Duponchelle
271e987972 Upgrade Qt 5.7 2016-12-20 11:40:32 +01:00
grossmj
80402185a5 AUX console button text change in MainWindow. 2016-12-20 18:31:46 +08:00
Julien Duponchelle
1df34148b2 1.5.3 rc1 2016-12-20 09:32:38 +01:00
Julien Duponchelle
dc2070b24e Remove debug 2016-12-20 08:56:08 +01:00
grossmj
7dcbb14b75 Remove forgotten code comments. Ref #1713. 2016-12-20 12:27:41 +11:00
Julien Duponchelle
36c90e0d96 Fix GNS3 Client not connecting to remote controller
Fix #1726
2016-12-19 18:58:05 +01:00
Julien Duponchelle
53b0bef527 Delete from project list deleted projects
Fix #1724
2016-12-19 18:52:31 +01:00
Julien Duponchelle
8227cf1aba Keep a shared list of projects internally
Ref #1724
2016-12-19 18:29:48 +01:00
Julien Duponchelle
7e09d9042b Fix recent files in new project dialog 2016-12-19 17:45:16 +01:00
Julien Duponchelle
49688d6c0e Move recent projects to the file menu
Fix #1713
2016-12-19 17:40:20 +01:00
Julien Duponchelle
962ce3e3d8 Fix Tail process for wireshark trace not killed when we change project
Fix #1725
2016-12-19 14:57:55 +01:00
grossmj
8cbae911e9 Move project menu items. Ref #1713. 2016-12-19 23:23:26 +11:00
Julien Duponchelle
10c94baab7 Fix AttributeError: 'NoneType' object has no attribute 'project_updated_signal'
Fix #1728
2016-12-19 09:11:39 +01:00
Julien Duponchelle
2f2c0ba3ba Display recent files for local controller, recent project for remote controller
Ref #1713
2016-12-16 11:43:01 +01:00
Julien Duponchelle
879647de5e Do not display the remote server if the server is use as a GNS3 VM
Fix #1718
2016-12-15 18:07:00 +01:00
Julien Duponchelle
cd2c9d6b0c If the notification stream is stopped by something we auto reconnect 2016-12-14 19:58:09 +01:00
Julien Duponchelle
95c443e127 Ignore system proxy to avoid trouble with "Security Suites"
Fix #1721
2016-12-13 18:04:19 +01:00
Julien Duponchelle
d403b26b44 Avoid close and delete a project at the same time
Ref #1714
2016-12-12 22:08:49 +01:00
Julien Duponchelle
0f4d4e2071 Alpha sort of servers summaries 2016-12-12 21:29:16 +01:00
Julien Duponchelle
b51e772664 Fix new remote server doesn't show up in compute summary
Fix #1703
2016-12-12 21:25:33 +01:00
Julien Duponchelle
83c7be0f60 Fix interface number for Switch & Hub templates
Fix #1711, #1712
2016-12-12 16:30:33 +01:00
Julien Duponchelle
0063f7d97f Fix sync of node alignements with the server
Fix #1710
2016-12-12 12:50:02 +01:00
Julien Duponchelle
0a65eeeee2 Fix rare condition when you close a project and add a node
Fix #1716
2016-12-12 09:57:49 +01:00
Julien Duponchelle
a7e69e7260 Fix 'LocalServer' object has no attribute '_server_started_by_me'
Fix #1715
2016-12-12 09:13:05 +01:00
Julien Duponchelle
cea15dab4c Options -q for quiet startup
Fix #1708
2016-12-12 09:11:13 +01:00
Julien Duponchelle
7646b59078 Fix an error when apply permission on OSX 2016-12-08 18:52:48 +01:00
Julien Duponchelle
a3c996a3d8 Fix test suites 2016-12-08 17:49:06 +01:00
Julien Duponchelle
e6017ea102 Support Qemu cpus in GNS3A
Fix https://github.com/GNS3/gns3-server/issues/811
2016-12-08 17:34:48 +01:00
Julien Duponchelle
e7db81c277 Support for BIOS images
Fix https://github.com/GNS3/gns3-gui/issues/1700
2016-12-08 16:21:30 +01:00
Julien Duponchelle
a182f5e4b6 Fix IdlePC can't be found during setup wizard
Fix #1693
2016-12-08 12:41:16 +01:00
Julien Duponchelle
da73357763 2.0.0 dev 6 2016-12-08 10:31:34 +01:00
Julien Duponchelle
09e37725f7 2.0.0b1 2016-12-07 19:46:33 +01:00
Julien Duponchelle
5ab9f95379 Use osascript on OSX for asking admin permission
Fix #1699
2016-12-07 16:45:01 +01:00
Julien Duponchelle
6731909971 Change the method for creating the tmpdir for symbols cache
Fix #1701
2016-12-07 16:09:05 +01:00
Julien Duponchelle
284948e377 Fix a connection error at the end of the setup wizard
Fix #1698
2016-12-07 15:59:54 +01:00
Julien Duponchelle
1e55c974eb Merge pull request #1702 from GNS3/CapnCheapo-remove_use_local
Remove use local
2016-12-07 15:33:03 +01:00
grossmj
b186dee326 Change some more preferences tab names. 2016-12-07 22:04:28 +11:00
grossmj
3ae0abc19e Change how some tabs are organized or named. 2016-12-07 21:42:09 +11:00
Julien Duponchelle
dde660ae15 General settings => local settings 2016-12-06 19:40:33 +01:00
Julien Duponchelle
d9fbd04552 Drop more reference to use local server 2016-12-06 19:36:24 +01:00
pyup.io bot
a20c15cbac Update pytest from 3.0.4 to 3.0.5 (#1697) 2016-12-05 17:29:42 +01:00
Stephen Moore
5a54b9f5ca Remove local server checkbox from preferences 2016-12-04 22:14:19 -06:00
Julien Duponchelle
3528d85501 Make sure to not start local server during setup wizard remote server
Fix #1674
2016-12-01 18:18:59 +01:00
Julien Duponchelle
b773599c56 Merge branch 'master' into 2.0 2016-12-01 13:43:22 +01:00
Julien Duponchelle
73346c8de7 Fix Error when editing IOS image created using .gns3a file
Fix #1684
2016-12-01 13:42:42 +01:00
Julien Duponchelle
d33b21fdc1 Fix Error when editing IOS image created using .gns3a file
Fix #1684
2016-12-01 13:41:33 +01:00
Julien Duponchelle
8139010796 Fix test suites around sip deleted 2016-12-01 13:30:00 +01:00
Julien Duponchelle
f0bc1a4abb Do not auto start the local server in setup wizard
Until you quit setup wizard or choose local server the server
don't start.

Fix #1687
2016-12-01 12:33:00 +01:00
Julien Duponchelle
9d0d5da3fc On OSX execute all sudo in a single operation
Fix #1687
2016-12-01 12:10:46 +01:00
Julien Duponchelle
26d188f228 Catch key Compute is missing during conversion error
Fix #1690
2016-12-01 11:12:21 +01:00
Julien Duponchelle
c7af6c2ae2 Fix rare crash in gns3.dialogs.appliance_wizard in validateCurrentPage
Fix #1691
2016-12-01 10:52:28 +01:00
Julien Duponchelle
8707113db8 Fix AttributeError: 'Nat' object has no attribute 'configPage'
Fix #1685
2016-12-01 10:10:19 +01:00
Julien Duponchelle
e88b4814e7 Catch one more RuntimeError: wrapped C/C++
Fix #1692
2016-12-01 09:56:19 +01:00
Julien Duponchelle
97926fe6d5 Fix a rare crash in port
Fix #1688
2016-12-01 09:46:14 +01:00
Julien Duponchelle
4fb9d46953 Fix a rare crash when set symbol
Fix #1689
2016-12-01 09:44:57 +01:00
Julien Duponchelle
c066f7f3df Fix a potential crash 2016-11-29 08:51:33 +01:00
Julien Duponchelle
5117ef8a58 Fix a potential crash at exit 2016-11-28 20:00:03 +01:00
Julien Duponchelle
35ebb39e22 Fix crashes 2016-11-28 14:41:54 +01:00
Julien Duponchelle
66f8c19478 Remove unused settings from general preferences
Fix #1681
2016-11-28 14:17:01 +01:00
Julien Duponchelle
e07ea40b24 Catch error when you try to import a IOU bin as a licence
Fix #1679
2016-11-28 11:08:34 +01:00
Julien Duponchelle
149bc38b2b Fix rare crash when exiting
Fix #1676
2016-11-28 11:06:45 +01:00
Julien Duponchelle
23ca81ccc4 Fix crash when freeing some ressources
Fix #1678, #1677
2016-11-28 11:05:01 +01:00
Julien Duponchelle
b4ab559b00 Fix timeout when exporting large project
Fix https://github.com/GNS3/gns3-server/issues/797
2016-11-25 17:32:09 +01:00
Julien Duponchelle
ad88735e63 Avoid a rare crash when we free a port
Fix #1672
2016-11-25 17:20:16 +01:00
Julien Duponchelle
66ec4f9de5 Fix you can't download symbols after you got an error
Fix #1673
2016-11-25 17:15:41 +01:00
Julien Duponchelle
35d6b424e6 2.0.0dev5 2016-11-24 12:52:54 +01:00
Julien Duponchelle
eb6b099fa1 2.0.0a4 2016-11-24 12:09:33 +01:00
grossmj
b8c1a7a4c9 Some cosmetic for the setup wizard. 2016-11-23 23:06:36 +11:00
Julien Duponchelle
bc07bdaf74 Fix project open (merge error) 2016-11-22 18:41:20 +01:00
Jeremy Grossmann
3109befe73 Merge pull request #1491 from GNS3/dissallow_unknow
Dissallow unknown extensions
2016-11-22 19:02:27 +10:30
Julien Duponchelle
044683de21 Mark preferences changes when you change a QPlainTextEdit 2016-11-21 18:16:02 +01:00
Julien Duponchelle
e607663575 Force the VPCS config initial file
Ref #1655
2016-11-21 17:11:22 +01:00
Julien Duponchelle
abcff7192b Replace the IOU licence path by an input text
Ref #1662
2016-11-21 15:29:53 +01:00
Julien Duponchelle
bc43c43c53 Fix 403 when loading a remote project
Fix #782
2016-11-18 17:59:30 +01:00
Julien Duponchelle
dbc7c4675f Fix some possible PID issues
Fix #1658
2016-11-18 16:45:55 +01:00
Julien Duponchelle
3f0e23f34d Hide the connection refused dialog when we success to reconnect
Fix #1665
2016-11-18 12:36:59 +01:00
Julien Duponchelle
ef7bd7c77c Avoid a rare crash when changing topology 2016-11-18 09:43:43 +01:00
Julien Duponchelle
77950c8a2c When loading another project disconnect from current project
Ref #1646
2016-11-17 12:09:21 +01:00
Julien Duponchelle
a801b5b21f Do not crash if we can't list remote list of GNS3 VM engines 2016-11-16 12:02:18 +01:00
Julien Duponchelle
d637a6dcac Init the VPCS base config
Fix #1655
2016-11-16 10:33:36 +01:00
Julien Duponchelle
8370a22966 Fix invalid ressource path on OSX 2016-11-15 11:41:06 +01:00
Julien Duponchelle
7ea8c8b8f1 Disable the usage of the wheel for Qt 5.7 2016-11-15 11:37:57 +01:00
Julien Duponchelle
11ab87245b Crash fix 2016-11-15 11:33:06 +01:00
Julien Duponchelle
cf63b49b82 Fix segfault when deleting a node
Ref #1654
2016-11-14 19:05:01 +01:00
Julien Duponchelle
030384c990 Do not download multiple time the same symbol
Fix #1661
2016-11-14 16:04:19 +01:00
Julien Duponchelle
49c734b52c Kill tail process when capture stop
Fix #772
2016-11-14 10:41:09 +01:00
pyup.io bot
2a7b6144d6 Update pytest from 3.0.3 to 3.0.4 (#1657) 2016-11-14 09:45:47 +01:00
Julien Duponchelle
f1ecc0cc15 Fix Topology summary contain non existing links
Fix #1640
2016-11-11 11:18:29 +01:00
Julien Duponchelle
adb7663d03 Fix a rare crash when deleting a link
Fix #1653
2016-11-11 09:59:54 +01:00
pyup.io bot
118b0a85d2 Update pytest-timeout from 1.0.0 to 1.2.0 (#1652) 2016-11-10 22:54:29 +01:00
Julien Duponchelle
5fcf1e156d Fix export of debug informations when not connected to the controller
Fix #1650
2016-11-10 13:29:30 +01:00
Julien Duponchelle
5272befb60 Fix AttributeError: 'DockerVM' object has no attribute 'server'
Fix #1647
2016-11-06 21:49:17 +01:00
Julien Duponchelle
b10a524496 Fix error message if you double click on builtin switch
Fix #1642
2016-11-04 13:34:43 +01:00
Julien Duponchelle
de7417787b Fix a rare crash in packet capture
Fix #1643
2016-11-04 13:28:16 +01:00
Julien Duponchelle
a00b80529b Restrict ubridge to admin users on OSX 2016-11-04 09:50:14 +01:00
Julien Duponchelle
b1fa44d176 Natural sort of Nodes in topology summary
Fix #1634
2016-11-03 19:53:36 +01:00
Julien Duponchelle
8251b460ad Drop serial console type
Fix https://github.com/GNS3/gns3-server/issues/748
2016-11-03 18:56:39 +01:00
Julien Duponchelle
4403044584 Fix syntax error 2016-11-03 18:45:42 +01:00
Julien Duponchelle
39c7a56041 Display an error if you try to open a 0.8.x file
Fix #1639
2016-11-03 15:15:03 +01:00
Julien Duponchelle
383dbb5fca Fix tab order when editing a compute 2016-11-02 13:35:04 +01:00
Julien Duponchelle
fc94e784cc Fix a crash in ethernet switch settings
Fix #1635
2016-11-02 11:58:24 +01:00
Julien Duponchelle
37937b0096 2.0.0dev4 2016-10-28 19:38:32 +02:00
Julien Duponchelle
3e9d303ec4 Update changelog 2016-10-28 19:25:26 +02:00
Julien Duponchelle
68908e17d3 2.0.0 alpha 3 2016-10-28 19:23:51 +02:00
Julien Duponchelle
026f482b1b Merge branch 'master' into 2.0 2016-10-28 10:29:28 +02:00
Julien Duponchelle
879784cd67 Fix error when opening a project from the cli with a gns3 installed via setup.py
Fix #1581
2016-10-28 10:27:07 +02:00
Julien Duponchelle
8b8cbb4d74 Fix a rare crash in snapshot dialog
Fix #1627
2016-10-28 09:54:20 +02:00
Julien Duponchelle
40783eba15 Fix crash when importing project on a remote server
Fix #1628
2016-10-28 09:52:52 +02:00
Julien Duponchelle
e63d20ce48 Fix crash in appliance wizard
Fix #1624
2016-10-27 21:01:11 +02:00
Julien Duponchelle
f6ecc4d0bb Fix crash when local server is not available 2016-10-27 20:14:36 +02:00
Julien Duponchelle
64339e6846 Disallow to overwrite a running project
Fix https://github.com/GNS3/gns3-server/issues/745
2016-10-27 16:02:08 +02:00
Julien Duponchelle
2fe73056f0 Fix a rare crash when deleting a link
Fix #1623
2016-10-27 15:29:03 +02:00
Julien Duponchelle
95051035a9 Merge branch 'master' into 2.0 2016-10-27 12:30:27 +02:00
Julien Duponchelle
af6294fe7e Fix appliance with wrong file name after import 2016-10-27 12:29:32 +02:00
Julien Duponchelle
2fd53be083 Fix a crash at startup on Mac when coming from old GNS3 version
Fix #1622
2016-10-26 17:51:07 +02:00
Julien Duponchelle
93199e3695 Fix a crash 2016-10-26 17:04:54 +02:00
Julien Duponchelle
d0a3051656 Fix key error in settings if a compute no longer exists
Fix #1612
2016-10-26 16:16:55 +02:00
Julien Duponchelle
516e84d328 All check for vmware linked base are already made server side
Fix #1609
2016-10-26 15:50:13 +02:00
Julien Duponchelle
a06e4cb7ba Fix Save as is not switching to the saved project
Fix #1620
2016-10-26 15:37:13 +02:00
Julien Duponchelle
a1dd5fee9f Auto reopen a project if connection is lost
Fix #1619
2016-10-26 15:28:25 +02:00
Julien Duponchelle
7e00ac4e50 Empty the list of computes nodes when connection is lost
Ref #1619
2016-10-26 15:01:17 +02:00
Julien Duponchelle
59201e8af6 Try to fix duplicate nodes after snapshot restore on some user computer
Fix #1571
2016-10-26 11:28:35 +02:00
Julien Duponchelle
fbde9e0746 Allow only IPV4 in setup wizard
Fix #1608
2016-10-26 10:55:51 +02:00
Julien Duponchelle
f714add057 Catch error if user tmp directory is read only
Fix #1614
2016-10-26 10:25:59 +02:00
Julien Duponchelle
ce2724a892 Raise a proper error if packet capture program is invalid
Fix #1597
2016-10-26 10:23:20 +02:00
Julien Duponchelle
aff475cd5b Fix AttributeError: 'NoneType' object has no attribute 'upper'
Fix #1615
2016-10-26 10:20:55 +02:00
Julien Duponchelle
b18d7c7cc5 Fix rare crash when killing wireshark
Fix #1616
2016-10-26 10:19:00 +02:00
Julien Duponchelle
687088cbd7 Export debug informations also from the controller
Ref #1562, https://github.com/GNS3/gns3-server/issues/740
2016-10-25 11:41:49 +02:00
Julien Duponchelle
31fc18b150 Fix a crash in vm wizard
Fix #1610
2016-10-25 10:41:09 +02:00
Julien Duponchelle
dd960d5556 Fix an invalid test 2016-10-25 10:39:25 +02:00
Julien Duponchelle
004d9f90e3 Fix error when uploading an images from preferences
Fix #1594
2016-10-25 10:27:56 +02:00
Julien Duponchelle
f8f7ee686e Fix snap to grid when initialy drop a node in the topology 2016-10-24 21:58:47 +02:00
Bernhard Ehlers
c695e565ea Optimize snap-to-grid code
Signed-off-by: Julien Duponchelle <julien@gns3.net>

Fix #1599
2016-10-24 21:45:01 +02:00
Julien Duponchelle
d32c3ebebe Fix a crash with linked clone 2016-10-24 19:20:41 +02:00
Julien Duponchelle
b863bae4e7 Move prevent using twice the same VM when linked clone is not enable
Ref #1593
2016-10-24 18:14:01 +02:00
Julien Duponchelle
8f033e8bd3 Fix If you show interface label and delete the link ghost interface label will appear
Fix #1607
2016-10-24 17:40:51 +02:00
Julien Duponchelle
c8776486c5 Display short interface label instead of long version
Fix #1606
2016-10-24 17:30:34 +02:00
Julien Duponchelle
679e9ad4bf Fix error AttributeError: 'NoneType' object has no attribute 'capabilities'
Fix #1595
2016-10-24 17:15:17 +02:00
Julien Duponchelle
877b255f23 Fix PermissionError when killing local server
Fix #1601
2016-10-24 17:03:18 +02:00
Julien Duponchelle
e404716f88 Handle empty color
Fix #1602
2016-10-24 17:01:35 +02:00
Julien Duponchelle
4e58df60ea Fix rare crash in save as
Fix #1592
2016-10-24 16:58:56 +02:00
Julien Duponchelle
e7f761c8d6 Fix crash in restore default server settings
Fix #1603
2016-10-24 16:51:52 +02:00
Julien Duponchelle
50ca85b7ce Fix an error during import of some 0.8x projects
Fix #1578
2016-10-21 10:55:26 +02:00
Julien Duponchelle
9659c29dd0 2.0.0dev3 2016-10-20 22:12:39 +02:00
Julien Duponchelle
535f3193db 2.0.0a2 2016-10-20 21:33:01 +02:00
Julien Duponchelle
7fae1eac48 Support pure remote server for importing appliance
Fix #1590
2016-10-20 21:24:38 +02:00
Julien Duponchelle
4cc2975e97 Dissallow bindingi GNS3 server to an IPV6 (not supported by some emulators)
Ref https://github.com/GNS3/gns3-server/issues/725
2016-10-20 09:51:17 +02:00
Julien Duponchelle
149d1ad590 Drop vmware host type choice in client
Fix https://github.com/GNS3/gns3-gui/issues/1579
2016-10-19 12:05:21 +02:00
Julien Duponchelle
abc900a764 Merge branch 'master' into 2.0 2016-10-19 10:51:12 +02:00
Julien Duponchelle
14084192da Ask for restart after installing vmrun
Fix https://github.com/GNS3/gns3-server/issues/724
2016-10-19 10:50:07 +02:00
Julien Duponchelle
2f6603070b Ask user to restart GNS3 after VMware installation
Ref https://github.com/GNS3/gns3-server/issues/724
2016-10-19 10:29:11 +02:00
Julien Duponchelle
c3aac9f0a6 Improve duplicate prevention in topology summary
Fix #1571
2016-10-19 09:01:23 +02:00
Julien Duponchelle
afde80dab5 Add a duplicate button in the project library dialog
Fix #1585
2016-10-18 16:55:49 +02:00
pyup.io bot
b7f68afbf1 Update dependencies from pyup
* Pin pep8 to latest version 1.7.0
* Pin pypiwin32 to latest version 219
* Pin pytest to latest version 3.0.3
* Pin pytest-capturelog to latest version 0.7
* Pin pytest-pythonpath to latest version 0.7.1

* Pin pytest-timeout to latest version 1.0.0
2016-10-18 15:21:44 +02:00
Julien Duponchelle
08634f330c Fix error introduce in previous commits 2016-10-18 12:04:55 +02:00
Julien Duponchelle
83e35e6aa0 Fix duplicates in recent project list
Fix #1584
2016-10-18 11:28:40 +02:00
Julien Duponchelle
dc3bfef038 Fix a project override error 2016-10-18 11:26:18 +02:00
Julien Duponchelle
c13bb77b08 Fix Duplicated node in node summary when restoring a snapshot
Fix #1571
2016-10-18 10:12:36 +02:00
Julien Duponchelle
fab621bb40 Try pyup.io 2016-10-18 09:41:27 +02:00
Julien Duponchelle
bb760cd861 Fix a crash in the VMware / VirtualBox wizard 2016-10-17 18:44:00 +02:00
Julien Duponchelle
59e7b3fd93 If console host is 0.0.0.0 use controller address 2016-10-17 18:30:44 +02:00
Julien Duponchelle
315870aa09 Fix save issue when importing an appliance
Fix #1564
2016-10-17 14:26:01 +02:00
Julien Duponchelle
5ed17acd6b Strip HTML in console view logs and log files
Fix #1576
2016-10-17 10:01:55 +02:00
Julien Duponchelle
d0dde822dd Fix TypeError: _expandAllSlot() takes 1 positional argument but 2 were given
Fix #1577
2016-10-14 23:02:22 +02:00
Julien Duponchelle
e3023532ed Fix Cannot open created project by using Recents projects
Fix #1544
2016-10-14 22:57:19 +02:00
grossmj
2a4c229c9d Update edit project Ui. 2016-10-08 14:51:00 -06:00
Julien Duponchelle
7b77627db7 Update crash report key 2016-10-07 10:31:36 +02:00
Julien Duponchelle
fd5f32bcc4 Fix a crash when exporting debug without project open 2016-10-05 16:29:30 +02:00
Julien Duponchelle
7e7eecfa3e Fix a crash 2016-10-05 16:28:33 +02:00
Julien Duponchelle
8325339b58 Fix a crash in rare condition when logging informations to the console 2016-10-05 11:35:47 +02:00
Julien Duponchelle
b39185ceb3 Fix a crash in compute summary view 2016-10-05 11:01:55 +02:00
Julien Duponchelle
d93100f3c3 Add a text about how to change the topology size in 2.0 in general preferences
Fix #1561
2016-10-05 10:36:11 +02:00
Julien Duponchelle
1691558989 Merge branch 'master' into 2.0 2016-10-05 09:36:11 +02:00
Julien Duponchelle
17cabba48f Improve warning when connection issue to GNS3 VM 2016-10-05 09:32:31 +02:00
Julien Duponchelle
c9a2cc05ea Fix crash in setup wizard 2016-10-04 21:11:18 +02:00
Julien Duponchelle
f9ac5302ca Fix the wizard for creating appliance template doesn't support remote main server
Fix #1550
2016-10-04 20:44:32 +02:00
Julien Duponchelle
7c8c8dfc20 Appliance wizard support remote controller
Ref #1550
2016-10-04 19:22:47 +02:00
Julien Duponchelle
6622bc7560 Fix Browse button is not working in the local server page in the setup wizard
Fix #1559
2016-10-04 17:14:21 +02:00
Julien Duponchelle
3df7bd99eb Fix test 2016-10-04 17:11:40 +02:00
Julien Duponchelle
da1636af60 Check if local server is running in the setup wizard
Fix #1560
2016-10-04 16:45:36 +02:00
Julien Duponchelle
2378d7ff78 Hide setup wizard after first successful run 2016-10-04 16:00:59 +02:00
Julien Duponchelle
c2f357cc0e Import appliance and New project are display at the same time
Fix #1558
2016-10-04 15:58:33 +02:00
Julien Duponchelle
f2bd85d803 Support remote controller in the setup wizard
Fix #1549
2016-10-04 15:44:56 +02:00
Julien Duponchelle
ec3a856c59 Fix When importing a gns3a the correct qemu binary is not selected
Fix #1556
2016-10-03 22:56:13 +02:00
Julien Duponchelle
1a55487e7b Increase creation timeout for docker container 2016-10-03 22:30:54 +02:00
Julien Duponchelle
0f0ac33345 Make WaitForLambdaWorker more crash proof 2016-10-03 18:41:36 +02:00
Julien Duponchelle
d0b4e5045b Fix a crash when importing appliance
Fix #705
2016-10-03 18:30:36 +02:00
Julien Duponchelle
393cff0343 Fix error in import appliances 2016-10-03 17:24:15 +02:00
Julien Duponchelle
8e56b59bb8 Try to fix the a segfault when importing appliance
Fix https://github.com/GNS3/gns3-server/issues/705
2016-10-03 15:38:56 +02:00
Julien Duponchelle
a939dfe37f Fix crash in upload images 2016-10-03 12:34:51 +02:00
Julien Duponchelle
9b7e06bd54 Trust the server for link creation error (avoid sync issue)
Fix #1553
2016-10-03 12:31:53 +02:00
Julien Duponchelle
cc2f7920b8 Try to fix crash when import appliances
Fix #705
2016-10-03 10:44:37 +02:00
Julien Duponchelle
66a69dd22d Fix an Error in server preference page
Fix #1551
2016-10-03 10:36:58 +02:00
Julien Duponchelle
d3f7eaee1c Fix compatibility with remote server of 1.X
Fix https://github.com/GNS3/gns3-server/issues/696
2016-09-30 16:04:00 +02:00
Julien Duponchelle
3200f4cd28 New appliance dialog should not be display if you cancel the setup wizard
Fix #1545
2016-09-30 10:56:01 +02:00
Julien Duponchelle
fc04b30b7e 2.0.0dev2 2016-09-29 20:41:50 +02:00
Julien Duponchelle
716f65786d 2.0.0 alpha 1 2016-09-29 16:59:35 +02:00
Julien Duponchelle
0c397ba0a8 Retry connection to the server
Fix #1542
2016-09-29 11:38:44 +02:00
Julien Duponchelle
bff8a09147 Change seperator for images directories otherwise bug on windows 2016-09-27 17:09:53 +02:00
Julien Duponchelle
f65ea13c6f Fix tests on Windows 2016-09-27 14:21:59 +02:00
Julien Duponchelle
3975b09898 Add 0.0.0.0 to the list of possible host binding
Fix #1506
2016-09-27 12:03:18 +02:00
Julien Duponchelle
6525d3130c At the end of the setup wizard display add appliance dialog
Fix #1528
2016-09-26 15:45:16 +02:00
Julien Duponchelle
5d8ad83cbe Make first screen of the setup wizard more welcome
Fix #1529
2016-09-23 18:55:11 +02:00
Julien Duponchelle
13a819ee87 Display serial link with a serial line
Fix #1538
2016-09-23 10:36:22 +02:00
grossmj
cd1beb5191 Changes spacer in project dialog. 2016-09-22 22:22:15 -06:00
Julien Duponchelle
483259ba2c Fill cloud interface server side
Fix https://github.com/GNS3/gns3-gui/issues/1535
2016-09-22 17:47:26 +02:00
Julien Duponchelle
3d828c42de Fix tests 2016-09-22 15:55:39 +02:00
Julien Duponchelle
d70dbe82d7 Fix invalid port number for cloud
Fix #1533
2016-09-22 15:44:23 +02:00
Julien Duponchelle
243c5a0f82 Display an error if can't create capture file
Fix #1532
2016-09-22 14:48:43 +02:00
Julien Duponchelle
f4470d8190 Try to improve to speed of Project Dialog with large number of project
Fix #1525
2016-09-22 14:40:58 +02:00
Julien Duponchelle
b435163a3c Prevent a crash in VPCS
Fix #1531
2016-09-22 11:44:47 +02:00
Julien Duponchelle
26f6315b69 Better protection of topologies summary slots
Fix #1526
2016-09-22 11:19:01 +02:00
Julien Duponchelle
b81a02fbb4 Update crash report key 2016-09-22 10:47:02 +02:00
Julien Duponchelle
b19988784f Merge branch 'master' into 2.0 2016-09-21 17:51:20 +02:00
Julien Duponchelle
5caf576f83 Add vpcus and ram settings for GNS3 VM in preferences pages
Fix #1445
2016-09-21 17:08:15 +02:00
Julien Duponchelle
e61b132c93 Fix a bug in setup wizard when you reduce the ram and increase it 2016-09-21 14:55:51 +02:00
Julien Duponchelle
41b826e9ac Make it clear that asa will not work on Windows 10 2016-09-21 12:34:54 +02:00
Julien Duponchelle
4079f19e25 Repare setup wizard
Ref #1329
2016-09-21 12:01:00 +02:00
Julien Duponchelle
04f108cbdf Fix multi drop
Fix #1524
2016-09-21 10:07:53 +02:00
Julien Duponchelle
e222e3e7c2 Allow to change the size of the scene
https://github.com/GNS3/gns3-server/issues/683
2016-09-21 09:44:13 +02:00
grossmj
cb687205a4 Change text in GNS3 VM preferences. 2016-09-20 20:41:55 -06:00
grossmj
4ef19056bb Merge remote-tracking branch 'origin/master' 2016-09-20 17:46:56 -06:00
grossmj
8a4fb55cdf Changes wording in VM wizards. 2016-09-20 17:46:42 -06:00
Julien Duponchelle
805b573370 Fix port name formatting
Fix #1523
2016-09-20 16:34:19 +02:00
Julien Duponchelle
3f87719d02 Bring back import devices from 1.5
Fix #1411
2016-09-20 10:53:26 +02:00
Julien Duponchelle
2a27bb560c Fix a traceback when dragging an IOS router
Fix #1522
2016-09-20 09:51:39 +02:00
Julien Duponchelle
73dbe68301 Display an error if winpcap or npcap is not installed
Fix https://github.com/GNS3/gns3-server/issues/674
2016-09-20 09:43:50 +02:00
grossmj
c2bd4c8984 Force content margins for tab layouts. 2016-09-19 11:17:23 -06:00
Julien Duponchelle
3021cfb164 Fix crash in cloud 2016-09-18 21:45:36 +02:00
Julien Duponchelle
794f317f83 Fix crash in nat node 2016-09-15 18:42:10 +02:00
Julien Duponchelle
e357581587 Fix Sometimes "Duplicate" put text too far in topology
Fix #1501
2016-09-15 18:36:17 +02:00
Julien Duponchelle
e46b699fcb Custom symbol from gns3a are not working
Fix #1515
2016-09-15 18:02:33 +02:00
Julien Duponchelle
c426ea6e03 Allow blank password for remote controller
Fix #1497
2016-09-15 17:36:58 +02:00
Julien Duponchelle
b0a1fdb65a Fix a crash
Fix #1516
2016-09-15 17:22:16 +02:00
Julien Duponchelle
4515620259 Fix import issue with gns3a
Fix #1507
2016-09-15 17:20:16 +02:00
Julien Duponchelle
c36695c59a Dissallow unknown extensions
Fix #1490
2016-09-15 17:12:09 +02:00
Julien Duponchelle
090caa967b Do not show progress when reconnect to fail controller
Fix #1496
2016-09-15 16:20:00 +02:00
Julien Duponchelle
e9ac774464 Do not show progress when refresh list of computes
Fix #1498
2016-09-15 16:15:30 +02:00
Julien Duponchelle
de2de45196 Fix crash in Dynamips preferences
Fix #1513
2016-09-15 16:11:54 +02:00
Julien Duponchelle
64dd0e0be3 Display errors about link creation 2016-09-15 14:49:35 +02:00
Julien Duponchelle
6cd5438c89 Fix duplicate in some cases for interfaces
Fix #1502
2016-09-15 11:50:31 +02:00
Julien Duponchelle
df5a09b46d Fix save project as location
Fix #1503
2016-09-14 19:12:28 +02:00
Julien Duponchelle
cba03a7539 Display error when you can't save as 2016-09-14 17:46:32 +02:00
Julien Duponchelle
069d02d908 Fix a crash when deleting an ethernet switch
Fix #1511
2016-09-14 17:22:19 +02:00
Julien Duponchelle
a207ba61de Fix crash in cloud settings 2016-09-14 16:49:11 +02:00
Julien Duponchelle
496db6427d Update node when you release the mouse
Fix #1494
2016-09-14 15:52:26 +02:00
Julien Duponchelle
1bcf6699f0 Get port names from server
Ref #1502, https://github.com/GNS3/gns3-server/issues/676
2016-09-14 15:30:21 +02:00
Julien Duponchelle
fa12721c3c Cleanup dead code around ports 2016-09-14 11:42:07 +02:00
Julien Duponchelle
65ad43431c Allow node are hotplug now 2016-09-14 11:42:07 +02:00
Julien Duponchelle
081544e778 ports => ports_mapping to avoid confusion 2016-09-14 11:42:07 +02:00
Julien Duponchelle
a30daf03d4 Fix Can't remove a slot from dynamips device
Fix #1510
2016-09-14 11:41:22 +02:00
Jeremy Grossmann
f80af230af Merge pull request #1425 from GNS3/colorblind
Support for colorblind people
2016-09-13 14:33:03 -06:00
Julien Duponchelle
71125a4b58 Fix crash in frame relay switch
Fix #1500
2016-09-13 16:09:21 +02:00
Julien Duponchelle
c1dfaf13fb Force icon size in nodes view 2016-09-08 22:25:23 +02:00
Julien Duponchelle
03a3081361 More debug for #1493 2016-09-08 22:02:27 +02:00
Julien Duponchelle
b5715e46d2 Merge branch 'master' into 2.0 2016-09-08 20:56:12 +02:00
Jeremy Grossmann
3aa9ed61c6 Merge pull request #1488 from GNS3/error_bad_version
Display an error if settings come from a more recent version of GNS3
2016-09-08 12:01:14 -06:00
grossmj
0b6cc588b6 Changed sentence. 2016-09-08 12:00:47 -06:00
Julien Duponchelle
ed4af4a8e7 Avoid to be spam by GNS3 VM errors 2016-09-08 19:52:08 +02:00
Julien Duponchelle
c1f6c3ddeb No error is display if refresh the list of GNS3 VM failed in settings
Fix #1492
2016-09-08 19:02:22 +02:00
Julien Duponchelle
d57652815e Support for colorblind people
Fix #1340
2016-09-08 16:22:37 +02:00
Julien Duponchelle
ea3191739b Fix tests 2016-09-08 16:21:39 +02:00
Julien Duponchelle
378b4f973b Display download url for the GNS3 VM in the preferences
Like in the setup wizard

Ref #1489
2016-09-08 16:00:18 +02:00
Julien Duponchelle
6ecbe59011 Suspend the GNS3 VM
Fix https://github.com/GNS3/gns3-server/issues/656
2016-09-08 15:31:42 +02:00
Julien Duponchelle
2fcaa1f6cc Fix crash in server preferences page 2016-09-08 15:02:59 +02:00
Julien Duponchelle
f0a7582fd2 Display an error if settings come from a more recent version of GNS3
GNS3 patch level version are compatible (ex 1.5.3 and 1.5.2). But if you open
settings from 1.6.1 with 1.5.1 you will have an error

Fix #1487
2016-09-08 12:54:32 +02:00
Julien Duponchelle
2da076a501 Remove a todo already done in another place of the code 2016-09-08 12:30:35 +02:00
Julien Duponchelle
f31cc4806f Fix error when shutdown local server
Fix https://github.com/GNS3/gns3-server/issues/673
2016-09-08 12:29:15 +02:00
Julien Duponchelle
ae228988a5 Merge branch 'master' into 2.0 2016-09-08 12:16:24 +02:00
Julien Duponchelle
185f0463d4 Fix Error when no GNS3 VM is configured and you click on new Docker or IOU
Fix #1486
2016-09-08 12:14:16 +02:00
Julien Duponchelle
cf668e774b Remove the setting slow_device_start_all
Fix #1485
2016-09-08 10:36:45 +02:00
Julien Duponchelle
b933cdd950 Fix a crash when frozen 2016-09-08 10:21:52 +02:00
Julien Duponchelle
f38d3bdb8e Allow to enable profile selection at startup
Fix #1484
2016-09-07 20:23:27 +02:00
Julien Duponchelle
2c5ed9c884 Disable setup wizard 2016-09-07 18:40:31 +02:00
Julien Duponchelle
f2e457de6c Fix VPCS lost his config
Fix #628
2016-09-07 16:46:24 +02:00
Julien Duponchelle
6b146fc7a7 Process node update events
Fix https://github.com/GNS3/gns3-gui/issues/1482, https://github.com/GNS3/gns3-gui/issues/1483
2016-09-07 15:52:30 +02:00
Julien Duponchelle
25b44a2070 Merge branch 'master' into 2.0 2016-09-07 14:21:12 +02:00
Julien Duponchelle
ac8ef05b06 Disallow / in docker container name
Fix https://github.com/GNS3/gns3-server/issues/668
2016-09-07 14:20:25 +02:00
Julien Duponchelle
663185d93e Add hub, switch and cloud in the new template appliance dialog
Fix #1479
2016-09-07 12:18:37 +02:00
Julien Duponchelle
61ac6ed6e0 Fix crash at the end of the cloud wizard
Fix #1480
2016-09-07 11:38:50 +02:00
Julien Duponchelle
0fa51bcd36 Add a default VPCS node
Fix #1382
2016-09-07 10:50:09 +02:00
Julien Duponchelle
f000425350 Fix After adding a router via the new appliance button the settings are not saved
Fix #1478
2016-09-07 10:22:45 +02:00
Julien Duponchelle
8df07808a9 Fix a crash in IOS template edit 2016-09-07 10:22:27 +02:00
Julien Duponchelle
7f169261d4 Remove --controller 2016-09-07 10:08:37 +02:00
Julien Duponchelle
a86c728a99 Fix error when dragging a node and not default Z
Fix #1477
2016-09-07 09:52:52 +02:00
Julien Duponchelle
11d56d1ea7 Fix can't open two GUI
Fix #1475
2016-09-06 18:44:13 +02:00
Julien Duponchelle
9c52cf0b0b Fix layer is lost & PEP8 cleanup
Fix #1473
2016-09-06 18:17:01 +02:00
Julien Duponchelle
cd96492dff Set a minimum height for item in nodes dock
Fix #1476
2016-09-06 17:57:31 +02:00
Julien Duponchelle
43ab1deb24 Fix position are lost when editing multiple items
Fix #1472
2016-09-06 17:51:54 +02:00
Julien Duponchelle
646bf10017 When multiple node are selected select the group config by default 2016-09-06 16:24:04 +02:00
Julien Duponchelle
6a23874054 Test port name 2016-09-06 15:17:44 +02:00
Julien Duponchelle
b6fe18b975 Fix icon missing in node dock
Fix #1471
2016-09-06 14:48:04 +02:00
Julien Duponchelle
84125fe463 Support right click on VPCS template
Ref #1382
2016-09-06 14:11:53 +02:00
Julien Duponchelle
12732715bd Use the VPCS icon
Ref #1382
2016-09-06 13:31:24 +02:00
Julien Duponchelle
ece0b94ae8 Add VPCS template from the new appliance button
Ref #1382
2016-09-06 13:25:12 +02:00
Julien Duponchelle
84d0532039 Support port name format
Fix  #1400, https://github.com/GNS3/gns3-server/issues/667
2016-09-06 11:46:14 +02:00
Julien Duponchelle
ab68c1f1ab Fix crash with empty config 2016-09-06 10:13:14 +02:00
Julien Duponchelle
b360d8d931 Detection of application outside /Applications more reliable 2016-09-06 09:52:27 +02:00
Julien Duponchelle
f9681f2766 Fix When reloading a topology with Nat we can't link
Fix #1470
2016-09-05 18:54:24 +02:00
Julien Duponchelle
e8a09eef72 Repare console open 2016-09-05 18:08:59 +02:00
Julien Duponchelle
3290639e54 Fix I can't change list of adapters
Fix #1467
2016-09-05 15:27:24 +02:00
Julien Duponchelle
02d3275475 Fix crash when changing IOU symbol
Fix #1463
2016-09-05 14:33:56 +02:00
Julien Duponchelle
fd92049cda Remove the internet VM when migrate to 2.0
Fix https://github.com/GNS3/gns3-server/issues/658
2016-09-05 11:28:51 +02:00
Julien Duponchelle
f48a5655ed Display an error if GNS3 is start from outside /Application
Fix #1468
2016-09-05 09:20:44 +02:00
Julien Duponchelle
22267b4123 Fix a crash when you cancel connection to the controller 2016-09-05 09:08:19 +02:00
Julien Duponchelle
b06230ef3d Try to reconnect to the controller if connection failed
Fix #1459
2016-09-05 08:49:01 +02:00
Bernhard Ehlers
87b37e2839 Update iTerm3 console settings
Fix #1466

Signed-off-by: Julien Duponchelle <julien@gns3.net>
2016-09-03 22:07:45 +02:00
Julien Duponchelle
0f3dd2e05d Merge branch 'master' into 2.0 2016-09-03 21:53:54 +02:00
Julien Duponchelle
0495429df8 Fix rename ethernet switch doesn't release the name
Fix #1460
2016-09-03 21:50:58 +02:00
Julien Duponchelle
2e97f1e037 Fix sometimes console host is missing
Fix #1464
2016-09-02 16:26:52 +02:00
Julien Duponchelle
8133ba61a5 Fix tests 2016-09-02 16:15:47 +02:00
Julien Duponchelle
4cce5cd4ff Fix capture status is incorrect when reconnect to a project
Fix #1464
2016-09-02 15:56:47 +02:00
Julien Duponchelle
1f1f95e3da Fix creation of drawing 2016-09-02 12:02:44 +02:00
Julien Duponchelle
7c9c66470d Support pcap capture on remote controller
Ref #1280
2016-09-02 10:57:06 +02:00
Julien Duponchelle
da5bead39c Fix tests 2016-09-01 16:35:43 +02:00
Julien Duponchelle
68a0c74a1f Improve display of node error (also for GNS3 VM errors)
Fix #1446
2016-09-01 15:38:11 +02:00
Julien Duponchelle
949d1a900a Remove MessageBox it was almost never used 2016-09-01 13:28:34 +02:00
Julien Duponchelle
88c7a472b1 Handle error when controller is not available
Fix #1373
2016-09-01 10:36:01 +02:00
Julien Duponchelle
4b49501630 Fix open project from the CLI
Fix #1456
2016-09-01 10:27:56 +02:00
Julien Duponchelle
9eb71b870a Temporary deactivate setup wizard
Ref #1329
2016-09-01 10:10:09 +02:00
Julien Duponchelle
5d46560427 Allow to select multiple project to delete 2016-08-31 17:18:51 +02:00
Julien Duponchelle
d7856af6db Fix Sometimes node are duplicated
Fix #1455
2016-08-31 16:54:04 +02:00
Julien Duponchelle
c0a093d044 Support overwrite existing project
Fix #1435
2016-08-31 15:38:56 +02:00
Julien Duponchelle
03c7df9dad Improve a lot the speed for parsing notifications
This use a different signal to detect that data is downloaded.
With that, detection of new notification is instant when you
have multiple notifications.

Fix #1447
2016-08-31 11:34:03 +02:00
Julien Duponchelle
d847316914 Merge branch 'master' into 2.0 2016-08-31 09:24:52 +02:00
Julien Duponchelle
190e17b445 Fix tests 2016-08-31 09:23:14 +02:00
Julien Duponchelle
ce5d1b56f0 Support for VNC display number in command line replacement
Fix #1453
2016-08-31 09:20:50 +02:00
Julien Duponchelle
cb7cbc15b3 Merge branch 'master' into 2.0 2016-08-31 09:11:17 +02:00
Julien Duponchelle
dc6756da04 Fix a crash when a directory with image is not accessible at gns3a
import

Fix #1454
2016-08-31 09:10:01 +02:00
Julien Duponchelle
e4eeb437eb Fix the invalid VM was selected in GNS3 VM preferences
Fix #1452
2016-08-30 10:25:32 +02:00
Julien Duponchelle
81b94070ac Remote GNS3 VM support
Fix https://github.com/GNS3/gns3-server/issues/623
2016-08-30 10:19:36 +02:00
Julien Duponchelle
de36a04d88 Server select use the server capabilities to filter what should be
display
2016-08-29 18:20:02 +02:00
Julien Duponchelle
1287b77dfe Watch for compute status change 2016-08-29 17:49:17 +02:00
Julien Duponchelle
146bb004c0 Save preference page only when the page change
Fix #1443
2016-08-26 15:05:42 +02:00
Julien Duponchelle
d26abecea7 Fix Even when you don't modify the preferences the pref dialog say "You have unsaved preferences."
Fix #1443
2016-08-26 15:02:36 +02:00
Julien Duponchelle
d2da14a951 Display name of preferences pages with changes
Ref #1443
2016-08-26 14:34:25 +02:00
Julien Duponchelle
4ae17a1f66 Prevent a crash 2016-08-26 12:14:02 +02:00
Julien Duponchelle
473f60167d Fix crash in settings 2016-08-25 14:19:55 +02:00
Julien Duponchelle
5377590520 Fix tests 2016-08-25 11:17:15 +02:00
Julien Duponchelle
58fe1f6e2b Stop send --host and --port as parameter to the GNS3 server
The server can already get them from config file. And since
we are using the same config file we have no longer risk of sync
issues.
2016-08-25 11:15:50 +02:00
Julien Duponchelle
c0564c89c8 Send GNS3 VM settings from the GUI
Fix #1441
2016-08-25 09:34:05 +02:00
Julien Duponchelle
34d27dc120 Display a settings button in the project dialog page
Fix #1442
2016-08-24 18:53:05 +02:00
Julien Duponchelle
d907af46bc In case of exception on dev machine exit immediately 2016-08-24 18:44:45 +02:00
Julien Duponchelle
a8055471cd Stop logging vmware error when we don't use vmware
Fix #1439
2016-08-23 21:45:39 +02:00
Julien Duponchelle
317c28eaf2 Fix start of the local server 2016-08-23 19:34:24 +02:00
Julien Duponchelle
d2b6aeddbd Display error when not connected to the controller
Ref #1373
2016-08-23 19:02:05 +02:00
Julien Duponchelle
4285d01a19 Fix crash 2016-08-23 18:44:36 +02:00
Julien Duponchelle
802c59a1d1 Hide in project dialog what is not require for remote controller
Fix #1280
2016-08-23 17:09:26 +02:00
Julien Duponchelle
f1879c8d4b Hide menu that should not be visible when using a remote controller
Ref #1280
2016-08-23 16:42:22 +02:00
Julien Duponchelle
97ad294623 Fix isolation issue between settings profil 2016-08-23 16:35:14 +02:00
Julien Duponchelle
6147dcd304 Allow connection to a remote controller
Ref #1280
2016-08-23 16:11:18 +02:00
Julien Duponchelle
d900842363 Merge branch 'profil_support' into 2.0 2016-08-23 10:07:21 +02:00
Julien Duponchelle
e6ac92abb4 Fix error with Qt 5.2
Fix #1431
2016-08-22 19:02:03 +02:00
Julien Duponchelle
944b5fc6c9 Support for profil settings
This PR add a --profil command line arguments.

If set a new set of settings will be create in
~/GNS3/profiles/PROFILENAME

Otherwise it's use the default location.

This allow to test settings without losing
original.

Also with the correct GUI this allow to switch from
local server to a remote (home / office).
2016-08-22 17:27:37 +02:00
Julien Duponchelle
bf6c645281 Only one location in the code for the path of the configuration file 2016-08-22 15:53:43 +02:00
Julien Duponchelle
14f04e5f88 Fix a status sync issue
Fix #1433
2016-08-22 11:19:43 +02:00
grossmj
ecbf7bd661 Do not add fw0 and p2p0 to the interfaces for cloud and nat objects. FIxes #1436. 2016-08-21 21:47:28 -06:00
Jeremy Grossmann
de15edcd0a Merge pull request #1437 from GNS3/nat
A nat node
2016-08-21 21:45:56 -06:00
grossmj
a5d619d6a8 Update some edit dialogs. 2016-08-20 11:31:17 -06:00
grossmj
271811d376 Cosmetic changes. 2016-08-20 11:10:34 -06:00
grossmj
6ed9652a2a Remove setTabBarAutoHide. Ref #1431. 2016-08-20 10:28:37 -06:00
Julien Duponchelle
0d62811a17 A nat node
Fix #599
2016-08-19 20:01:31 +02:00
Julien Duponchelle
615f1f2b5d Topology schema is now on controller 2016-08-19 19:14:12 +02:00
Julien Duponchelle
721bff01b0 Fix restore snapshots when running two clients
Fix #1417
2016-08-19 17:38:55 +02:00
Julien Duponchelle
69f671106c Fix invalid error message on Linux in qemu wizard 2016-08-19 10:08:21 +02:00
Julien Duponchelle
0c59970974 Merge branch 'master' into 2.0 2016-08-18 22:15:32 +02:00
Julien Duponchelle
d161a75ab2 1.5.3dev1 2016-08-18 22:14:58 +02:00
Julien Duponchelle
a0b4c38a44 Fix qemu wizard 2016-08-18 19:39:02 +02:00
Julien Duponchelle
c9cc98ae39 Display the warning about OSX and Windows not recommended for qemu
even when you have only one server.

Fix #1397
2016-08-18 18:56:49 +02:00
Julien Duponchelle
ad492e2b90 Fix sync between GUI and server.conf
Fix #1330
2016-08-18 18:50:30 +02:00
Julien Duponchelle
a977042017 Fix small crashes 2016-08-18 18:30:49 +02:00
Julien Duponchelle
90b9b2d29c The delete project button is display only for the project library 2016-08-18 17:27:31 +02:00
Julien Duponchelle
54b6efce59 Fix Save preference is broken
Fix #1421
2016-08-18 17:19:04 +02:00
Julien Duponchelle
1f77a825b3 Fix The wizard for Docker allow to select local on mac and windows
Fix #1406
2016-08-18 16:03:55 +02:00
Julien Duponchelle
283d787c8d Handle error when node can't be create
Fix #1398
2016-08-18 14:49:33 +02:00
Julien Duponchelle
47be4d39c2 Properly handle the error project name duplicate
Fix #1379
2016-08-18 14:28:28 +02:00
Julien Duponchelle
8a4fab9528 Merge branch 'master' into 2.0 2016-08-18 14:18:59 +02:00
Julien Duponchelle
3b88c72778 Cleanup the resources.qrc of references to non existing files 2016-08-18 11:25:55 +02:00
Julien Duponchelle
0e4a5da71a Merge branch 'master' into 2.0 2016-08-18 10:41:35 +02:00
Julien Duponchelle
8492a31dd7 Send label settings to the controller 2016-08-17 17:13:35 +02:00
Julien Duponchelle
bb91402d2c Remove dirty code for sync label position between nodes server is
smarter
2016-08-17 16:12:40 +02:00
Julien Duponchelle
35d8b4f848 Fix you can't quit GNS3 when controller is down 2016-08-17 12:01:21 +02:00
Julien Duponchelle
073dc1afe7 We no longer need to compute node size on client
https://github.com/GNS3/gns3-server/issues/620
2016-08-17 12:00:18 +02:00
Julien Duponchelle
b61e6dabd4 Fix tab display when you open new project 2016-08-16 19:18:56 +02:00
Julien Duponchelle
316fa688c3 Fix to project lost when you open the new blank project dialog 2016-08-16 19:16:36 +02:00
Julien Duponchelle
0724387257 Merge node create and update answer parsing to avoid mistake
This fix the bug where the when project is auto start the node
status was still red.
2016-08-16 15:51:23 +02:00
Julien Duponchelle
8e87c8cdbe Allow to configure auto start/auto close/auto open 2016-08-16 14:07:47 +02:00
Julien Duponchelle
9553d3ba25 Allow to delete a project from the project list
Fix #1419
2016-08-16 13:33:06 +02:00
Julien Duponchelle
4bccd6de25 Add in the project menu the list of recent opened projects
Fix #1410
2016-08-16 11:50:23 +02:00
Julien Duponchelle
dd5bafca0a Add an edit project dialog
For the moment only edit name is supported and sync between
the GUI but the architecure is here.

Ref #1410
2016-08-16 11:12:38 +02:00
Julien Duponchelle
6d3e28226a In telnet console command line replace %c by connection string
Ref #1403
2016-08-15 15:48:48 +02:00
Julien Duponchelle
0fc040773a Fix path for recently opened projects 2016-08-15 15:35:36 +02:00
Julien Duponchelle
1f2294f9bf Correctly support closing project when two client are connected
Fix https://github.com/GNS3/gns3-server/issues/602
2016-08-15 13:38:39 +02:00
Julien Duponchelle
bed3f1d8fa Delete ProjectManager and merge it in Topology
Ref #1326
2016-08-15 12:59:41 +02:00
grossmj
32f3137f4d Setup wizard to configure GNS3 VM or Local server. 2016-08-11 16:00:08 -06:00
grossmj
df10bca2c0 Fixes local server and uBridge paths loading. 2016-08-10 16:19:53 -06:00
grossmj
f56e7e8dd8 Remove setTabBarAutoHide property in project dialog. 2016-08-04 10:43:00 -06:00
Julien Duponchelle
7732f2a27e Merge branch 'master' into 2.0 2016-07-28 15:13:35 +02:00
Julien Duponchelle
c2a597ffcf Repare edit configuration files 2016-07-28 12:06:47 +02:00
Julien Duponchelle
fc4850afab Fix placeholder symbol 2016-07-28 12:03:08 +02:00
Julien Duponchelle
6c31de36ac Import export 2016-07-27 21:45:13 +02:00
Julien Duponchelle
4082aa8d77 Import config 2016-07-27 21:35:05 +02:00
Julien Duponchelle
71a835ff5f Export configuration 2016-07-27 21:08:22 +02:00
Julien Duponchelle
b156df6fc2 Allow to edit a configuration file from the GUI
Fix #1407
2016-07-27 18:49:49 +02:00
Julien Duponchelle
d077621ee9 Fix a crash when pos is None in ImageItem 2016-07-27 09:38:06 +02:00
Julien Duponchelle
3624502a23 restore snapshots in the GUI 2016-07-26 19:38:29 +02:00
Julien Duponchelle
c9f12fece7 PyQt 5.7 2016-07-26 18:27:36 +02:00
Julien Duponchelle
0f43fd4560 Place holder for loading symbols for nodes
Fix #1377
2016-07-25 19:46:47 +02:00
Julien Duponchelle
1c85f980d3 Fix Transport selection via DSN is deprecated. You should explicitly
pass the transport class to Client()

Fix #1341
2016-07-25 19:33:08 +02:00
Julien Duponchelle
49428d3ead Hide open project field when importing a project in new project dialog 2016-07-25 19:11:17 +02:00
Julien Duponchelle
2f48752ff2 Show a dialog when importing a project 2016-07-25 18:58:55 +02:00
Julien Duponchelle
48ac89abc9 Replace save as by a export / import
Fix #995
2016-07-25 16:17:01 +02:00
Julien Duponchelle
bd8bad5e4c Make sure the README is saved before closing the file editor dialog 2016-07-22 18:16:46 +02:00
Julien Duponchelle
13c189fb00 Merge branch 'master' into 2.0 2016-07-22 18:10:16 +02:00
Julien Duponchelle
61fb8246f0 Display error during the import to the user 2016-07-22 18:00:23 +02:00
Julien Duponchelle
b09249b384 Support import of .gns3project 2016-07-21 16:21:38 +02:00
Julien Duponchelle
7d6b98766c Repare edit readme
Fix #1401
2016-07-20 21:51:34 +02:00
Julien Duponchelle
96bcf55942 Display export errors 2016-07-20 17:16:59 +02:00
Julien Duponchelle
7bb6078b13 Tmp fix for reloading Vbox topologies
Ref #1400
2016-07-20 16:35:48 +02:00
Julien Duponchelle
c8d6a4640a Fix a crash when reloading some topologies 2016-07-20 16:27:38 +02:00
Julien Duponchelle
27be2b7a1d Fix a crash in VirtualBox wizard 2016-07-20 16:21:28 +02:00
Julien Duponchelle
b05d682aa3 Export project call the controller 2016-07-20 14:15:39 +02:00
Julien Duponchelle
636b26b0e8 Fix for the GNS3VM wizard 2016-07-20 14:00:38 +02:00
Julien Duponchelle
ae2a111536 Fix In the remote server list in preferences do not display the VM server
Fix #1396
2016-07-20 12:19:04 +02:00
Julien Duponchelle
33796a8bd3 Replace project id in open console
Fix #1395
2016-07-20 12:06:59 +02:00
Julien Duponchelle
695e5d3daa Merge the multiple telnet console open code 2016-07-20 12:00:27 +02:00
Julien Duponchelle
9d805d5d42 Avoid a race condition at label update 2016-07-20 11:59:15 +02:00
Julien Duponchelle
ec3fd63138 Fix drawing area not reset when cancel new project
Fix #1394
2016-07-20 11:25:45 +02:00
Julien Duponchelle
8e5e2d4a0c Fix selection of GNS3 VM in the wizard 2016-07-20 11:22:16 +02:00
Julien Duponchelle
42c54ef02f Fix enable the list of GNS3 VM
Fix #1391
2016-07-19 13:19:21 +02:00
Julien Duponchelle
227cbfc79a Do not add the VM to the list of remotes servers 2016-07-18 18:59:37 +02:00
Julien Duponchelle
0fd5a1a91d Fix crash when you have no routers nodes 2016-07-18 18:37:03 +02:00
Julien Duponchelle
4406c940b5 Merge branch 'master' into 2.0 2016-07-13 17:40:23 +02:00
Julien Duponchelle
f50f7153dc Fix crash at startup 2016-07-13 15:52:22 +02:00
Julien Duponchelle
a994f65d79 Handle error when cloud can't be created 2016-07-12 18:24:24 +02:00
grossmj
840e4aec54 Load/save GNS3 VM settings on controller side. 2016-07-11 21:43:01 -06:00
grossmj
74b660af61 Basic GNS3 VM configuration using the setup wizard. 2016-07-11 17:45:23 -06:00
grossmj
cc0c56087a Warn users they must start a node to console to it. Fixes #1314. 2016-07-11 10:08:45 -06:00
grossmj
990e6c0eed Allow customizable VPCS templates. Fixes #1306. 2016-07-10 20:33:19 -06:00
grossmj
3bc6cd8b4d Allow ports in cloud templates. Fixes #867. 2016-07-10 14:55:27 -06:00
grossmj
515119e1fa Fixes some imports 2016-07-09 19:02:23 -06:00
Julien Duponchelle
570303273c Fix color losts in drawing items 2016-07-09 11:48:07 +02:00
Julien Duponchelle
1739cc58d4 Support alpha channel for drawings 2016-07-09 11:28:30 +02:00
Julien Duponchelle
1e3883674e Fix crash when you cancel the creation of a new project 2016-07-08 11:26:11 +02:00
Julien Duponchelle
89fbc537bf Fix At node creation information about the node label formatting is not send
Fix #1366
2016-07-07 10:05:48 +02:00
Julien Duponchelle
d396cb911a Fix some issue recently introduce in HTTPClient 2016-07-06 15:43:03 +02:00
Julien Duponchelle
9064487a3e Remove constant that doesn't seem to have an impact
Fix #1345
2016-07-06 15:31:22 +02:00
Julien Duponchelle
8b03f32f95 Fix Open from a project from command line doesn't work bug
Fix #1346
2016-07-06 15:23:56 +02:00
Julien Duponchelle
3295cc514e Merge branch 'master' into 2.0 2016-07-06 14:40:09 +02:00
Julien Duponchelle
565c71cb80 Fix UnboundLocalError: local variable 'callback' referenced before assignment
Fix #1364
2016-07-06 10:35:04 +02:00
Julien Duponchelle
30bd710650 Merge branch 'master' into 2.0 2016-07-06 10:33:53 +02:00
Julien Duponchelle
7f58837111 Fix double call to /version at startup
It's fixe a deeper issue where the http_client open two connection
at the same time

Fix #1363
2016-07-05 18:31:03 +02:00
Julien Duponchelle
53f609c4d7 Make logs less noisy 2016-07-05 18:13:42 +02:00
Julien Duponchelle
26790fd80d Fix When reopen a project the node label could lost his style
Fix #1362
2016-07-05 18:02:47 +02:00
Julien Duponchelle
cfcb24a732 Now the controller take care of deleting links when you delete a node
Fix https://github.com/GNS3/gns3-server/issues/608
2016-07-05 16:29:32 +02:00
Julien Duponchelle
e31746b676 Rebuild server page after merge 2016-07-05 16:29:20 +02:00
Julien Duponchelle
154435d5a5 Merge branch 'master' into 2.0 2016-07-05 15:36:15 +02:00
Julien Duponchelle
fa8c135b22 Sync the link between gui 2016-07-01 21:50:05 +02:00
Julien Duponchelle
9d53d806fd Correct center the node label the first time and beginning of interface
label support
2016-07-01 20:14:08 +02:00
Julien Duponchelle
6f499e6c56 Support rotation for labels 2016-07-01 16:54:41 +02:00
Julien Duponchelle
e66bdc936a Use SVG style for labels 2016-07-01 15:26:53 +02:00
Julien Duponchelle
be34e062e7 Bold & italic support in text item 2016-07-01 15:10:09 +02:00
Julien Duponchelle
5664b32cc5 Start to use a svg style property for label and send node witdh and
height
2016-07-01 14:39:51 +02:00
Julien Duponchelle
6bf0ea63d4 Restore label position
Fix #1344
2016-07-01 12:15:23 +02:00
Julien Duponchelle
628970e588 Sync link creation & removal 2016-07-01 11:37:43 +02:00
Julien Duponchelle
fb68ccad15 Fix crash when using two gui 2016-06-30 18:52:19 +02:00
Julien Duponchelle
d6b394500f When updating a node on a GUI change is visible on other GUI 2016-06-30 18:44:57 +02:00
Julien Duponchelle
32508d60b1 Sync node creation and delete
Ref #1250
2016-06-30 16:14:17 +02:00
Julien Duponchelle
0ae23c30c4 Fix Symbol is lost when reloading project
Fix #1342
2016-06-30 15:08:09 +02:00
Julien Duponchelle
e5f18c5e22 Bring back compute node usage
Fix #1261
2016-06-30 10:12:26 +02:00
Julien Duponchelle
df0f25b234 If project creation fail reset the project in the GUI 2016-06-29 17:49:37 +02:00
Julien Duponchelle
dc1d9e59b0 Merge branch '1.5' into 2.0 2016-06-29 17:22:57 +02:00
Julien Duponchelle
037e531b22 Send initial config to the controller 2016-06-29 15:17:44 +02:00
Julien Duponchelle
54713b5d68 Support storing list of devices on the remote server
Fix #589
2016-06-29 15:14:18 +02:00
Julien Duponchelle
d94f9a91db Fix a crash in preferences
Fix #1337
2016-06-29 14:41:32 +02:00
Julien Duponchelle
516b8e848f Full symbol on controller support 2016-06-28 22:20:50 +02:00
Julien Duponchelle
6d1d1705b2 Support PNG for symbols 2016-06-28 21:51:54 +02:00
Julien Duponchelle
666a527aa3 Remove symbols not use in interface.
They are available in the controller when you use it
for nodes.
2016-06-28 17:03:35 +02:00
Julien Duponchelle
f296c7fdad Drop ressources not used in the interface 2016-06-28 17:00:48 +02:00
Julien Duponchelle
58e62da913 Script for detecting unused ressources 2016-06-28 16:56:15 +02:00
Julien Duponchelle
ca364d4d56 All place where we display a symbol we use the version from controller 2016-06-28 16:54:32 +02:00
Julien Duponchelle
2232680ded New crash report key 2016-06-27 20:41:07 +02:00
Julien Duponchelle
b662c54a07 Wip symbol from remote 2016-06-27 20:40:34 +02:00
grossmj
780ab5b14f Docker links are now hot pluggable. 2016-06-24 17:27:35 -06:00
grossmj
7371aebb76 Remove more GNS3VM code from the GUI. Ref #1254. 2016-06-24 15:18:10 -06:00
grossmj
c227f39a03 Allow packet capture on VPCS and Qemu nodes. 2016-06-23 16:52:41 -06:00
Julien Duponchelle
ff794f1578 Use SVG properties instead of style to store info 2016-06-23 18:51:59 +02:00
Julien Duponchelle
f26c342e82 Handle text items in the drawing api 2016-06-23 18:21:20 +02:00
Julien Duponchelle
edacb88ff5 Cleanup topologies test, removed unecessary elments 2016-06-23 13:32:13 +02:00
Julien Duponchelle
4854eac2da Show layer for all drawing items 2016-06-23 13:29:30 +02:00
Julien Duponchelle
60cd105b82 Snap to grid for all drawing items 2016-06-23 13:16:06 +02:00
Julien Duponchelle
184db222c5 Merge branch '1.5' into 2.0 2016-06-23 13:11:07 +02:00
Julien Duponchelle
abcfb9ee12 Handle duplicate of all drawing items 2016-06-23 13:07:45 +02:00
Julien Duponchelle
5f3ba669eb Repare display of symbols 2016-06-23 12:47:29 +02:00
Julien Duponchelle
942d4756c7 Shape => Drawing 2016-06-23 12:26:54 +02:00
Julien Duponchelle
d9b6dfd8d0 Fix test suite 2016-06-23 12:12:11 +02:00
Julien Duponchelle
fb5c4df4db Support sync create of shapes 2016-06-22 18:09:57 +02:00
Julien Duponchelle
9cb4eb775b Send image to the API as SVG element 2016-06-22 17:47:17 +02:00
Julien Duponchelle
8c349e4669 Merge branch '1.5' into 2.0 2016-06-22 14:12:38 +02:00
Julien Duponchelle
336f8d525b Dead code removal 2016-06-21 19:41:26 +02:00
Julien Duponchelle
f76b6afe6a Rotation support for the shapes 2016-06-21 19:39:28 +02:00
Julien Duponchelle
602b58d1df Support z value for shape 2016-06-21 19:23:31 +02:00
Julien Duponchelle
030edccc90 Sync shape delete 2016-06-21 19:16:51 +02:00
Julien Duponchelle
dd3317f4f6 Sync update shape 2016-06-21 19:13:08 +02:00
Julien Duponchelle
f29d0e45b7 Get shapes from the server 2016-06-21 19:03:58 +02:00
Julien Duponchelle
c038ed3db4 Delete shape on remote server 2016-06-21 16:19:00 +02:00
Julien Duponchelle
6bcc4c86e6 Send rectangle and ellipse to the servers 2016-06-21 15:19:29 +02:00
Julien Duponchelle
ded32730bf Fix starting test suites 2016-06-21 09:22:41 +02:00
Julien Duponchelle
2e1b6aef9f Fix a crash 2016-06-20 19:15:25 +02:00
Julien Duponchelle
7c37284901 Merge branch '1.5' into 2.0 2016-06-20 12:45:44 +02:00
Julien Duponchelle
222ea18bcd Merge branch 'open_project' into 2.0 2016-06-17 15:58:10 +02:00
Julien Duponchelle
df7c91f17f Save as you go
Fix #1243
2016-06-17 15:55:23 +02:00
grossmj
99331fcc54 Try to repair the GNS3 VM support. Ref #1254. 2016-06-16 18:06:51 -06:00
Julien Duponchelle
379b7a56ef Fix tests and a Virtualbox crash 2016-06-15 13:47:59 +02:00
Julien Duponchelle
8e9062c812 Merge branch '1.5' into 2.0 2016-06-15 13:38:31 +02:00
Julien Duponchelle
ec68deb7e4 Drop the commit system
Ref #1243
2016-06-14 10:18:48 +02:00
Julien Duponchelle
1d51f3eed5 Merge branch '1.5' into 2.0 2016-06-14 10:17:42 +02:00
Julien Duponchelle
e8e189d5f3 Drop unused servers 2016-06-13 19:11:00 +02:00
grossmj
b106be2ed5 Removes client side unique node name allocation system. 2016-06-11 16:55:23 -06:00
grossmj
842519d7d0 Merge branch '1.5' into 2.0
Conflicts:
	gns3/main_window.py
	gns3/modules/docker/pages/docker_vm_preferences_page.py
	gns3/modules/dynamips/dialogs/ios_router_wizard.py
	gns3/modules/virtualbox/dialogs/virtualbox_vm_wizard.py
2016-06-10 21:48:57 -06:00
grossmj
b5a04bfe63 Implements customizable Ethernet hub & switches templates. 2016-06-10 21:02:05 -06:00
grossmj
077e6a110e Implements customizable cloud node templates. Ref #867. 2016-06-10 16:44:53 -06:00
Julien Duponchelle
66d87e8b12 Minor fixes related to IOU
* Fix crash when starting an individual node
* Fix crash when getting project path of the node
2016-06-10 15:34:26 +02:00
Julien Duponchelle
6bf5e7abcc Display HTTP error when no callback is register 2016-06-10 14:47:45 +02:00
Julien Duponchelle
39979a411d Avoid an error when we try to update a node before getting the node id 2016-06-10 14:35:49 +02:00
Julien Duponchelle
fdd5c71711 Fix a crash in IOU wizard 2016-06-10 14:34:26 +02:00
Julien Duponchelle
d6e20fe166 Send changes of label positions to the controller
Fix #1297
2016-06-10 11:24:05 +02:00
Julien Duponchelle
7988b13281 Do not destroy the NodeItem when changing symbol
This allow us to just replace the symbol in place and
notify the server.
2016-06-10 10:57:18 +02:00
Julien Duponchelle
8395865b75 Send X,Y,Z,Label and Symbol to server
We have an issue for the moment to detect change of symbol and
in label
2016-06-10 10:47:55 +02:00
Julien Duponchelle
6d9167c30f Removed the SvgNodeItem because it's was the only sub class of NodeItem 2016-06-09 18:08:22 +02:00
Julien Duponchelle
f8d698aea9 Fix docker VM wizard
Fix #1296
2016-06-08 16:00:36 +02:00
Julien Duponchelle
0cbde5046e Fix GNS3A support
Fix #1274
2016-06-08 15:10:32 +02:00
Julien Duponchelle
7a137a68ae Display human name of compute node in preferences
Fix #1291
2016-06-08 14:33:32 +02:00
Julien Duponchelle
09f7e6ce99 Avoid test side effect on user configuration files 2016-06-08 11:27:14 +02:00
Julien Duponchelle
305cc72485 Fix crash when browsing for local images directory 2016-06-08 11:18:57 +02:00
Julien Duponchelle
9b04901754 Upload missing images is now handled by the controller 2016-06-08 10:13:12 +02:00
Julien Duponchelle
d262f429c4 Fix text when stopping local server 2016-06-08 09:55:13 +02:00
Julien Duponchelle
6bb1223614 Repare IDLE PC in the IOS wizard 2016-06-07 12:04:28 +02:00
Julien Duponchelle
1d97b217cd Repare image upload 2016-06-07 11:39:15 +02:00
Julien Duponchelle
dfe48466e0 Fix list IOS images in the wizard 2016-06-06 16:09:16 +02:00
Julien Duponchelle
6fed45e7a8 Repare local copy for images 2016-06-06 15:56:01 +02:00
Julien Duponchelle
d97b75a3e1 Repare qemu-img create 2016-06-06 15:48:41 +02:00
Julien Duponchelle
87f2e08b3a Merge branch '1.5' into 2.0 2016-06-06 14:30:20 +02:00
Julien Duponchelle
f2c517a4a4 Start fixing qemu wizard 2016-06-02 20:11:36 +02:00
Julien Duponchelle
b8b810cdb1 Repare VMware and VirtualBox support
Fix https://github.com/GNS3/gns3-server/issues/537
2016-06-02 18:43:11 +02:00
Julien Duponchelle
c21900100e Repare Dynamips and IOU preference dialog (without image change) 2016-06-02 17:44:40 +02:00
Julien Duponchelle
50222f5083 Fix save of images search directories
The config file is not a JSON but a INI so we need to export
string.
2016-06-02 14:10:01 +02:00
grossmj
594b596cf9 Revert move "/version" endpoint to "/server/version".
Move "/server/shutdown" endpoint to "/shutdown".
2016-06-01 18:21:07 -06:00
grossmj
3494a4875c Some cleaning + move "/version" endpoint to "/server/version". 2016-06-01 17:50:31 -06:00
Jeremy Grossmann
a5d880e411 Merge pull request #1281 from GNS3/shutdown_server
Shutdown local server via controller
2016-06-01 15:37:52 -06:00
Jeremy Grossmann
e9f445380b Merge pull request #1275 from GNS3/images_directory
UI for settings image directories
2016-06-01 15:32:05 -06:00
grossmj
ea51f15253 Fixes graphical bug when remove interface from the cloud. 2016-06-01 12:48:02 -06:00
grossmj
35ff7fd83e Filter special interfaces in cloud. Fixes #1279. 2016-06-01 11:57:45 -06:00
Julien Duponchelle
1375d7922c Shutdown local server via controller
Fix #1191
2016-06-01 17:22:53 +02:00
Julien Duponchelle
eecf1f4a54 Avoid a segfault when exiting with debug enabled 2016-06-01 16:17:53 +02:00
grossmj
22fcb14f9a Remove manual console port choice. Fixes #1167. 2016-05-31 21:53:51 -06:00
grossmj
6f2294f9b9 Automatically add ports for the cloud object when creating a new one. 2016-05-31 18:53:38 -06:00
Julien Duponchelle
3889c8c1fa Fix a crash 2016-05-31 08:34:55 +02:00
grossmj
d122e10703 Small improvements for the packet capture dialog. 2016-05-30 21:51:58 -06:00
grossmj
05f1fa0ecb Drop the host node. 2016-05-30 21:02:03 -06:00
grossmj
3974629e34 Improvements on the cloud interface. Ref #1116.
- Delete multiple interfaces or tunnels at the same time.
- Possibility to filter virtualization interfaces via contextual menu.
2016-05-30 18:56:06 -06:00
grossmj
dfd8147873 Simplify how to count for number of files in a path. 2016-05-30 15:52:26 -06:00
grossmj
bb503d9cc7 Additional checks when adding a binary image path. 2016-05-30 15:41:47 -06:00
Julien Duponchelle
913cb1a3cd UI for settings image directories
Ref https://github.com/GNS3/gns3-server/issues/546
2016-05-30 21:46:41 +02:00
Julien Duponchelle
8523e3d1a4 Ensure project is not closed twice 2016-05-30 16:06:54 +02:00
Julien Duponchelle
e2415b68d3 Fix a crash when updating local server settings 2016-05-30 14:35:45 +02:00
Julien Duponchelle
0850a3428e Fix Topology Summary view missing link
Fix #1273
2016-05-30 14:33:37 +02:00
Julien Duponchelle
6f44a8b6ee Cleanup dead code around packet capture 2016-05-30 12:33:58 +02:00
Julien Duponchelle
0cc16c232b Fix crash when local server port is already used 2016-05-30 12:06:34 +02:00
Julien Duponchelle
57bd21d346 Merge branch '1.5' into 2.0 2016-05-30 11:41:32 +02:00
grossmj
7397d2da50 Merge branch '1.5' into 2.0
Conflicts:
	gns3/main_window.py
	gns3/modules/builtin/ui/cloud_configuration_page_ui.py
	gns3/modules/vmware/ui/vmware_preferences_page_ui.py
	gns3/modules/vmware/ui/vmware_vm_configuration_page_ui.py
	gns3/node.py
	gns3/version.py
2016-05-29 20:14:08 -06:00
grossmj
f70c457e88 Started to streamline the could. Ref #1116. 2016-05-28 21:49:18 -06:00
Julien Duponchelle
1375578b52 Support delete a project from GUI
Fix #822
2016-05-26 11:21:43 +02:00
Julien Duponchelle
1e2326913b Support add a remote compute server 2016-05-26 10:28:17 +02:00
Julien Duponchelle
82c41e09b5 Avoid a crash because VM is not supported in preferences 2016-05-26 10:14:40 +02:00
Julien Duponchelle
16f3b71af4 Support update of computes
Fix #795
2016-05-26 10:09:52 +02:00
grossmj
03373f3cda Add message on GraphicsScene when a project must be created. 2016-05-25 10:04:32 -06:00
Julien Duponchelle
2c9c01b991 Select the correct tab in general preferences 2016-05-25 14:25:33 +02:00
Julien Duponchelle
8a44b6fdb7 Support delete a remote server 2016-05-25 13:40:58 +02:00
Julien Duponchelle
3ceb886ca9 Support all computes informations 2016-05-25 10:34:42 +02:00
Julien Duponchelle
fb3df39263 Remove the auto launch project dialog settings because it's now
always open
2016-05-24 20:04:23 +02:00
Julien Duponchelle
829e8ed745 Drop support for temporary projects
Fix #982
2016-05-24 18:56:56 +02:00
Julien Duponchelle
ed5c52a807 Drop legacy call to /compute/version 2016-05-24 16:32:40 +02:00
Julien Duponchelle
8afc5afadf Less usages of the Servers 2016-05-24 16:31:07 +02:00
Julien Duponchelle
b26401203f VPCS now allow to select the server where the node will run 2016-05-24 15:48:24 +02:00
Julien Duponchelle
c127548dd1 one less Servers references 2016-05-24 15:27:33 +02:00
Julien Duponchelle
f8c1a48350 Tests are green again 2016-05-24 15:23:14 +02:00
Julien Duponchelle
55f634bec3 Remove debug 2016-05-24 11:20:40 +02:00
Julien Duponchelle
8c14e42a09 Merge branch '1.5' into 2.0 2016-05-24 10:16:37 +02:00
Julien Duponchelle
3fc4898904 First step of refactoring Servers 2016-05-23 19:47:52 +02:00
grossmj
6a402fe544 First step towards the cloud node re-factoring. 2016-05-22 19:24:14 -06:00
grossmj
310ae5905f More node re-factoring (setup -> create etc.) 2016-05-22 18:23:39 -06:00
grossmj
23aa820cdf Try to reduce node boilerplate code. 2016-05-22 15:57:26 -06:00
grossmj
74f702cea6 Layout change for the capture dialog. 2016-05-22 10:22:43 -06:00
grossmj
7cb6af85a8 Drop VPCS multi host support. Fixes #1252. 2016-05-20 18:18:26 -06:00
grossmj
358ef34918 Fixes base node. 2016-05-20 18:00:46 -06:00
grossmj
a66d194e12 Frame Relay and ATM switches migrated to the new API. 2016-05-20 17:59:59 -06:00
Julien Duponchelle
57b3ce4666 Start to drop Servers class 2016-05-20 13:17:32 +02:00
grossmj
aaa2b6f817 Ethernet hub and Ethernet switch almost fully migrated to the new API. 2016-05-19 22:45:04 -06:00
Julien Duponchelle
97b56e5620 Drop duplicate code already manage by the class VMWizard 2016-05-19 17:47:36 +02:00
Julien Duponchelle
79850176c3 Compute IDLE PC on controller 2016-05-19 16:22:29 +02:00
Julien Duponchelle
8e1896ef5b Support console_host 2016-05-19 14:05:06 +02:00
Julien Duponchelle
8cf911bb15 Merge branch '1.5' into 2.0 2016-05-19 13:34:21 +02:00
Julien Duponchelle
2aada61af3 Support update node from another GUI 2016-05-18 20:59:07 +02:00
Julien Duponchelle
66b9b4c68c Fix node not deleted on controller
Fix #1249
2016-05-18 20:45:14 +02:00
Julien Duponchelle
60d6151ce9 Drop unusued code from old cloud servers support 2016-05-18 20:34:24 +02:00
Julien Duponchelle
bfb4b0b9da Merge branch '1.5' into 2.0 2016-05-18 11:38:11 +02:00
grossmj
71536ef9d3 Functional Ethernet hub with new API. 2016-05-17 21:32:57 -06:00
grossmj
354e73b4e7 Fixes project save/load. 2016-05-17 21:32:05 -06:00
Julien Duponchelle
30121e3617 aiohttp_cors 2016-05-17 18:06:19 +02:00
Julien Duponchelle
4d422e716b Support node.updated 2016-05-16 21:50:54 +02:00
Julien Duponchelle
8aa0f8d070 Fix error message for packet capture 2016-05-16 21:50:54 +02:00
grossmj
7c03c0cbcf Fixes #1246. 2016-05-16 10:56:35 -06:00
Julien Duponchelle
c13a4835b2 Test green :) 2016-05-16 17:27:47 +02:00
Julien Duponchelle
412d9b7645 Merge branch '1.5' into 2.0 2016-05-16 16:49:39 +02:00
Julien Duponchelle
6449973ddc Fix delete node 2016-05-16 14:32:02 +02:00
grossmj
bd9a168667 Base for generic switch nodes. Started to move the Ethernet hub. 2016-05-15 21:02:50 -06:00
grossmj
5c0b03f133 Fixes suspended notification. 2016-05-14 12:24:55 -06:00
grossmj
5708f039c0 Fixes tests. 2016-05-14 12:18:16 -06:00
grossmj
ba0809159c Move most of the project management code its own class. Ref #1224. 2016-05-14 12:15:20 -06:00
grossmj
4aeb4238b2 Move style code to its own class. Ref #1224. 2016-05-14 10:36:30 -06:00
grossmj
119bc8207f Use start/stop/suspend/reload all endpoints. 2016-05-13 20:44:41 -06:00
grossmj
5e7dc27e1f Start, stop, suspend and reload endpoints for all nodes belonging to a project. Fixes #1212. 2016-05-13 19:26:51 -06:00
grossmj
ccba9aa4d5 Update upload endpoints to match with the server. 2016-05-13 18:02:26 -06:00
Julien Duponchelle
47cbc91b02 Fix test 2016-05-13 15:34:06 +02:00
Julien Duponchelle
7a10fa157d Fix show in file manager
Fix #1238
2016-05-12 10:47:58 +02:00
Julien Duponchelle
a27ed4051c Test OK 2016-05-12 09:22:47 +02:00
Julien Duponchelle
dbbde4b098 Rollback some test schema to a previous version of GNS3 2016-05-12 09:00:25 +02:00
grossmj
7e2284e094 Refactoring to use a common node class for all VMs and other (future) objects. 2016-05-11 16:54:55 -06:00
Julien Duponchelle
0ce5c198aa Merge branch '1.5' into 2.0 2016-05-11 10:34:32 +02:00
grossmj
72f580efb8 Fixes incomplete merge 2016-05-10 13:31:28 -06:00
Julien Duponchelle
b0eb0d74fb Merge branch '1.5' into 2.0 2016-05-10 10:51:27 +02:00
Julien Duponchelle
1e3d216961 Capture packet on link instead of port 2016-04-27 11:05:11 +02:00
Julien Duponchelle
1ae7be4f6a Remove noisy logs 2016-04-21 14:41:41 +02:00
Julien Duponchelle
50a92e9ea0 Merge branch '1.5' into 2.0 2016-04-20 18:02:07 +02:00
Julien Duponchelle
5c29d42d8c Update server summary with server status on controller
Fix #1186
2016-04-20 16:46:18 +02:00
Julien Duponchelle
0825ae8cb5 Remove .NET from project dialog 2016-04-19 08:50:14 +02:00
Julien Duponchelle
c032c9f458 Update and delete VPCS via controller 2016-04-18 18:56:38 +02:00
Julien Duponchelle
fa5a9621e0 Reload VM on controller 2016-04-18 17:00:53 +02:00
Julien Duponchelle
b2db2cc719 Drop GNS3 converter
Fix #1147
2016-04-18 16:45:05 +02:00
Julien Duponchelle
0f76819936 Drop IOUVM converter
Ref #1147
2016-04-18 16:42:44 +02:00
Julien Duponchelle
66d1597312 Merge branch '1.5' into 2.0 2016-04-18 16:00:28 +02:00
Julien Duponchelle
d217d9a291 Rename hypervisor to compute 2016-04-15 17:56:27 +02:00
Julien Duponchelle
79c64f0e38 Start / Stop / Suspend on the controller 2016-04-14 12:23:02 +02:00
Julien Duponchelle
b8a3deeb02 Now each log message with level Error or Warning are display on the
console without the need of using print
2016-04-12 18:54:34 +02:00
Julien Duponchelle
108c774c0f Merge branch '1.5' into 2.0 2016-04-12 17:09:26 +02:00
Julien Duponchelle
5470add29a Merge branch '1.5' into 2.0 2016-04-12 17:01:05 +02:00
Julien Duponchelle
1b873acd72 2.0.0dev1 2016-03-24 10:40:07 +01:00
Julien Duponchelle
a76ac9b5e3 Merge branch '1.5' into 2.0 2016-03-24 10:39:48 +01:00
Julien Duponchelle
4f575fda73 Cleanup 2016-03-18 17:04:48 +01:00
Julien Duponchelle
14b6c70f47 Consume the global notification stream 2016-03-18 17:03:41 +01:00
Julien Duponchelle
8f4e9ac48f Fix duplicate initial VM with network V2 2016-03-15 15:31:39 +01:00
Julien Duponchelle
30069e719b Success to add a dynamips with api V2 2016-03-15 10:45:32 +01:00
Julien Duponchelle
40f3a78795 Support delete link 2016-03-14 20:52:05 +01:00
Julien Duponchelle
0d11c71bb7 Create a link between two VPCS work 2016-03-14 17:05:16 +01:00
Julien Duponchelle
758480dd5f Remove /controller from the V2 call 2016-03-11 17:52:06 +01:00
Julien Duponchelle
2ca84501ba Create VPCS VM on controller 2016-03-11 15:06:39 +01:00
Julien Duponchelle
f9756e0977 Close project via controller 2016-03-10 10:57:34 +01:00
Julien Duponchelle
31ba460553 Create project via controller 2016-03-10 09:07:30 +01:00
Julien Duponchelle
57f519db65 API V2 support 2016-03-08 12:07:11 +01:00
Julien Duponchelle
edf6c65e38 Register server on the controller when added 2016-03-04 08:32:54 +01:00
Julien Duponchelle
349cf1981a Split HTTPClient in two parts
The HTTP client class is responsible to make HTTP calls to
gns3 API server (controller or standard server).

Server is a GNS3 Server. In the transition period the server
will call the HttpClient of the GNS3 controller of the HttpClient
of the server depending of the endpoint.
2016-03-03 15:54:55 +01:00
428 changed files with 113537 additions and 137131 deletions

2
.pyup.yml Normal file
View File

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

View File

@@ -1,19 +1,16 @@
sudo: required
services:
- docker
- docker
notifications:
email: false
#email:
# - julien@gns3.net
#irc:
# channels:
# - "chat.freenode.net#gns3"
# on_success: change
# on_failure: always
script:
- docker build -t gns3-gui-test .
- docker run gns3-gui-test
- docker run gns3-gui-test
deploy:
provider: pypi
user: noplay
password:
secure: FofcqlJjgqf2jaDaXpLHeigVoexbrOz3WwnDuiJpwJxeFUlPY8s2cQs/Bm+dzxzZaOaGiVE0A83v/Xa10yD5tflThHt4sqYJK3iQCinA7wgeAlDimB4xrWUNplfNJZ/Eod5Ssa++E02W+3i29PxpXY//mjCY7qDxaoxul1gnFJY=
on:
tags: true
repo: GNS3/gns3-gui

440
CHANGELOG
View File

@@ -1,5 +1,445 @@
# Change Log
## 2.0.2 30/05/2017
* Show a default symbol in case of corrupted file
* When another gui is already running exit instead of proper close to avoid any issue
* Fix duplicate on remote server use wrong location
* Display the location of settings when we disallow opening due to old release
* Improve search for dynamips in development on OSX
* Fix error display when loading a .png custom symbol
* Fix a crash in the progress dialog
* Fix a race condition when exporting a closed project
* Fix RuntimeError: wrapped C/C++ object of type NodeItem has been deleted
## 2.0.1 16/05/2017
* Improve inline help. Fixes #1999. Add a warning about wifi interfaces in the cloud. Fixes #1902.
* Copy remote directory path into clipboard in "Show in FileManager". Fixes #1966.
* Fix display of error in progress dialog when we don't have thread
* Fix lost slot and port in dynamips settings
* Do not run import / export of project in seperate thread
* Assert when running an HTTP query outside the main thread
* Proper error when you try to load the pid file as config file
* Log malformed svg text item
* Fix a race condition when right click and delete a node at the same time
* Fix a race condition when snapshoting a closed project
* Update doctor_dialog.py
* Catch remaining missing function listxattr on some Linux host.
* Fix a race condition when creating node and closing project
* Fix error if you put a path in a .gns3a file for qemu
* Fix AttributeError: 'NoneType' object has no attribute '_refreshVisibleWidgets'
* Do not crash if the logging code raise an exception
* Fix some crash in dynamips device preference page
* Fix warning when loading IOU images on Windows
* Do not crash if you don't have configure a packet capture program on Windows
* Ignore error when we can't kill the packet capture
* Fix AttributeError: 'NoneType' object has no attribute 'wasCanceled'
* Fix RuntimeError: wrapped C/C++ object of type QComboBox has been deleted
* Fix RuntimeError: wrapped C/C++ object of type QTreeWidgetItem has been deleted
* Fix detection of https when use for the local server
* Silent the _COMPIZ_TOOLKIT_ACTION warning
* Cacth TypeError: native Qt signal is not callable
* Fix AttributeError: 'C7200' object has no attribute 'warning_signal'
* Catch missing function listxattr on some linux host
* Disallow opening a .gns3 on a remote server
* Fix project closing when we have multiple client connected
## 2.0.0 02/05/2017
* Clarify that we don't override vmware custom adapters
* Strip space from path at project creation
## 2.0.0rc4 20/04/2017
* Catch all error during the generation of log messages.
* Catch a rare node creation error
* Fix missing menu text at application startup
* Fix a race condition in the drawing item
* Catch system error when connecting to local server
* Catch a rare error when killing the capture
* Improve pcap streaming speed
* Upgrade to 5.7.1
* Recent projects list bug
* Fix a race condition in the preferences dialog
* Try to fix some windows Z issues
* Catch a garbage collection issue in the right click on a link
* Fix a compatibility issue with Python 3.4
## 1.5.4 13/04/2017
* Limit ubridge permission to the admin group on OSX
* Upgrade to Qt 5.7.1 on Windows
## 2.0.0rc3 31/03/2017
* Improve timeout handling
* Improve logging when we display a qt message box
* Try to detect computer hibernation
* Fix crash when we send some errors to the user console
* Use QtFile for managing file capture
* Allow to delete a profile from the profile select dialog
* Filter hidden folder in the profil directory
* Prevent user putting port in the remote host name
* Fix RuntimeError: wrapped C/C++ object of type EllipseItem has been deleted
* Fix a rare error in LinkItem
* Fix Image field in nodes list is stale after changing an image
* Fix RuntimeError: Set changed size during iteration
* Better detection of remote server changes
* Add a notice about the fact you need to apply server settings
* Check python version only for setup.py install
* Catch appliance error when creating an appliance new version
* If a node can't be deleted do not remove it
* If something is wrong during packet capture do not disconnect us from the server
* Fix saving dynamips
* Try to fix the hang dialog on some computers
* Fix a rare crash in progress dialog
* If we pass --profile skip the profile select dialog
* Raise an error if the progress dialog is not created from the main thread
* Log qt log to python log
* Fix image are not uploaded to remote main server
* Fix race condition when editing a project
* Poll settings each 5 seconds
* Avoid progress dialog not disapear
* Remove wrong mention about the fact super putty is include
* Avoid a crash when an ios router don't have a chassis
* Fix a potentatial crash in the progress dialog
* Support official docker images in appliances
## 2.0.0rc2 10/03/2017
* Deploy on pypi when we tag
* Fix rare crash in GNS3 VM preference page
* Fix an error on Windows when loading SVG files
* Prevent a potential crash
* Workaround a rare crash when sending analytics
* Catch error when you try to create a node a not existing server
* Fix an error when your local server crash and computer return non unicode
* Fix KeyError: 'slot1'
* Fix a rare crash in import appliance
* Rollback to PyQT 5.8 because 5.8.1 seem to have trouble at install
* Update pyqt5 from 5.8 to 5.8.1
## 2.0.0 RC 1 06/03/2017
* UltraVNC support
* Display less noisy dialog when we can't connect to the remote server
* Prevent the usage of gns3vm as a remote server name
* Fix the VMware wizard for not using a remote server by default
* Prevent the GNS3 VM to appear in remote compute in the VM wizard
* Remove iouyap settings
* Fix missing permission error management
* Avoid a crash when create a new dynamips version in the appliance wizard
* Disallow user to add the same server as a remote server and as local server
* Fix 'module' object has no attribute 'run'
* Monitor and display local server stderr
* Fix some import errors
* Remove placeholder string from appliance wizard
* Avoiding calling multiple time /computes at the same time. And reduce timeout
* Support for appliance v4
* Some tweaks for enabling/disabling HDPI mode.
* Do not display error at first step of the setup wizard
* Disable HDPI by default on Linux and allow to configure it
* Fix an issue when you edit a VPCS node from the node view
* Catch a race condition in managing error static assets download
* Handle error if you try to import an appliance without having the images
* Improve crash proof code of the progress dialog
## 2.0.0 beta 4 19/01/2017
* Update pyqt5 from 5.7.1 to 5.8
* Drop from console view the show command not supported by 2.0
* Try to avoid segfault in some PyQT version
* Support for strike and underline
* Do not use native font selector on mac it could crash
* Use a dedicated QNetwork manager for notification
* Fix a display error in console error message
* Use signal for writting on console to avoid some potential segfault
* Fix a rare warning
* Add more debug when we have an http error
* Disable timeout on project open
* Support for gvncviewer
* Fix a rare crash in the file editor dialog
* Fix a race condition when we display the error
* Fix an issue with invalid hostname detected as an IPV6
* When you update a a node from the node view send settings to controller
* Fix error when permission on the loaded image is broken
* Fix crash with invalid image file in appliance wizard
* Fix error when loading an handmade appliance file
* Fix no error if your VNC client is not configured
* Avoid high cpu usage when connection is lost
* Support {name} in cloud template
* Fix text of the export dialog
* Fix error message when a project is already open
* Fix missing info in tooltip of ethernet switch
* The server manage the vmname when we update the linked virtual box VM
* Fix z value for text
* Avoid a segfault when display an error
* Add sata options in the appliance schema
* Fix a rare crash when exporting IOU configurations
* Allow additionnal properties in registry files
* Fix a potential crash when a symbol is not found
* Strip unused code for OVA support in the registry
* Increase the timeout for killing local server
* Fix error when changing the layer of a drawing item
* Fix double click for open file on OSX
* Add debug to see the arguments use to start the application
* Put the selected engine in the first position of the listbox
* Fix rare crash with dynamips
* Fix rare crash in the progress dialog
* Fix a rare crash in console view
* Fix crash when you drag a file inside GNS3
## 2.0.0 beta 3 19/01/2017
* Fix error if you already have an image with a different name on remote server
* Drop gns3 converter from requirements
* Show correct server name in tooltip
* Menu item to open controller webpage
* Fixes potential exception when adding network module to an IOS router. Fixes #1774.
* Do not export a file config file if empty
* Allow to set console type in qemu wizard
* Fix overwrite of projects
* Fix creation of new appliance version when filename is different
* Fix you can't configure port 0 on ethernet switch
* Fix a race condition when saving as a project and closing it
* Reorder multi link when you delete one
* Ensure we can't connect to occupy port
* Fix AttributeError: 'QImageSvgRenderer' object has no attribute '_svg'
* Fix Unsaved preferences in GNS3 VM warning
* Force margins in configuration tabs.
* Sata disk interface support for Qemu VMs.
* Remove "sata" disk interface. Does not exist in Qemu. Ref #1749
* Add SATA and none disk interfaces on Qemu VM configuration page. Fixes #1749.
* Update pyqt5 from 5.7 to 5.7.1
* Fix TypeError: argument of type 'NoneType' is not iterable
* Fix an error when you edit readme and no projet is opened
* Upgrade Qt 5.7
## 1.5.3 12/01/2017
* Upgrade Qt 5.7
## 2.0.0 beta 2 20/12/2016
* AUX console button text change in MainWindow.
* Fix GNS3 Client not connecting to remote controller
* Delete from project list deleted projects
* Keep a shared list of projects internally
* Fix recent files in new project dialog
* Move recent projects to the file menu
* Fix Tail process for wireshark trace not killed when we change project
* Move project menu items. Ref #1713.
* Display recent files for local controller, recent project for remote controller
* Do not display the remote server if the server is use as a GNS3 VM
* If the notification stream is stopped by something we auto reconnect
* Ignore system proxy to avoid trouble with "Security Suites"
* Avoid close and delete a project at the same time
* Alpha sort of servers summaries
* Fix new remote server doesn't show up in compute summary
* Fix interface number for Switch & Hub templates
* Fix sync of node alignements with the server
* Fix rare condition when you close a project and add a node
* Options -q for quiet startup
* Fix an error when apply permission on OSX
* Support Qemu cpus in GNS3A
* Support for BIOS images
* Fix IdlePC can't be found during setup wizard
## 2.0.0 beta 1 07/12/2016
* Use osascript on OSX for asking admin permission
* Change the method for creating the tmpdir for symbols cache
* Fix a connection error at the end of the setup wizard
* Change how some tabs are organized or named.
* General settings => local settings
* Drop more reference to use local server
* Remove local server checkbox from preferences
* Make sure to not start local server during setup wizard remote server
* Fix Error when editing IOS image created using .gns3a file
* Fix test suites around sip deleted
* Do not auto start the local server in setup wizard
* On OSX execute all sudo in a single operation
* Catch key Compute is missing during conversion error
* Fix rare crash in gns3.dialogs.appliance_wizard in validateCurrentPage
* Fix AttributeError: 'Nat' object has no attribute 'configPage'
* Catch one more RuntimeError: wrapped C/C++
* Fix a rare crash in port
* Fix a rare crash when set symbol
* Fix a potential crash
* Fix a potential crash at exit
* Fix crashes
* Remove unused settings from general preferences
* Catch error when you try to import a IOU bin as a licence
* Fix rare crash when exiting
* Fix crash when freeing some ressources
* Fix timeout when exporting large project
* Avoid a rare crash when we free a port
* Fix you can't download symbols after you got an error
## 2.0.0 alpha 4 24/11/2016
* Mark preferences changes when you change a QPlainTextEdit
* Force the VPCS config initial file
* Replace the IOU licence path by an input text
* Fix 403 when loading a remote project
* Fix some possible server not starting on Windows
* Hide the connection refused dialog when we success to reconnect
* Avoid a rare crash when changing topology
* When loading another project disconnect from current project
* Do not crash if we can't list remote list of GNS3 VM engines
* Init the VPCS base config
* Fix invalid ressource path on OSX
* Fix segfault when deleting a node
* Do not download multiple time the same symbol
* Kill tail process when capture stop
* Fix Topology summary contain non existing links
* Fix a rare crash when deleting a link
* Fix export of debug informations when not connected to the controller
* Fix AttributeError: 'DockerVM' object has no attribute 'server'
* Fix error message if you double click on builtin switch
* Fix a rare crash in packet capture
* Restrict ubridge to admin users on OSX
* Natural sort of Nodes in topology summary
* Drop serial console type
* Display an error if you try to open a 0.8.x file
* Fix tab order when editing a compute
* Fix a crash in ethernet switch settings
* Dissallow unknown extensions
## 2.0.0 alpha 3 28/10/2016
* Fix error when opening a project from the cli with a gns3 installed via setup.py
* Fix a rare crash in snapshot dialog
* Fix crash when importing project on a remote server
* Fix crash in appliance wizard
* Fix crash when local server is not available
* Disallow to overwrite a running project
* Fix a rare crash when deleting a link
* Fix appliance with wrong file name after import
* Fix a crash at startup on Mac when coming from old GNS3 version
* Fix key error in settings if a compute no longer exists
* All check for vmware linked base are already made server side
* Fix Save as is not switching to the saved project
* Auto reopen a project if connection is lost
* Empty the list of computes nodes when connection is lost
* Try to fix duplicate nodes after snapshot restore on some user computer
* Allow only IPV4 in setup wizard
* Catch error if user tmp directory is read only
* Raise a proper error if packet capture program is invalid
* Fix AttributeError: 'NoneType' object has no attribute 'upper'
* Fix rare crash when killing wireshark
* Export debug informations also from the controller
* Fix a crash in vm wizard
* Fix error when uploading an images from preferences
* Fix snap to grid when initialy drop a node in the topology
* Optimize snap-to-grid code
* Fix a crash with linked clone
* Move prevent using twice the same VM when linked clone is not enable
* Fix If you show interface label and delete the link ghost interface label will appear
* Display short interface label instead of long version
* Fix error AttributeError: 'NoneType' object has no attribute 'capabilities'
* Fix PermissionError when killing local server
* Handle empty color
* Fix rare crash in save as
* Fix crash in restore default server settings
* Fix an error during import of some 0.8x projects
## 2.0.0 alpha 2 20/10/2016
* Support pure remote server for importing appliance
* Dissallow binding GNS3 server to an IPV6 (not supported by some emulators)
* Drop vmware host type choice in client
* Ask user to restart GNS3 after VMware installation
* Improve duplicate prevention in topology summary
* Add a duplicate button in the project library dialog
* Fix error introduce in previous commits
* Fix duplicates in recent project list
* Fix a project override error
* Fix Duplicated node in node summary when restoring a snapshot
* Fix a crash in the VMware / VirtualBox wizard
* If console host is 0.0.0.0 use controller address
* Fix save issue when importing an appliance
* Strip HTML in console view logs and log files
* Fix TypeError: _expandAllSlot() takes 1 positional argument but 2 were given
* Fix Cannot open created project by using Recents projects
* Update edit project Ui.
* Update crash report key
* Fix a crash when exporting debug without project open
* Fix a crash in rare condition when logging informations to the console
* Fix a crash in compute summary view
* Add a text about how to change the topology size in 2.0 in general preferences
* Improve warning when connection issue to GNS3 VM
* Fix crash in setup wizard
* Fix the wizard for creating appliance template doesn't support remote main server
* Appliance wizard support remote controller
* Fix Browse button is not working in the local server page in the setup wizard
* Check if local server is running in the setup wizard
* Hide setup wizard after first successful run
* Import appliance and New project are display at the same time
* Support remote controller in the setup wizard
* Fix When importing a gns3a the correct qemu binary is not selected
* Increase creation timeout for docker container
* Make WaitForLambdaWorker more crash proof
* Fix a crash when importing appliance
* Fix error in import appliances
* Try to fix the a segfault when importing appliance
* Fix crash in upload images
* Trust the server for link creation error (avoid sync issue)
* Fix an Error in server preference page
* Fix compatibility with remote server of 1.X
* New appliance dialog should not be display if you cancel the setup wizard
## 2.0.0 alpha 1 29/09/2016
* Save as you go
* Smart packet capture
* Capture on any link between any node
* Select where to run a VPCS node
* Delete a project from the GUI
* Project options
* The cloud is a real node
* Cloud templates
* New cloud interface
* VPCS / Ethernet Switch / Ethernet Hub templates
* Search OS images in multiple locations
* Periodic extraction of startup configs for Dynamips and IOU
* Custom cloud, Ethernet hub and Ethernet switch templates
* Snap to grid for all objects
* Synchronize the node templates when using multiple GUI
* Link label style
* New place holders in command line for opening consoles
* %i will be replaced by the project UUID
* %c will be replaced by the connection string
* Export a portable project from multiple remote servers
* New save as
* Snapshots with remote servers
* Better start / stop / suspend all nodes
* Edit config
* NAT node
* Support for colorblind users
* Support for non local server
* Support for profiles
* Suspend the GNS3VM when closing GNS3
* Edit the scene size
* New API
## 1.5.3 rc1 20/12/2016
* Fix Error when editing IOS image created using .gns3a file
* Fix error when opening a project from the cli with a gns3 installed via setup.py
* Fix a crash at startup on Mac when coming from old GNS3 version
* Fix an error during import of some 0.8x projects
* Ask for restart after installing vmrun
* Improve warning when connection issue to GNS3 VM
* Changes wording in VM wizards.
* Changed sentence.
* Display an error if settings come from a more recent version of GNS3
* Fix Error when no GNS3 VM is configured and you click on new Docker or IOU
* Disallow / in docker container name
* Update iTerm3 console settings
* Fix rename ethernet switch doesn't release the name
* Support for VNC display number in command line replacement
* Fix a crash when a directory with image is not accessible at gns3a import
## 1.5.2 18/08/2016
* Make more clear that VMware VM are not ESXi

13
COPYING
View File

@@ -272,10 +272,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License notice for gns3-converter
---------------------------------
https://github.com/dlintott/gns3-converter/blob/master/COPYING
License notice for pywin32
--------------------------
https://github.com/SublimeText/Pywin32/blob/master/License.txt
@@ -497,3 +493,12 @@ https://github.com/allanlei/python-zipstream/blob/master/LICENSE
Source code is available here:
https://pypi.python.org/pypi/zipstream
Licence notice for aiohttp_cors
-------------------------------
Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>.
Licensed under the Apache License, Version 2.0, see LICENSE file for details.
https://github.com/aio-libs/aiohttp_cors

View File

@@ -40,3 +40,8 @@ Or start the app with --debug flag.
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
https://github.com/Kozea/wdb
Security issues
----------------
Please contact us using contact informations available here:
http://docs.gns3.com/1ON9JBXSeR7Nt2-Qum2o3ZX0GU86BZwlmNSUgvmqNWGY/index.html

19
appveyor.yml Normal file
View File

@@ -0,0 +1,19 @@
version: '{build}-{branch}'
image: Visual Studio 2015
platform: x64
environment:
PYTHON: "C:\\Python36-x64"
DISTUTILS_USE_SDK: "1"
install:
- cinst nmap
- "%PYTHON%\\python.exe -m pip install -r dev-requirements.txt"
- "%PYTHON%\\python.exe -m pip install -r win-requirements.txt"
build: off
test_script:
- "%PYTHON%\\python.exe -m pytest -v"

View File

@@ -1,7 +1,6 @@
-rrequirements.txt
pep8
pytest
pytest-pythonpath # useful for running tests outside tox
pytest-timeout
pytest-capturelog
pep8==1.7.0
pytest==3.1.0
pytest-pythonpath==0.7.1 # useful for running tests outside tox
pytest-timeout==1.2.0

View File

@@ -33,6 +33,8 @@ sys.path.insert(0, os.path.dirname(sys.executable))
sys.path.insert(0, os.path.join(os.path.dirname(sys.executable), 'site-packages'))
sys.frozen = True
sys.executable = "/Applications/GNS3.app/Contents/MacOS/gns3"
os.environ["_"] = "/Applications/GNS3.app/Contents/MacOS/gns3"
module = importlib.import_module("gns3.main")
module.main()

View File

@@ -29,14 +29,20 @@ log = logging.getLogger(__name__)
class Application(QtWidgets.QApplication):
file_open_signal = QtCore.pyqtSignal(str)
def __init__(self, argv):
def __init__(self, argv, hdpi=True):
self.setStyle(QtWidgets.QStyleFactory.create("Fusion"))
# both Qt and PyQt must be version >= 5.6 in order to enable high DPI scaling
if parse_version(QtCore.QT_VERSION_STR) >= parse_version("5.6") and parse_version(QtCore.PYQT_VERSION_STR) >= parse_version("5.6"):
# only available starting Qt version 5.6
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
if hdpi:
if sys.platform.startswith("linux"):
log.warning("HDPI mode is enabled. HDPI support on Linux is not fully stable and GNS3 may crash depending of your version of Linux. To disabled HDPI mode please edit ~/.config/GNS3/gns3_gui.conf and set 'hdpi' to 'false'")
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
else:
log.info("HDPI mode is disabled")
self.setAttribute(QtCore.Qt.AA_DisableHighDpiScaling)
super().__init__(argv)

328
gns3/base_node.py Normal file
View File

@@ -0,0 +1,328 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Base class for node classes.
"""
from .qt import QtCore
from .ports.port import Port
import logging
log = logging.getLogger(__name__)
class BaseNode(QtCore.QObject):
"""
BaseNode implementation.
:param module: Module instance
:param server: client connection to a server
:param project: Project instance
"""
# signals used to let the GUI know about some events.
created_signal = QtCore.Signal(int)
started_signal = QtCore.Signal()
stopped_signal = QtCore.Signal()
suspended_signal = QtCore.Signal()
updated_signal = QtCore.Signal()
loaded_signal = QtCore.Signal()
deleted_signal = QtCore.Signal()
error_signal = QtCore.Signal(int, str)
server_error_signal = QtCore.Signal(int, str)
_instance_count = 1
_allocated_names = set()
# node statuses
stopped = 0
started = 1
suspended = 2
# node categories
routers = 0
switches = 1
end_devices = 2
security_devices = 3
def __init__(self, module, compute, project):
super().__init__()
# create an unique ID
self._id = BaseNode._instance_count
BaseNode._instance_count += 1
self._module = module
self._compute = compute
assert project is not None
self._project = project
self._initialized = False
self._loading = False
self._status = BaseNode.stopped
self._ports = []
self._links = set()
def links(self):
"""
Links connected to the node
"""
return self._links
def addLink(self, link):
self._links.add(link)
def deleteLink(self, link):
try:
self._links.remove(link)
except KeyError:
pass
@classmethod
def reset(cls):
"""
Reset the instance count.
"""
cls._instance_count = 1
def module(self):
"""
Returns this node module.
:returns: Module instance
"""
return self._module
def compute(self):
"""
Returns this node compute.
:returns: Compute instance
"""
return self._compute
def project(self):
"""
Returns this node project.
:returns: Project instance
"""
return self._project
def id(self):
"""
Returns this node identifier.
:returns: node identifier (integer)
"""
return self._id
def setId(self, new_id):
"""
Sets an identifier for this node.
:param new_id: node identifier (integer)
"""
self._id = new_id
# update the instance count to avoid conflicts
if new_id >= BaseNode._instance_count:
BaseNode._instance_count = new_id + 1
def status(self):
"""
Returns the status of this node.
0 = stopped, 1 = started, 2 = suspended.
:returns: node status (integer)
"""
return self._status
def setStatus(self, status):
"""
Sets a status for this node.
0 = stopped, 1 = started, 2 = suspended.
:param status: node status (integer)
"""
if status == self._status:
return
self._status = status
if status == self.started:
for port in self._ports:
# set ports as started
port.setStatus(Port.started)
self.started_signal.emit()
log.info("{} has started".format(self.name()))
elif status == self.stopped:
for port in self._ports:
# set ports as stopped
port.setStatus(Port.stopped)
self.stopped_signal.emit()
log.info("{} has stopped".format(self.name()))
elif status == self.suspended:
for port in self._ports:
# set ports as suspended
port.setStatus(Port.suspended)
self.suspended_signal.emit()
log.info("{} has suspended".format(self.name()))
def initialized(self):
"""
Returns if the node has been initialized
:returns: boolean
"""
return self._initialized
def setInitialized(self, initialized):
"""
Sets if the node has been initialized
:param initialized: boolean
"""
self._initialized = initialized
def update(self, new_settings):
"""
Updates the settings for this node.
Must be overloaded.
:param new_settings: settings dictionary
"""
raise NotImplementedError()
def ports(self):
"""
Returns all the ports for this node.
:returns: list of Port instances
"""
return self._ports
@staticmethod
def defaultCategories():
"""
Returns the default categories.
:returns: dict
"""
categories = {"Routers": BaseNode.routers,
"Switches": BaseNode.switches,
"End devices": BaseNode.end_devices,
"Security devices": BaseNode.security_devices}
return categories
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this node.
Must be overloaded.
:returns: symbol path (or resource).
"""
raise NotImplementedError()
@staticmethod
def symbolName():
"""
Returns the symbol name (for the nodes view).
:returns: name (string)
"""
raise NotImplementedError()
@staticmethod
def categories(self):
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node category (integer)
"""
raise NotImplementedError()
def __str__(self):
"""
Must be overloaded.
"""
raise NotImplementedError()
def controllerHttpPost(self, path, callback, body={}, context={}, **kwargs):
"""
POST on current server / project
:param path: Remote path
:param callback: callback method to call when the server replies
:param body: params to send (dictionary)
:param context: Pass a context to the response callback
"""
self._project.post(path, callback, body=body, context=context, **kwargs)
def controllerHttpPut(self, path, callback, body={}, context={}, **kwargs):
"""
PUT on current server / project
:param path: Remote path
:param callback: callback method to call when the server replies
:param body: params to send (dictionary)
:param context: Pass a context to the response callback
"""
self._project.put(path, callback, body=body, context=context, **kwargs)
def controllerHttpGet(self, path, callback, context={}, **kwargs):
"""
Get on current server / project
:param path: Remote path
:param callback: callback method to call when the server replies
:param body: params to send (dictionary)
:param context: Pass a context to the response callback
"""
self._project.get(path, callback, context=context, **kwargs)
def controllerHttpDelete(self, path, callback, context={}, **kwargs):
"""
Delete on current server / project
:param path: Remote path
:param callback: callback method to call when the server replies
:param context: Pass a context to the response callback
"""
self._project.delete(path, callback, context=context, **kwargs)

120
gns3/compute.py Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import uuid
class Compute:
"""
A compute node on the remote server
"""
def __init__(self, compute_id=None):
if compute_id is None:
compute_id = str(uuid.uuid4())
self._compute_id = compute_id
self._name = compute_id
self._connected = False
self._protocol = "http"
self._host = None
self._port = 3080
self._user = None
self._password = None
self._cpu_usage_percent = None
self._memory_usage_percent = None
self._capabilities = {
"node_types": []
}
def id(self):
return self._compute_id
def name(self):
return self._name
def setName(self, name):
self._name = name
def connected(self):
return self._connected
def setConnected(self, value):
self._connected = value
def port(self):
return self._port
def setPort(self, port):
self._port = port
def user(self):
return self._user
def setUser(self, user):
self._user = user
def setPassword(self, password):
self._password = password
def protocol(self):
return self._protocol
def setProtocol(self, protocol):
self._protocol = protocol
def host(self):
return self._host
def setHost(self, host):
self._host = host
def setCpuUsagePercent(self, usage):
self._cpu_usage_percent = usage
def cpuUsagePercent(self):
return self._cpu_usage_percent
def setMemoryUsagePercent(self, usage):
self._memory_usage_percent = usage
def memoryUsagePercent(self):
return self._memory_usage_percent
def capabilities(self):
return self._capabilities
def setCapabilities(self, val):
self._capabilities = val
def __str__(self):
return self._compute_id
def __json__(self):
return {
"host": self._host,
"port": self._port,
"protocol": self._protocol,
"user": self._user,
"password": self._password,
"name": self._name,
"compute_id": self._compute_id
}
def __eq__(self, v):
if isinstance(v, Compute):
return self.__json__() == v.__json__()
return False

225
gns3/compute_manager.py Normal file
View File

@@ -0,0 +1,225 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .qt import QtCore
from .compute import Compute
from .controller import Controller
import sys
import copy
import urllib
import datetime
import logging
log = logging.getLogger(__name__)
class ComputeManager(QtCore.QObject):
created_signal = QtCore.Signal(str)
updated_signal = QtCore.Signal(str)
deleted_signal = QtCore.Signal(str)
def __init__(self):
super().__init__()
self._computes = {}
self._controller = Controller.instance()
self._controller.connected_signal.connect(self._controllerConnectedSlot)
self._controller.disconnected_signal.connect(self._controllerDisconnectedSlot)
self._controllerConnectedSlot()
# If we receive fresh data from the notification feed no need to refresh via an API call
self._last_computes_refresh = datetime.datetime.now().timestamp()
self._timer = QtCore.QTimer()
self._timer.setInterval(1000)
self._refreshingComputes = False
self._timer.timeout.connect(self._refreshComputesSlot)
self._timer.start()
def _refreshComputesSlot(self):
if self._refreshingComputes:
return
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 5:
self._last_computes_refresh = datetime.datetime.now().timestamp()
self._refreshingComputes = True
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=15)
def _controllerConnectedSlot(self):
if self._controller.connected():
self._refreshingComputes = True
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=15)
def _controllerDisconnectedSlot(self):
for compute_id in list(self._computes):
del self._computes[compute_id]
self.deleted_signal.emit(compute_id)
def _listComputesCallback(self, result, error=False, **kwargs):
self._refreshingComputes = False
if error is True:
log.error("Error while getting compute list: {}".format(result["message"]))
return
for compute in result:
self.computeDataReceivedCallback(compute)
def computeDataReceivedCallback(self, compute):
"""
Called when we received data from a compute
node.
"""
self._last_computes_refresh = datetime.datetime.now().timestamp()
new_node = False
compute_id = compute["compute_id"]
if compute_id not in self._computes:
new_node = True
self._computes[compute_id] = Compute(compute_id)
self._computes[compute_id].setName(compute["name"])
self._computes[compute_id].setConnected(compute["connected"])
self._computes[compute_id].setProtocol(compute["protocol"])
self._computes[compute_id].setHost(compute["host"])
self._computes[compute_id].setPort(compute["port"])
self._computes[compute_id].setUser(compute["user"])
self._computes[compute_id].setCpuUsagePercent(compute["cpu_usage_percent"])
self._computes[compute_id].setMemoryUsagePercent(compute["memory_usage_percent"])
self._computes[compute_id].setCapabilities(compute["capabilities"])
if new_node:
self.created_signal.emit(compute_id)
else:
self.updated_signal.emit(compute_id)
def computeIsTheRemoteGNS3VM(self, compute):
"""
:returns: Boolean True if the remote server is the remote GNS3 VM
"""
if compute.id() != "local" and compute.id() != "vm":
if self.vmCompute() and "GNS3 VM ({})".format(compute.name()) == self.vmCompute().name():
return True
return False
def computes(self):
"""
:returns: List of computes nodes
"""
computes = []
for compute in self._computes.values():
# We filter the remote GNS3 VM compute from the computes list
if not self.computeIsTheRemoteGNS3VM(compute):
computes.append(compute)
return computes
def vmCompute(self):
"""
:returns: The GNS3 VM compute node or None
"""
try:
return self._computes["vm"]
except KeyError:
return None
def localCompute(self):
"""
:returns: The local compute node or None
"""
try:
return self._computes["local"]
except KeyError:
return None
def localPlatform(self):
"""
Return the platform of the local compute.
With a remote controller it could be different of our local platform
"""
c = self.localCompute()
if c is None:
return sys.platform
return c.capabilities().get("platform", sys.platform)
def remoteComputes(self):
"""
:returns: List of non local and non VM computes
"""
return [c for c in self._computes.values() if c.id() != "local" and c.id() != "vm"]
def getCompute(self, compute_id):
if compute_id.startswith("http:") or compute_id.startswith("https:"):
u = urllib.parse.urlsplit(compute_id)
for compute in self._computes.values():
if "{}:{}".format(compute.host(), compute.port()) == u.netloc:
return compute
raise KeyError("Compute {} is missing.".format(compute_id))
if compute_id not in self._computes:
self._computes[compute_id] = Compute(compute_id)
self.created_signal.emit(compute_id)
return self._computes[compute_id]
def deleteCompute(self, compute_id):
if compute_id in self._computes:
compute = self._computes[compute_id]
del self._computes[compute_id]
self._controller.delete("/computes/" + compute_id, None)
self.deleted_signal.emit(compute_id)
def updateList(self, computes):
"""
Sync an array of compute server with remote
"""
for compute_id in copy.copy(self._computes):
# Delete compute on controller not in the new computes
if compute_id in ["local", "vm"]:
continue
if compute_id not in [c.id() for c in computes]:
log.debug("Delete compute %s", compute_id)
self.deleteCompute(compute_id)
else:
# Update the changed nodes
for c in computes:
if c.id() == compute_id and c != self._computes[compute_id]:
log.debug("Update compute %s", compute_id)
self._controller.put("/computes/" + compute_id, None, body=c.__json__())
self._computes[compute_id] = c
self.updated_signal.emit(compute_id)
# Create the new nodes
for compute in computes:
if compute.id() not in self._computes:
log.debug("Create compute %s", compute.id())
self._controller.post("/computes", None, body=compute.__json__())
self._computes[compute.id()] = compute
self.created_signal.emit(compute.id())
@staticmethod
def reset():
ComputeManager._instance = None
@staticmethod
def instance():
"""
Singleton to return only on instance of ComputeManager.
:returns: instance of ComputeManager
"""
if not hasattr(ComputeManager, '_instance') or ComputeManager._instance is None:
ComputeManager._instance = ComputeManager()
return ComputeManager._instance

View File

@@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Compute summary view that list all the compute, their status.
"""
import sip
from .qt import QtGui, QtCore, QtWidgets
from .compute_manager import ComputeManager
import logging
log = logging.getLogger(__name__)
class ComputeItem(QtWidgets.QTreeWidgetItem):
"""
Custom item for the QTreeWidget instance
(topology summary view).
:param parent: parent widget
:param compute: Compute instance
"""
def __init__(self, parent, compute):
super().__init__(parent)
self._compute = compute
self._parent = parent
self._status = "unknown"
self._refreshStatusSlot()
def _refreshStatusSlot(self):
"""
Changes the icon to show the node status (started, stopped etc.)
"""
if self is None:
return
usage = None
text = self._compute.name()
if self._compute.cpuUsagePercent() is not None:
text = "{} CPU {}%, RAM {}%".format(text, self._compute.cpuUsagePercent(), self._compute.memoryUsagePercent())
self.setText(0, text)
self.setToolTip(0, text + " on " + self._compute.capabilities().get("platform", ""))
if self._compute.connected():
self._status = "connected"
if usage is None or (self._compute.cpuUsagePercent() < 90 and self._compute.memoryUsagePercent() < 90):
self.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
else:
self.setIcon(0, QtGui.QIcon(':/icons/led_yellow.svg'))
else:
if self._status == "unknown":
self.setIcon(0, QtGui.QIcon(':/icons/led_gray.svg'))
else:
self._status = "stopped"
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
self._parent.sortItems(0, QtCore.Qt.AscendingOrder)
class ComputeSummaryView(QtWidgets.QTreeWidget):
"""
Compute summary view implementation.
:param parent: parent widget
"""
def __init__(self, parent):
super().__init__(parent)
self._computes = {}
ComputeManager.instance().created_signal.connect(self._computeAddedSlot)
ComputeManager.instance().updated_signal.connect(self._computeUpdatedSlot)
ComputeManager.instance().deleted_signal.connect(self._computeRemovedSlot)
for compute in ComputeManager.instance().computes():
self._computeAddedSlot(compute.id())
def _computeAddedSlot(self, compute_id):
"""
Called when a compute is added to the list of computes
:params url: URL of the compute
"""
compute = ComputeManager.instance().getCompute(compute_id)
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
return
self._computes[compute_id] = ComputeItem(self, compute)
def _computeUpdatedSlot(self, compute_id):
"""
Called when a compute is updated
:params url: URL of the compute
"""
if compute_id in self._computes:
compute = ComputeManager.instance().getCompute(compute_id)
# We hide the remote GNS3 VM
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
self._computeRemovedSlot(compute_id)
else:
self._computes[compute_id]._refreshStatusSlot()
else:
self._computeAddedSlot(compute_id)
def _computeRemovedSlot(self, compute_id):
"""
Called when a compute is removed to the list of computes
:params url: URL of the compute
"""
if compute_id in self._computes:
self.takeTopLevelItem(self.indexOfTopLevelItem(self._computes[compute_id]))
del self._computes[compute_id]

View File

@@ -25,13 +25,10 @@ import logging
import struct
import sip
import json
from .qt import QtCore
from .node import Node
from .qt import QtCore
from .version import __version__
try:
from gns3converter import __version__ as gns3converter_version
except ImportError:
gns3converter_version = "Not installed"
class ConsoleCmd(cmd.Cmd):
@@ -45,7 +42,6 @@ class ConsoleCmd(cmd.Cmd):
if hasattr(sys, "frozen"):
compiled = "(compiled)"
print("GNS3 version is {} {}".format(__version__, compiled))
print("GNS3 Converter version is {}".format(gns3converter_version))
print("Python version is {}.{}.{} ({}-bit) with {} encoding".format(sys.version_info[0],
sys.version_info[1],
sys.version_info[2],
@@ -188,14 +184,9 @@ class ConsoleCmd(cmd.Cmd):
:param node: Node instance
"""
name = node.name()
console_port = node.console()
console_host = node.server().host()
try:
from .telnet_console import telnetConsole
telnetConsole(name, console_host, console_port)
except (OSError, ValueError) as e:
print("Cannot start console application: {}".format(e))
from .telnet_console import nodeTelnetConsole
nodeTelnetConsole(node, console_port)
def do_debug(self, args):
"""
@@ -258,55 +249,6 @@ class ConsoleCmd(cmd.Cmd):
print("{}: no such device".format(node_name))
continue
def _show_run(self, params):
"""
Handles the 'show run' command.
:param params: list of parameters
"""
if self._topology.project is None:
print("Sorry, the project hasn't been saved yet")
return
topology = self._topology.dump()
if len(params) == 1:
# print out whole topology
print(json.dumps(topology, sort_keys=True, indent=4))
elif len(params) >= 2:
# this is a 'show run <device_name>'
params.pop(0)
for param in params:
node_name = param
node_id = None
# get the node ID
for node in self._topology.nodes():
if node.name() == node_name:
node_id = node.id()
break
if node_id is None:
print("{}: no such device".format(node_name))
continue
if "nodes" in topology["topology"]:
for node in topology["topology"]["nodes"]:
if node["id"] == node_id:
print(json.dumps(node, sort_keys=True, indent=4))
break
def _show_gnsvm(self, params):
"""
Handles the 'show gns3vm' command.
:param params: list of parameters
"""
from gns3.gns3_vm import GNS3VM
vm = GNS3VM.instance()
print("Running: {}".format(vm.isRunning()))
print("Settings: {}".format(vm.settings()))
def do_show(self, args):
"""
Show detail information about every device in current lab:
@@ -314,15 +256,6 @@ class ConsoleCmd(cmd.Cmd):
Show detail information about a device:
show device <device_name>
Show the whole topology:
show run
Show topology info of a device:
show run <device_name>
Show the GNS3 VM status
show gns3vm
"""
if '?' in args or args.strip() == "":
@@ -332,10 +265,6 @@ class ConsoleCmd(cmd.Cmd):
params = args.split()
if params[0] == "device":
self._show_device(params)
elif params[0] == "run":
self._show_run(params)
elif params[0] == "gns3vm":
self._show_gnsvm(params)
else:
print(self.do_show.__doc__)

View File

@@ -15,13 +15,14 @@
# 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 platform
import sys
import sip
import struct
import inspect
import datetime
import platform
from .qt import QtCore
from .qt import QtCore, Qt
from .topology import Topology
from .version import __version__
from .console_cmd import ConsoleCmd
@@ -29,9 +30,39 @@ from .pycutext import PyCutExt
from .modules import MODULES
from .local_config import LocalConfig
import logging
log = logging.getLogger(__name__)
class ConsoleLogHandler(logging.StreamHandler):
"""
Display log event to the console
"""
def emit(self, record):
if sip.isdeleted(self._console_view):
return
message = self.format(record)
level_no = record.levelno
if level_no >= logging.ERROR:
self._console_view.write_message_signal.emit("{}\n".format(message), "error")
elif level_no >= logging.WARNING:
self._console_view.write_message_signal.emit("{}\n".format(message), "warning")
elif level_no >= logging.INFO:
# To avoid noise on console we display all event only if log level is debug
# or if we force the display in the log record
if "show" in record.__dict__ or logging.getLogger().getEffectiveLevel() == logging.DEBUG:
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
elif level_no >= logging.DEBUG:
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
class ConsoleView(PyCutExt, ConsoleCmd):
# Emit this signal to write a message on console
write_message_signal = QtCore.Signal(str, str)
def __init__(self, parent):
# Set the prompt PyCutExt
@@ -41,13 +72,10 @@ class ConsoleView(PyCutExt, ConsoleCmd):
# Set introduction message
bitness = struct.calcsize("P") * 8
current_year = datetime.date.today().year
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {}.\n" \
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, current_year)
if LocalConfig.instance().experimental():
self.intro += "\nWARNING: Experimental features enable. You can use some unfinished features and lost data."
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, Qt.PYQT_VERSION_STR, current_year)
# Parent class initialization
try:
@@ -66,14 +94,39 @@ class ConsoleView(PyCutExt, ConsoleCmd):
except Exception as e:
sys.stderr.write(e)
self._handleLogs()
if LocalConfig.instance().experimental():
log.warning("WARNING: Experimental features enable. You can use some unfinished features and lost data.")
for module in MODULES:
instance = module.instance()
instance.notification_signal.connect(self.writeNotification)
self.write_message_signal.connect(self._writeMessageSlot)
# required for Cmd module (do_help etc.)
self.stdout = sys.stdout
self._topology = Topology.instance()
def _writeMessageSlot(self, message, level):
if level == "error":
self.write(message, error=True)
elif level == "warning":
self.write(message, warning=True)
else:
self.write(message)
def _handleLogs(self):
"""
Catch log message and display them
"""
log = logging.getLogger()
log_handler = ConsoleLogHandler()
log_handler._console_view = self
log.addHandler(log_handler)
def isatty(self):
"""
For exception handling purposes
@@ -138,69 +191,64 @@ class ConsoleView(PyCutExt, ConsoleCmd):
"""
text = "Server notification: {}".format(message)
self.write(text, error=True)
self.write("\n")
if details:
self.write(details)
self.write("\n")
text += "\n" + details
self.write_message_signal.emit(text, "info")
def writeError(self, node_id, message):
def writeError(self, base_node_id, message):
"""
Write error messages.
:param node_id: node identifier
:param base_node_id: base node identifier
:param message: error message
"""
node = Topology.instance().getNode(node_id)
node = Topology.instance().getNode(base_node_id)
name = ""
if node and node.name():
name = " {}:".format(node.name())
text = "Error:{name} {message}".format(name=name,
message=message)
self.write(text, error=True)
self.write("\n")
self.write_message_signal.emit(text, "error")
def writeWarning(self, node_id, message):
def writeWarning(self, base_node_id, message):
"""
Write warning messages.
:param node_id: node identifier
:param base_node_id: base node identifier
:param message: warning message
"""
node = Topology.instance().getNode(node_id)
node = Topology.instance().getNode(base_node_id)
name = ""
if node and node.name():
name = " {}:".format(node.name())
text = "Warning:{name} {message}".format(name=name,
message=message)
self.write(text, warning=True)
self.write("\n")
self.write_message_signal.emit(text, "warning")
def writeServerError(self, node_id, message):
def writeServerError(self, base_node_id, message):
"""
Write server error messages coming from the server.
:param node_id: node identifier
:param base_node_id: Base node identifier
:param code: error code
:param message: error message
"""
node = Topology.instance().getNode(node_id)
node = Topology.instance().getNode(base_node_id)
server = name = ""
if node:
if node.name():
name = " {}:".format(node.name())
server = "from {}".format(node.server().url())
server = "from {}".format(node.compute().name())
text = "Server error {server}:{name} {message}".format(server=server,
name=name,
message=message)
self.write(text, error=True)
self.write("\n")
self.write_message_signal.emit(text.strip(), "error")
def _run(self):
"""

292
gns3/controller.py Normal file
View File

@@ -0,0 +1,292 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import hashlib
import tempfile
from .qt import QtCore, QtGui, QtWidgets, qpartial, qslot
from .symbol import Symbol
from .local_server_config import LocalServerConfig
from .settings import LOCAL_SERVER_SETTINGS
import logging
log = logging.getLogger(__name__)
class Controller(QtCore.QObject):
"""
An instance of the GNS3 server controller
"""
connected_signal = QtCore.Signal()
disconnected_signal = QtCore.Signal()
connection_failed_signal = QtCore.Signal()
project_list_updated_signal = QtCore.Signal()
def __init__(self, parent=None):
super().__init__()
self._connected = False
self._connecting = False
self._cache_directory = tempfile.mkdtemp()
self._http_client = None
# If it's the first error we display an alert box to the user
self._first_error = True
self._error_dialog = None
self._display_error = True
self._projects = []
# If we do multiple call in order to download the same symbol we queue them
self._static_asset_download_queue = {}
def host(self):
return self._http_client.host()
def isRemote(self):
"""
:returns Boolean: True if the controller is remote
"""
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
return not settings["auto_start"]
def connecting(self):
"""
:returns: True if connection is in progress
"""
return self._connecting
def connected(self):
"""
Is the controller connected
"""
return self._connected
def httpClient(self):
"""
:returns: HTTP client for connected to the controller
"""
return self._http_client
def setHttpClient(self, http_client):
"""
:param http_client: Instance of HTTP client to communicate with the server
"""
self._http_client = http_client
if self._http_client:
if self.isRemote():
self._http_client.setMaxTimeDifferenceBetweenQueries(120)
self._http_client.connection_connected_signal.connect(self._httpClientConnectedSlot)
self._http_client.connection_disconnected_signal.connect(self._httpClientDisconnectedSlot)
self._connectingToServer()
def setDisplayError(self, val):
"""
Allow error to be visible or not
"""
self._display_error = val
self._first_error = True
def _connectingToServer(self):
"""
Connection process as started
"""
self._connected = False
self._connecting = True
self.get('/version', self._versionGetSlot)
def _httpClientDisconnectedSlot(self):
if self._connected:
self._connected = False
self.disconnected_signal.emit()
self._connectingToServer()
def _versionGetSlot(self, result, error=False, **kwargs):
"""
Called after the inital version get
"""
if error:
if self._first_error:
self._connecting = False
self.connection_failed_signal.emit()
if "message" in result and self._display_error:
self._error_dialog = QtWidgets.QMessageBox(self.parent())
self._error_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
self._error_dialog.setWindowTitle("Connection to server")
self._error_dialog.setText("Error when connecting to the GNS3 server:\n{}".format(result["message"]))
self._error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
self._error_dialog.show()
# Try to connect again in x seconds
QtCore.QTimer.singleShot(5000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))
self._first_error = False
else:
self._first_error = True
if self._error_dialog:
self._error_dialog.reject()
self._error_dialog = None
def _httpClientConnectedSlot(self):
if not self._connected:
self._connected = True
self._connecting = False
self.connected_signal.emit()
self.refreshProjectList()
def get(self, *args, **kwargs):
return self.createHTTPQuery("GET", *args, **kwargs)
def getCompute(self, path, compute_id, *args, **kwargs):
"""
API get on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/{}{}".format(compute_id, path)
return self.get(path, *args, **kwargs)
def post(self, *args, **kwargs):
return self.createHTTPQuery("POST", *args, **kwargs)
def postCompute(self, path, compute_id, *args, **kwargs):
"""
API post on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/{}{}".format(compute_id, path)
return self.post(path, *args, **kwargs)
def __fix_compute_id(self, compute_id):
"""
Support for remote server <= 1.5
This fix should be not require after the 2.1
when all the appliance template will be managed
on server
"""
if compute_id.startswith("http:") or compute_id.startswith("https:"):
from .compute_manager import ComputeManager
try:
return ComputeManager.instance().getCompute(compute_id).id()
except KeyError:
return compute_id
return compute_id
def put(self, *args, **kwargs):
return self.createHTTPQuery("PUT", *args, **kwargs)
def delete(self, *args, **kwargs):
return self.createHTTPQuery("DELETE", *args, **kwargs)
def createHTTPQuery(self, method, path, *args, **kwargs):
"""
Forward the query to the HTTP client or controller depending of the path
"""
if self._http_client:
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
def getSynchronous(self, endpoint, timeout=2):
return self._http_client.getSynchronous(endpoint, timeout)
@staticmethod
def instance():
"""
Singleton to return only on instance of Controller.
:returns: instance of Controller
"""
if not hasattr(Controller, '_instance') or Controller._instance is None:
Controller._instance = Controller()
return Controller._instance
def getStatic(self, url, callback):
"""
Get a URL from the /static on controller and cache it on disk
:param url: URL without the protocol and host part
:param callback: Callback to call when file is ready
"""
if not self._http_client:
return
m = hashlib.md5()
m.update(url.encode())
if ".svg" in url:
extension = ".svg"
else:
extension = ".png"
path = os.path.join(self._cache_directory, m.hexdigest() + extension)
if os.path.exists(path):
callback(path)
elif path in self._static_asset_download_queue:
self._static_asset_download_queue[path].append(callback)
else:
self._static_asset_download_queue[path] = [callback]
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, url, path))
def _getStaticCallback(self, url, path, result, error=False, raw_body=None, **kwargs):
if error:
log.error("Error while downloading file: {}".format(url))
if path in self._static_asset_download_queue:
del self._static_asset_download_queue[path]
return
try:
with open(path, "wb+") as f:
f.write(raw_body)
except OSError as e:
log.error("Can't write to {}: {}".format(path, str(e)))
return
log.debug("File stored {} for {}".format(path, url))
for callback in self._static_asset_download_queue[path]:
callback(path)
del self._static_asset_download_queue[path]
def getSymbolIcon(self, symbol_id, callback):
"""
Get a QIcon for a symbol from the controller
:param url: URL without the protocol and host part
:param callback: Callback to call when file is ready
"""
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback))
def _getIconCallback(self, callback, path):
icon = QtGui.QIcon()
icon.addFile(path)
callback(icon)
def deleteProject(self, project_id, callback=None):
Controller.instance().delete("/projects/{}".format(project_id), qpartial(self._deleteProjectCallback, callback=callback, project_id=project_id))
def _deleteProjectCallback(self, result, error=False, project_id=None, callback=None, **kwargs):
if error:
log.error("Error while deleting project: {}".format(result["message"]))
else:
self.refreshProjectList()
self._projects = [p for p in self._projects if p["project_id"] != project_id]
if callback:
callback(result, error=error, **kwargs)
@qslot
def refreshProjectList(self, *args):
self.get("/projects", self._projectListCallback)
def _projectListCallback(self, result, error=False, **kwargs):
if not error:
self._projects = result
self.project_list_updated_signal.emit()
def projects(self):
return self._projects

View File

@@ -23,6 +23,7 @@ import struct
try:
import raven
from raven.transport.http import HTTPTransport
RAVEN_AVAILABLE = True
except ImportError:
# raven is not installed with deb package in order to simplify packaging
@@ -50,7 +51,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "sync+https://03a292b244b941d9abee28365c999972:038f2bb8fc9145468c3428b17e5f1e44@app.getsentry.com/38506"
DSN = "sync+https://063691a489374eda912ad454a1d80777:5ddb34d6b23c4a08b040efce23aaac78@sentry.io/38506"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):
@@ -68,20 +69,21 @@ class CrashReport:
sentry_uncaught.disabled = True
def captureException(self, exception, value, tb):
from .servers import Servers
from .local_server import LocalServer
local_server = Servers.instance().localServerSettings()
local_server = LocalServer.instance().localServerSettings()
if local_server["report_errors"]:
if not RAVEN_AVAILABLE:
return
if os.path.exists(".git"):
log.warning("A .git directory exist crash report is turn off for developers")
log.warning("A .git directory exist crash report is turn off for developers. Instant exit")
sys.exit(1)
return
if hasattr(exception, "fingerprint"):
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint])
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint], transport=HTTPTransport)
else:
client = raven.Client(CrashReport.DSN, release=__version__)
client = raven.Client(CrashReport.DSN, release=__version__, transport=HTTPTransport)
context = {
"os:name": platform.system(),
"os:release": platform.release(),

View File

@@ -16,42 +16,51 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import sip
from ..qt import QtWidgets, QtCore, QtGui, qpartial
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
from ..image_manager import ImageManager
from ..modules import Qemu
from ..registry.appliance import Appliance
from ..registry.appliance import Appliance, ApplianceError
from ..registry.registry import Registry
from ..registry.config import Config, ConfigException
from ..registry.image import Image
from ..utils import human_filesize
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
from ..utils.progress_dialog import ProgressDialog
from ..servers import Servers
from ..gns3_vm import GNS3VM
from ..compute_manager import ComputeManager
from ..controller import Controller
from ..local_config import LocalConfig
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
images_changed_signal = QtCore.Signal()
versions_changed_signal = QtCore.Signal()
def __init__(self, parent, path):
super().__init__(parent)
self.setupUi(self)
self.images_changed_signal.connect(self._refreshVersions)
self.versions_changed_signal.connect(self._versionRefreshedSlot)
self._refreshing = False
self._path = path
self.setupUi(self)
# Count how many images are curently uploading
self._image_uploading_count = 0
images_directories = list()
images_directories.append(os.path.dirname(self._path))
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
if download_directory != "" and download_directory != os.path.dirname(self._path):
images_directories.append(download_directory)
self._registry = Registry(images_directories)
self._registry.image_list_changed_signal.connect(self.images_changed_signal.emit)
self._appliance = Appliance(self._registry, self._path)
self._registry.appendImageDirectory(os.path.join(ImageManager.instance().getDirectory(), self._appliance.image_dir_name()))
self.uiApplianceVersionTreeWidget.currentItemChanged.connect(self._applianceVersionCurrentItemChangedSlot)
self.uiRefreshPushButton.clicked.connect(self._refreshVersions)
self.uiRefreshPushButton.clicked.connect(self.images_changed_signal.emit)
self.uiDownloadPushButton.clicked.connect(self._downloadPushButtonClickedSlot)
self.uiImportPushButton.clicked.connect(self._importPushButtonClickedSlot)
self.uiCreateVersionPushButton.clicked.connect(self._createVersionPushButtonClickedSlot)
@@ -61,6 +70,9 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
if Controller.instance().isRemote():
self.uiLocalRadioButton.setText("Run the appliance on the main server")
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
def initializePage(self, page_id):
@@ -117,13 +129,19 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
elif self.page(page_id) == self.uiServerWizardPage:
self.uiRemoteServersComboBox.clear()
for server in Servers.instance().remoteServers().values():
self.uiRemoteServersComboBox.addItem(server.url(), server)
if len(ComputeManager.instance().remoteComputes()) == 0:
self.uiRemoteRadioButton.setEnabled(False)
else:
self.uiRemoteRadioButton.setEnabled(True)
for compute in ComputeManager.instance().remoteComputes():
self.uiRemoteServersComboBox.addItem(compute.name(), compute)
if not GNS3VM.instance().isRunning():
if not ComputeManager.instance().vmCompute():
self.uiVMRadioButton.setEnabled(False)
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")):
if ComputeManager.instance().localPlatform() is None:
self.uiLocalRadioButton.setEnabled(False)
elif (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
if type == "qemu":
# Qemu has issues on OSX and Windows we disallow usage of the local server
if not LocalConfig.instance().experimental():
@@ -131,20 +149,20 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
elif type != "dynamips":
self.uiLocalRadioButton.setEnabled(False)
if GNS3VM.instance().isRunning():
if ComputeManager.instance().vmCompute():
self.uiVMRadioButton.setChecked(True)
elif Servers.instance().localServer().isLocalServerRunning() and self.uiLocalRadioButton.isEnabled():
elif ComputeManager.instance().localCompute() and self.uiLocalRadioButton.isEnabled():
self.uiLocalRadioButton.setChecked(True)
elif len(Servers.instance().remoteServers().values()) > 0:
elif self.uiRemoteRadioButton.isEnabled():
self.uiRemoteRadioButton.setChecked(True)
else:
self.uiRemoteRadioButton.setChecked(False)
elif self.page(page_id) == self.uiFilesWizardPage:
self._refreshVersions()
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
elif self.page(page_id) == self.uiQemuWizardPage:
Qemu.instance().getQemuBinariesFromServer(self._server, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
elif self.page(page_id) == self.uiSummaryWizardPage:
self.uiSummaryTreeWidget.clear()
@@ -167,12 +185,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiCheckServerLabel.setText("Please wait while checking server capacities...")
if 'qemu' in self._appliance:
if self._appliance['qemu'].get('kvm', 'require') == 'require':
self._server_check = False # If the server as the capacities for running the appliance
Qemu.instance().getQemuCapabilitiesFromServer(self._server, qpartial(self._qemuServerCapabilitiesCallback))
self._server_check = False # If the server as the capacities for running the appliance
self.uiCheckServerLabel.setText("")
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
return
self.uiCheckServerLabel.setText("")
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
self._server_check = True
self.next()
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
"""
@@ -193,91 +211,111 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
def _uiServerWizardPage_isComplete(self):
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
def _refreshVersions(self):
def _imageUploadedCallback(self, result, error=False, **kwargs):
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
@qslot
def _refreshVersions(self, *args):
"""
Refresh the list of files for different version of the appliance
"""
self.uiFilesWizardPage.setSubTitle("The following versions are available for " + self._appliance["product_name"] + ". Check the status of files required to install.")
self.uiApplianceVersionTreeWidget.clear()
if self._refreshing:
return
self._refreshing = True
worker = WaitForLambdaWorker(lambda: self._resfreshDialogWorker())
self.uiFilesWizardPage.setSubTitle("The following versions are available for " + self._appliance["product_name"] + ". Check the status of files required to install.")
worker = WaitForLambdaWorker(lambda: self._refreshDialogWorker())
progress_dialog = ProgressDialog(worker, "Add appliance", "Scanning directories for files...", None, busy=True, parent=self)
progress_dialog.show()
if progress_dialog.exec_():
for version in self._appliance["versions"]:
top = QtWidgets.QTreeWidgetItem(["{} {}".format(self._appliance["product_name"], version["name"])])
size = 0
status = "Ready to install"
for image in version["images"].values():
if image["status"] == "Missing":
status = "Missing files"
@qslot
def _versionRefreshedSlot(self, *args):
"""
Called when we finish to scan the disk for new versions
"""
if self._refreshing or self.currentPage() != self.uiFilesWizardPage:
return
self._refreshing = True
self.uiApplianceVersionTreeWidget.clear()
size += image.get("filesize", 0)
image_widget = QtWidgets.QTreeWidgetItem(
[
"",
image["filename"],
human_filesize(image.get("filesize", 0)),
image["status"],
image["version"],
image.get("md5sum", "")
])
for version in self._appliance["versions"]:
top = QtWidgets.QTreeWidgetItem(self.uiApplianceVersionTreeWidget, ["{} {}".format(self._appliance["product_name"], version["name"])])
size = 0
status = "Ready to install"
for image in version["images"].values():
if image["status"] == "Missing":
status = "Missing files"
if image["status"] == "Missing":
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
else:
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
# Associated data stored are col 0: version, col 1: image
image_widget.setData(0, QtCore.Qt.UserRole, version)
image_widget.setData(1, QtCore.Qt.UserRole, image)
image_widget.setData(2, QtCore.Qt.UserRole, self._appliance)
top.addChild(image_widget)
font = top.font(0)
font.setBold(True)
top.setFont(0, font)
expand = True
if status == "Missing files":
top.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
size += image.get("filesize", 0)
image_widget = QtWidgets.QTreeWidgetItem(
[
"",
image["filename"],
human_filesize(image.get("filesize", 0)),
image["status"],
image["version"],
image.get("md5sum", "")
])
if image["status"] == "Missing":
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
else:
expand = False
top.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
top.setData(2, QtCore.Qt.DisplayRole, human_filesize(size))
top.setData(3, QtCore.Qt.DisplayRole, status)
top.setData(2, QtCore.Qt.UserRole, self._appliance)
top.setData(0, QtCore.Qt.UserRole, version)
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
if expand:
top.setExpanded(True)
# Associated data stored are col 0: version, col 1: image
image_widget.setData(0, QtCore.Qt.UserRole, version)
image_widget.setData(1, QtCore.Qt.UserRole, image)
image_widget.setData(2, QtCore.Qt.UserRole, self._appliance)
top.addChild(image_widget)
font = top.font(0)
font.setBold(True)
top.setFont(0, font)
expand = True
if status == "Missing files":
top.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
else:
expand = False
top.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
top.setData(2, QtCore.Qt.DisplayRole, human_filesize(size))
top.setData(3, QtCore.Qt.DisplayRole, status)
top.setData(2, QtCore.Qt.UserRole, self._appliance)
top.setData(0, QtCore.Qt.UserRole, version)
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
# self.uiApplianceVersionTreeWidget.setCurrentItem(top)
if expand:
top.setExpanded(True)
if len(self._appliance["versions"]) > 0:
self.uiApplianceVersionTreeWidget.resizeColumnToContents(0)
self.uiApplianceVersionTreeWidget.resizeColumnToContents(1)
self.uiApplianceVersionTreeWidget.setCurrentItem(self.uiApplianceVersionTreeWidget.topLevelItem(0))
self._refreshing = False
def _resfreshDialogWorker(self):
def _refreshDialogWorker(self):
"""
Scan local directory in order to found the images on disk
"""
# Docker do not have versions
if not "versions" in self._appliance:
if "versions" not in self._appliance:
return
for version in self._appliance["versions"]:
for image in version["images"].values():
img = self._registry.search_image_file(image["filename"], image.get("md5sum"), image.get("filesize"))
img = self._registry.search_image_file(self._appliance.emulator(), image["filename"], image.get("md5sum"), image.get("filesize"))
if img:
image["status"] = "Found"
image["md5sum"] = img.md5sum
image["filesize"] = img.filesize
else:
image["status"] = "Missing"
self._refreshing = False
self.versions_changed_signal.emit()
@qslot
def _applianceVersionCurrentItemChangedSlot(self, current, previous):
"""
Called when user select a different item in the list of appliance files
@@ -286,7 +324,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiImportPushButton.hide()
self.uiExplainDownloadLabel.hide()
if current is None:
if current is None or sip.isdeleted(current):
return
image = current.data(1, QtCore.Qt.UserRole)
@@ -295,14 +333,18 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiDownloadPushButton.show()
self.uiImportPushButton.show()
def _downloadPushButtonClickedSlot(self):
@qslot
def _downloadPushButtonClickedSlot(self, *args):
"""
Called when user want to download an appliance images.
He should have selected the file before.
"""
if self._refreshing:
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
if current is None:
if current is None or sip.isdeleted(current):
return
data = current.data(1, QtCore.Qt.UserRole)
@@ -315,40 +357,48 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.warning(self, "Add appliance", "Download will redirect you where the required file can be downloaded, you may have to be registered with the vendor in order to download the file.")
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["download_url"]))
def _createVersionPushButtonClickedSlot(self):
@qslot
def _createVersionPushButtonClickedSlot(self, *args):
"""
Allow user to create a new version of an appliance
"""
new_version, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Creating a new version allows to import unknown files to use with this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.Normal)
if ok:
self._appliance.create_new_version(new_version)
self._refreshVersions()
try:
self._appliance.create_new_version(new_version)
except ApplianceError as e:
QtWidgets.QMessageBox.critical(self.parent(), "Create new version", str(e))
return
self.images_changed_signal.emit()
def _importPushButtonClickedSlot(self):
@qslot
def _importPushButtonClickedSlot(self, *args):
"""
Called when user want to import an appliance images.
He should have selected the file before.
"""
if self._refreshing:
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
if not current:
return
disk = current.data(1, QtCore.Qt.UserRole)
path, _ = QtWidgets.QFileDialog.getOpenFileName()
if len(path) == 0:
return
image = Image(path)
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct file. The MD5 sum is {} and should be {}. For OVA you need to import the OVA/OVF not the file inside the archive.".format(image.md5sum, disk["md5sum"]))
image = Image(self._appliance.emulator(), path, filename=disk["filename"])
try:
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct file. The MD5 sum is {} and should be {}.".format(image.md5sum, disk["md5sum"]))
return
except OSError as e:
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
return
config = Config()
worker = WaitForLambdaWorker(lambda: image.copy(os.path.join(config.images_dir, self._appliance.image_dir_name()), disk["filename"]), allowed_exceptions=[OSError, ValueError])
progress_dialog = ProgressDialog(worker, "Add appliance", "Importing the appliance...", None, busy=True, parent=self)
if not progress_dialog.exec_():
return
self._refreshVersions()
image.upload(self._compute_id, callback=self._imageUploadedCallback)
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
"""
@@ -369,6 +419,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiQemuListComboBox.addItem("{path}".format(path=qemu["path"]), qemu["path"])
if self.uiQemuListComboBox.count() == 1:
self.next()
else:
i = self.uiQemuListComboBox.findText(self._appliance["qemu"]["arch"], QtCore.Qt.MatchContains)
if i != -1:
self.uiQemuListComboBox.setCurrentIndex(i)
def _install(self, version):
"""
@@ -386,14 +440,11 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if version is None:
appliance_configuration = self._appliance.copy()
else:
appliance_configuration = self._appliance.search_images_for_version(version)
if self._server.isLocal():
server_string = "local"
elif self._server.isGNS3VM():
server_string = "vm"
else:
server_string = self._server.url()
try:
appliance_configuration = self._appliance.search_images_for_version(version)
except ApplianceError as e:
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
return False
while len(appliance_configuration["name"]) == 0 or not config.is_name_available(appliance_configuration["name"]):
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "The name \"{}\" is already used by another appliance".format(appliance_configuration["name"]))
@@ -403,7 +454,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if "qemu" in appliance_configuration:
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, server_string), allowed_exceptions=[ConfigException, OSError])
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, self._compute_id), allowed_exceptions=[ConfigException, OSError])
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
progress_dialog.show()
if not progress_dialog.exec_():
@@ -416,6 +467,21 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
return True
def _uploadImages(self, version):
"""
Upload an image to the compute
"""
appliance_configuration = self._appliance.search_images_for_version(version)
for image in appliance_configuration["images"]:
if image["location"] == "local":
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
image.upload(self._compute_id, self._applianceImageUploadedCallback)
self._image_uploading_count += 1
def _applianceImageUploadedCallback(self, result, error=False, **kwargs):
self._image_uploading_count -= 1
def nextId(self):
if self.currentPage() == self.uiServerWizardPage:
if "docker" in self._appliance:
@@ -433,8 +499,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
"""
if self.currentPage() == self.uiFilesWizardPage:
if self._refreshing:
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
if current is None or sip.isdeleted(current):
return False
version = current.data(0, QtCore.Qt.UserRole)
if version is None:
return False
appliance = current.data(2, QtCore.Qt.UserRole)
if not self._appliance.is_version_installable(version["name"]):
QtWidgets.QMessageBox.warning(self, "Appliance", "Sorry, you cannot install {} with missing files".format(appliance["name"]))
@@ -443,8 +515,13 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return False
self._uploadImages(version["name"])
elif self.currentPage() == self.uiUsageWizardPage:
if self._image_uploading_count > 0:
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for image uploading")
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
if current:
version = current.data(0, QtCore.Qt.UserRole)
@@ -454,24 +531,21 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
elif self.currentPage() == self.uiServerWizardPage:
if self.uiRemoteRadioButton.isChecked():
if not Servers.instance().remoteServers():
if len(ComputeManager.instance().remoteComputes()) == 0:
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
return False
self._server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
gns3_vm_server = Servers.instance().vmServer()
if gns3_vm_server is None:
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "The GNS3 VM is not running")
return False
self._server = gns3_vm_server
self._compute_id = "vm"
else:
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")):
if "qemu" in self._appliance:
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and MacOSX is not supported by the GNS3 team. Are you sur to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return False
if ComputeManager.instance().localPlatform():
if (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
if "qemu" in self._appliance:
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and MacOSX is not supported by the GNS3 team. Are you sur to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return False
self._server = Servers.instance().localServer()
self._compute_id = "local"
elif self.currentPage() == self.uiQemuWizardPage:
if self.uiQemuListComboBox.currentIndex() == -1:
@@ -483,6 +557,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
return True
@qslot
def _vmToggledSlot(self, checked):
"""
Slot for when the VM radio button is toggled.
@@ -493,6 +568,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiRemoteServersGroupBox.setEnabled(False)
self.uiRemoteServersGroupBox.hide()
@qslot
def _remoteServerToggledSlot(self, checked):
"""
Slot for when the remote server radio button is toggled.
@@ -504,6 +580,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiRemoteServersGroupBox.setEnabled(True)
self.uiRemoteServersGroupBox.show()
@qslot
def _localToggledSlot(self, checked):
"""
Slot for when the local server radio button is toggled.

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3.qt import QtWidgets
from gns3.ui.capture_dialog_ui import Ui_CaptureDialog
import logging
log = logging.getLogger(__name__)
class CaptureDialog(QtWidgets.QDialog, Ui_CaptureDialog):
"""
This dialog allow configure the packet capture
"""
def __init__(self, parent, file_name, auto_start, ethernet_link=True):
super().__init__(parent)
self.setupUi(self)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self._okButtonClickedSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
if ethernet_link:
self.uiDataLinkTypeComboBox.addItem("Ethernet", "DLT_EN10MB")
else:
self.uiDataLinkTypeComboBox.addItem("Cisco HDLC", "DLT_C_HDLC")
self.uiDataLinkTypeComboBox.addItem("Cisco PPP", "DLT_PPP_SERIAL")
self.uiDataLinkTypeComboBox.addItem("Frame Relay", "DLT_FRELAY")
self.uiDataLinkTypeComboBox.addItem("ATM", "DLT_ATM_RFC1483")
self.uiCaptureFileNameLineEdit.setText(file_name)
self.uiStartCommandCheckBox.setChecked(auto_start)
def _okButtonClickedSlot(self):
if len(self.fileName()) == 0:
QtWidgets.QMessageBox.warning(self.parent(), "Packet capture", "Please provide a file name for the capture")
return
self.accept()
def fileName(self):
return self.uiCaptureFileNameLineEdit.text()
def dataLink(self):
"""
Type of link for capture
"""
return self.uiDataLinkTypeComboBox.currentData()
def commandAutoStart(self):
return self.uiStartCommandCheckBox.isChecked()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
main = QtWidgets.QMainWindow()
dialog = CaptureDialog(main, "test.pcap")
dialog.show()
exit_code = app.exec_()
print(dialog.dataLink())
print(dialog.fileName())

View File

@@ -49,6 +49,10 @@ class ConfigurationDialog(QtWidgets.QDialog, Ui_configurationDialog):
self._settings = settings
self._configuration_page = configuration_page
@property
def settings(self):
return self._settings
def on_uiButtonBox_clicked(self, button):
"""
Slot called when a button of the uiButtonBox is clicked.

View File

@@ -22,7 +22,6 @@ from gns3.qt import QtWidgets
from gns3.local_config import LocalConfig
from gns3.ui.console_command_dialog_ui import Ui_uiConsoleCommandDialog
from gns3.settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, \
PRECONFIGURED_SERIAL_CONSOLE_COMMANDS, \
PRECONFIGURED_VNC_CONSOLE_COMMANDS, \
CUSTOM_CONSOLE_COMMANDS_SETTINGS
@@ -63,9 +62,6 @@ 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])
else:
self._consoles = copy.copy(PRECONFIGURED_SERIAL_CONSOLE_COMMANDS)
self._consoles.update(self._settings[self._console_type])
self.uiCommandComboBox.clear()
self.uiCommandComboBox.addItem("Custom", "")

View File

@@ -24,7 +24,7 @@ import struct
from gns3.qt import QtWidgets
from gns3.ui.doctor_dialog_ui import Ui_DoctorDialog
from gns3.servers import Servers
from gns3.local_server import LocalServer
from gns3.local_config import LocalConfig
from gns3 import version
from gns3.modules.vmware import VMware
@@ -76,7 +76,7 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
def checkLocalServerEnabled(self):
"""Checking if the local server is enabled"""
if Servers.instance().shouldLocalServerAutoStart() is False:
if LocalServer.instance().shouldLocalServerAutoStart() is False:
return (2, "The local server is disabled. Go to Preferences -> Server -> Local Server and enable the local server.")
return (0, None)
@@ -130,7 +130,7 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
# we are root, so we should have privileged access.
return (0, None)
path = Servers.instance().localServerSettings().get("ubridge_path")
path = LocalServer.instance().localServerSettings().get("ubridge_path")
if path is None:
return (0, None)
if not os.path.exists(path):
@@ -138,18 +138,22 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
request_setuid = False
if sys.platform.startswith("linux"):
if "security.capability" in os.listxattr(path):
caps = os.getxattr(path, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
return(2, "Ubridge require CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
else:
# capabilities not supported
request_setuid = True
try:
if "security.capability" in os.listxattr(path):
caps = os.getxattr(path, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
return (2, "Ubridge requires CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
else:
# capabilities not supported
request_setuid = True
except AttributeError:
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
return (1, "Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)".format(path=path))
if sys.platform.startswith("darwin") or request_setuid:
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
return (2, "Ubridge should be setuid. Run sudo chown root {path} and sudo chmod 4755 {path}".format(path=path))
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
return (2, "Ubridge should be setuid. Run sudo chown root:admin {path} and sudo chmod 4750 {path}".format(path=path))
return (0, None)
def checkDynamipsPermission(self):
@@ -158,17 +162,21 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
# we are root, so we should have privileged access.
return (0, None)
path = Servers.instance().localServerSettings().get("dynamips_path")
path = LocalServer.instance().localServerSettings().get("dynamips_path")
if path is None:
return (0, None)
if not os.path.exists(path):
return (2, "Dynamips path {path} doesn't exists".format(path=path))
if sys.platform.startswith("linux") and "security.capability" in os.listxattr(path):
caps = os.getxattr(path, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
return (2, "Dynamips require CAP_NET_RAW. Run sudo setcap cap_net_raw,cap_net_admin+eip {path}".format(path=path))
try:
if sys.platform.startswith("linux") and "security.capability" in os.listxattr(path):
caps = os.getxattr(path, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
return (2, "Dynamips requires CAP_NET_RAW. Run sudo setcap cap_net_raw,cap_net_admin+eip {path}".format(path=path))
except AttributeError:
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
return (1, "Could not determine if CAP_NET_RAW capability is set for Dynamips (Python bug)".format(path=path))
return (0, None)
def checkGNS3InstalledTwice(self):
@@ -220,5 +228,5 @@ if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
main = QtWidgets.QMainWindow()
dialog = DoctorDialog(main, console=True)
#dialog.show()
# dialog.show()
#exit_code = app.exec_()

View File

@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from gns3.qt import QtWidgets
from gns3.compute import Compute
from gns3.ui.edit_compute_dialog_ui import Ui_EditComputeDialog
class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
"""
New compute dialog.
:param parent: parent widget.
"""
def __init__(self, parent, compute=None):
super().__init__(parent)
self.setupUi(self)
self.uiEnableAuthenticationCheckBox.toggled.connect(self._enableAuthenticationSlot)
self._compute = compute
if self._compute:
self.uiServerNameLineEdit.setText(self._compute.name())
self.uiServerHostLineEdit.setText(self._compute.host())
self.uiServerPortSpinBox.setValue(self._compute.port())
index = self.uiServerProtocolComboBox.findText(self._compute.protocol().upper())
self.uiServerProtocolComboBox.setCurrentIndex(index)
if self._compute.user():
self.uiEnableAuthenticationCheckBox.setChecked(True)
self.uiServerUserLineEdit.setText(self._compute.user())
else:
self.uiEnableAuthenticationCheckBox.setChecked(False)
self.uiWarningLabel.setVisible(False)
else:
self.uiEnableAuthenticationCheckBox.setChecked(False)
self.uiWarningLabel.setVisible(False)
self._enableAuthenticationSlot(self.uiEnableAuthenticationCheckBox.isChecked())
def _enableAuthenticationSlot(self, state):
"""
Slot to enable or not the authentication.
"""
if self.uiEnableAuthenticationCheckBox.isChecked():
self.uiServerUserLineEdit.setVisible(True)
self.uiServerPasswordLineEdit.setVisible(True)
self.uiServerUserLabel.setVisible(True)
self.uiServerPasswordLabel.setVisible(True)
else:
self.uiServerUserLineEdit.setVisible(False)
self.uiServerPasswordLineEdit.setVisible(False)
self.uiServerUserLabel.setVisible(False)
self.uiServerPasswordLabel.setVisible(False)
def compute(self):
return self._compute
def accept(self):
"""
Adds a new remote compute.
"""
host = self.uiServerHostLineEdit.text().strip()
name = self.uiServerNameLineEdit.text().strip()
protocol = self.uiServerProtocolComboBox.currentText().lower()
port = self.uiServerPortSpinBox.value()
user = self.uiServerUserLineEdit.text().strip()
password = self.uiServerPasswordLineEdit.text().strip()
if not re.match(r"^[a-zA-Z0-9\.{}-]+$".format("\u0370-\u1CDF\u2C00-\u30FF\u4E00-\u9FBF"), host):
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server hostname {}".format(host))
return
if name == "gns3vm":
QtWidgets.QMessageBox.critical(self, "Remote compute", "{} is a reserved name".format(name))
return
if len(name) == 0:
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server name {}".format(name))
return
if port is None or port < 1:
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server port {}".format(port))
return
if not self._compute:
self._compute = Compute()
self._compute.setName(name)
self._compute.setProtocol(protocol)
self._compute.setHost(host)
self._compute.setPort(port)
if self.uiEnableAuthenticationCheckBox.isChecked():
self._compute.setUser(user)
self._compute.setPassword(password)
else:
self._compute.setUser(None)
self._compute.setPassword(None)
super().accept()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
main = QtWidgets.QMainWindow()
dialog = EditComputeDialog(main)
dialog.show()
exit_code = app.exec_()

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from ..qt import QtWidgets
from ..topology import Topology
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog
class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
"""
Edit current project settings
"""
def __init__(self, parent):
super().__init__(parent)
self.setupUi(self)
self._project = Topology.instance().project()
self.uiProjectNameLineEdit.setText(self._project.name())
self.uiProjectAutoOpenCheckBox.setChecked(self._project.autoOpen())
self.uiProjectAutoCloseCheckBox.setChecked(not self._project.autoClose())
self.uiProjectAutoStartCheckBox.setChecked(self._project.autoStart())
self.uiSceneWidthSpinBox.setValue(self._project.sceneWidth())
self.uiSceneHeightSpinBox.setValue(self._project.sceneHeight())
def done(self, result):
"""
Called when the dialog is closed.
:param result: boolean (accepted or rejected)
"""
if result:
self._project.setName(self.uiProjectNameLineEdit.text())
self._project.setAutoOpen(self.uiProjectAutoOpenCheckBox.isChecked())
self._project.setAutoClose(not self.uiProjectAutoCloseCheckBox.isChecked())
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
self._project.update()
super().done(result)

View File

@@ -25,10 +25,12 @@ from gns3.version import __version__
from gns3.qt import QtWidgets, QtCore
from gns3.ui.export_debug_dialog_ui import Ui_ExportDebugDialog
from gns3.local_config import LocalConfig
from gns3.controller import Controller
import logging
log = logging.getLogger(__name__)
class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
"""
This dialog allow user to export useful information
@@ -43,29 +45,48 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
def _okButtonClickedSlot(self):
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export debug file", None, "Zip file (*.zip)", "Zip file (*.zip)")
if len(path) == 0:
if Controller.instance().isRemote():
QtWidgets.QMessageBox.critical(self, "Debug", "Export debug information from a remote server is not supported")
self.reject()
return
log.info("Export debug information to %s", path)
self._path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export debug file", None, "Zip file (*.zip)", "Zip file (*.zip)")
if len(self._path) == 0:
self.reject()
return
if Controller.instance().connected():
Controller.instance().post("/debug", self._exportDebugCallback)
else:
self._exportDebugCallback({}, error=True)
def _exportDebugCallback(self, result, error=False, **kwargs):
log.info("Export debug information to %s", self._path)
try:
with ZipFile(path, 'w') as zip:
with ZipFile(self._path, 'w') as zip:
zip.writestr("debug.txt", self._getDebugData())
dir = LocalConfig.configDirectory()
dir = LocalConfig.instance().configDirectory()
for filename in os.listdir(dir):
path = os.path.join(dir, filename)
if os.path.isfile(path):
zip.write(path, filename)
dir = self._project.filesDir()
if dir:
dir = os.path.join(LocalConfig.instance().configDirectory(), "debug")
if os.path.exists(dir):
for filename in os.listdir(dir):
path = os.path.join(dir, filename)
if os.path.isfile(path):
zip.write(path, filename)
if self._project:
dir = self._project.filesDir()
if dir:
for filename in os.listdir(dir):
path = os.path.join(dir, filename)
if os.path.isfile(path):
zip.write(path, filename)
except OSError as e:
QtWidgets.QMessageBox.critical(self, "Debug", "Can't export debug information: {}".format(str(e)))
self.accept()

View File

@@ -32,7 +32,7 @@ class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
check return a tuple result and a message in case of failure.
"""
def __init__(self, node, path, parent=None):
def __init__(self, target, path, parent=None, default=""):
if parent is None:
from gns3.main_window import MainWindow
@@ -41,23 +41,32 @@ class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
super().__init__(parent)
self.setupUi(self)
self._node = node
self._target = target
self._path = path
self._default = default
self.setWindowTitle(node.name() + " " + os.path.basename(path))
self.setWindowTitle(target.name() + " " + os.path.basename(path))
self.uiRefreshButton.pressed.connect(self._refreshSlot)
self.accepted.connect(self._acceptedCallback)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Save).clicked.connect(self._okButtonClickedSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
self._refreshSlot()
def _acceptedCallback(self):
def _okButtonClickedSlot(self):
text = self.uiFileTextEdit.toPlainText()
self._node.httpPost("/files" + self._path, None, body=text)
self._target.post("/files/" + self._path, self._saveCallback, body=text)
def _saveCallback(self, result, error=False, **kwargs):
if not error:
self.accept()
def _refreshSlot(self):
self._node.httpGet("/files" + self._path, self._getCallback)
self._target.get("/files/" + self._path, self._getCallback)
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
if not error:
self.uiFileTextEdit.setText(raw_body)
self.uiFileTextEdit.setText(raw_body.decode("utf-8"))
elif result.get("status") == 404:
if self._default:
self.uiFileTextEdit.setText(self._default)

View File

@@ -61,6 +61,14 @@ class NewApplianceDialog(QtWidgets.QDialog, Ui_NewApplianceDialog):
self._setPreferencesPane(dialog, "VMware").uiNewVMwareVMPushButton.clicked.emit(False)
elif self.uiAddDockerVMRadioButton.isChecked():
self._setPreferencesPane(dialog, "Docker").uiNewDockerVMPushButton.clicked.emit(False)
elif self.uiAddVPCSRadioButton.isChecked():
self._setPreferencesPane(dialog, "VPCS").uiNewVPCSPushButton.clicked.emit(False)
elif self.uiAddCloudRadioButton.isChecked():
self._setPreferencesPane(dialog, "Cloud nodes").uiNewCloudNodePushButton.clicked.emit(False)
elif self.uiAddEthernetHubRadioButton.isChecked():
self._setPreferencesPane(dialog, "Ethernet hubs").uiNewEthernetHubPushButton.clicked.emit(False)
elif self.uiAddEthernetSwitchRadioButton.isChecked():
self._setPreferencesPane(dialog, "Ethernet switches").uiNewEthernetSwitchPushButton.clicked.emit(False)
else:
return
dialog.exec_()
@@ -83,15 +91,31 @@ class NewApplianceDialog(QtWidgets.QDialog, Ui_NewApplianceDialog):
:returns: current QWidget
"""
pane = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFixedString)[0]
child_pane = pane.child(0)
dialog.uiTreeWidget.setCurrentItem(child_pane)
panes = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFixedString)
if len(panes) > 0:
child_pane = panes[0].child(0)
dialog.uiTreeWidget.setCurrentItem(child_pane)
else:
i = 0
root = dialog.uiTreeWidget.invisibleRootItem()
while i < root.childCount():
root_item = root.child(i)
x = 0
while x < root_item.childCount():
item = root_item.child(x)
x += 1
if item.text(0) == name:
dialog.uiTreeWidget.setCurrentItem(item)
i += 1
dialog.addModifiedPage(dialog.uiStackedWidget.currentWidget())
return dialog.uiStackedWidget.currentWidget()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
main = QtWidgets.QMainWindow()
dialog = NewApplianceDialog(main, console=True)
dialog = NewApplianceDialog(main)
dialog._setPreferencesPane(PreferencesDialog(main), "Ethernet hubs").uiNewEthernetHubPushButton.clicked.emit(False)
dialog.show()
exit_code = app.exec_()

View File

@@ -1,140 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from ..qt import QtCore, QtGui, QtWidgets
from ..ui.new_project_dialog_ui import Ui_NewProjectDialog
class NewProjectDialog(QtWidgets.QDialog, Ui_NewProjectDialog):
"""
New project dialog.
:param parent: parent widget.
:param showed_from_startup: boolean to indicate if this dialog
:param default_project_name: Project name by default
has been opened automatically when GNS3 started.
"""
def __init__(self, parent, showed_from_startup=False, default_project_name="untitled"):
super().__init__(parent)
self.setupUi(self)
self._main_window = parent
self._project_settings = {}
self.uiNameLineEdit.setText(default_project_name)
self.uiLocationLineEdit.setText(os.path.join(self._main_window.projectsDirPath(), default_project_name))
self.uiNameLineEdit.textEdited.connect(self._projectNameSlot)
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
if not showed_from_startup:
self.uiOpenProjectPushButton.hide()
self.uiRecentProjectsPushButton.hide()
def keyPressEvent(self, e):
"""
Event handler in order to properly handle escape.
"""
if e.key() == QtCore.Qt.Key_Escape:
self.close()
def _projectNameSlot(self, text):
project_dir = self._main_window.projectsDirPath()
if os.path.dirname(self.uiLocationLineEdit.text()) == project_dir:
self.uiLocationLineEdit.setText(os.path.join(project_dir, text))
def _projectPathSlot(self):
"""
Slot to select the a new project location.
"""
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Project location", os.path.join(self._main_window.projectsDirPath(),
self.uiNameLineEdit.text()))
if path:
self.uiLocationLineEdit.setText(path)
def getNewProjectSettings(self):
return self._project_settings
def _menuTriggeredSlot(self, action):
"""
Closes this dialog when a recent project
has been opened.
:param action: ignored.
"""
self.reject()
def _openProjectActionSlot(self):
"""
Opens a project and closes this dialog.
"""
self._main_window.openProjectActionSlot()
self.reject()
def _showRecentProjectsSlot(self):
"""
lot to show all the recent projects in a menu.
"""
menu = QtWidgets.QMenu()
menu.triggered.connect(self._menuTriggeredSlot)
for action in self._main_window._recent_file_actions:
menu.addAction(action)
menu.exec_(QtGui.QCursor.pos())
def done(self, result):
if result:
project_name = self.uiNameLineEdit.text()
project_location = self.uiLocationLineEdit.text()
project_type = "local"
if not project_name:
QtWidgets.QMessageBox.critical(self, "New project", "Project name is empty")
return
if not project_location:
QtWidgets.QMessageBox.critical(self, "New project", "Project location is empty")
return
if os.path.isdir(project_location):
reply = QtWidgets.QMessageBox.question(self,
"New project",
"Location {} already exists, overwrite it?".format(project_location),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return
self._project_settings["project_name"] = project_name
self._project_settings["project_path"] = os.path.join(project_location, project_name + ".gns3")
self._project_settings["project_files_dir"] = project_location
self._project_settings["project_type"] = project_type
super().done(result)

View File

@@ -1,93 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from gns3.qt import QtWidgets
from gns3.ui.new_server_dialog_ui import Ui_NewServerDialog
from gns3.servers import Servers
class NewServerDialog(QtWidgets.QDialog, Ui_NewServerDialog):
"""
New server dialog.
:param parent: parent widget.
has been opened automatically when GNS3 started.
"""
def __init__(self, parent):
super().__init__(parent)
self.setupUi(self)
self.uiEnableAuthenticationCheckBox.stateChanged.connect(self._enableAuthenticationSlot)
def _enableAuthenticationSlot(self, state):
"""
Slot to enable or not the authentication.
"""
if state:
self.uiServerUserLineEdit.setEnabled(True)
self.uiServerPasswordLineEdit.setEnabled(True)
else:
self.uiServerUserLineEdit.setEnabled(False)
self.uiServerPasswordLineEdit.setEnabled(False)
def accept(self):
"""
Adds a new remote server.
"""
protocol = self.uiServerProtocolComboBox.currentText().lower()
host = self.uiServerHostLineEdit.text().strip()
port = self.uiServerPortSpinBox.value()
if self.uiEnableAuthenticationCheckBox.isChecked():
user = self.uiServerUserLineEdit.text().strip()
password = self.uiServerPasswordLineEdit.text().strip()
else:
user = password = ""
if not re.match(r"^[a-zA-Z0-9\.{}-]+$".format("\u0370-\u1CDF\u2C00-\u30FF\u4E00-\u9FBF"), host):
QtWidgets.QMessageBox.critical(self, "Remote server", "Invalid remote server hostname {}".format(host))
return
if port is None or port < 1:
QtWidgets.QMessageBox.critical(self, "Remote server", "Invalid remote server port {}".format(port))
return
servers = Servers.instance()
remote_servers = servers.remoteServers()
# check if the remote server is already defined
for server in remote_servers.values():
if server.protocol() == protocol and server.host() == host and server.port() == port and server.user() == user:
QtWidgets.QMessageBox.critical(self, "Remote server", "Remote server is already defined.")
return
servers.getRemoteServer(protocol, host, port, user, settings={"password": password})
servers.save()
super().accept()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
main = QtWidgets.QMainWindow()
dialog = NewServerDialog(main)
dialog.show()
exit_code = app.exec_()

View File

@@ -93,6 +93,11 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
self.uiNodesTreeWidget.setCurrentItem(item)
self.showConfigurationPageSlot(item, 0)
self.splitter.setSizes([0, 600])
elif len(self._parent_items) > 0:
# We have multiple node we select the first group
item = next(iter(self._parent_items.values()))
self.uiNodesTreeWidget.setCurrentItem(item)
self.showConfigurationPageSlot(item, 0)
def showConfigurationPageSlot(self, item, column):
"""
@@ -132,9 +137,17 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
if page != self.uiEmptyPageWidget:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(True)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(True)
else:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(False)
# hide the contextual help button if there is no help text
if page.whatsThis():
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).show()
else:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).hide()
def on_uiButtonBox_clicked(self, button):
"""
@@ -148,6 +161,8 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
self.applySettings()
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset):
self.resetSettings()
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help):
self.showHelp()
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
QtWidgets.QDialog.reject(self)
else:
@@ -173,12 +188,10 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
# all children for that group
self.previousItem = None
self.previousNode = None
settings = item.child(0).settings().copy()
node = item.child(0).node()
page.saveSettings(settings, node, group=True)
settings = page.saveSettings({}, node, group=True)
for index in range(0, item.childCount()):
child = item.child(index)
# child.node().update(settings) #TODO: delete
child.settings().update(settings)
# update the nodes with the settings
@@ -212,6 +225,14 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
child = item.child(index)
child.setSettings(child.node().settings().copy())
def showHelp(self):
"""
Show contextual help for the current page.
"""
page = self.uiConfigStackedWidget.currentWidget()
if page != self.uiEmptyPageWidget and page.whatsThis():
QtWidgets.QMessageBox.information(self, "{} help".format(page.windowTitle()), page.whatsThis().strip())
class ConfigurationPageItem(QtWidgets.QTreeWidgetItem):

View File

@@ -24,8 +24,12 @@ from ..ui.preferences_dialog_ui import Ui_PreferencesDialog
from ..pages.server_preferences_page import ServerPreferencesPage
from ..pages.general_preferences_page import GeneralPreferencesPage
from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
from ..pages.gns3_vm_preferences_page import GNS3VMPreferencesPage
from ..modules import MODULES
import logging
log = logging.getLogger(__name__)
class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
@@ -40,6 +44,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
super().__init__(parent)
self.setupUi(self)
self._modified_pages = set()
# We adapt the max size to the screen resolution
# We need to manually do that otherwise on small screen the windows
@@ -69,7 +74,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
self.uiTreeWidget.setMaximumWidth(self.uiTreeWidget.sizeHintForColumn(0) + 10)
# Something has change?
self._modified = False
self._modified_pages = set()
def _loadPreferencePages(self):
"""
@@ -80,6 +85,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
pages = [
GeneralPreferencesPage,
ServerPreferencesPage,
GNS3VMPreferencesPage,
PacketCapturePreferencesPage,
]
@@ -100,6 +106,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
parent = self.uiTreeWidget
for cls in preference_pages:
preferences_page = cls()
preferences_page.setParent(self)
preferences_page.loadPreferences()
name = preferences_page.windowTitle()
item = QtWidgets.QTreeWidgetItem(parent)
@@ -122,7 +129,8 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
# Class name, changed signal
widget_to_watch = {
QtWidgets.QLineEdit: "textChanged",
QtWidgets.QTreeWidget: "itemChanged",
QtWidgets.QPlainTextEdit: "textChanged",
# QtWidgets.QTreeWidget: "itemChanged",
QtWidgets.QComboBox: "currentIndexChanged",
QtWidgets.QSpinBox: "valueChanged",
QtWidgets.QAbstractButton: "pressed"
@@ -134,10 +142,27 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
def _preferenceChangeSlot(self, *args):
"""
Called when somthing change in the preference dialog
Called when something change in the preference dialog
"""
self._applyButton.setEnabled(True)
self._modified = True
# Found the page with the change
widget = sender = self.sender()
while widget.parent() != self.uiStackedWidget:
widget = widget.parent()
if self.addModifiedPage(widget):
log.debug("%s value has changed", sender.objectName())
def addModifiedPage(self, widget):
"""
:returns: True is the page is initialized and element added
"""
# The widget can trigger signal before the end of init due to async api call
if not hasattr(widget, 'pageInitialized') or widget.pageInitialized():
self._applyButton.setEnabled(True)
self._modified_pages.add(widget)
return True
return False
def _showPreferencesPageSlot(self, current, previous):
"""
@@ -159,7 +184,7 @@ 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.setMinimumSize(widget.size())
self.uiStackedWidget.resize(widget.size())
self.uiStackedWidget.setCurrentIndex(index)
@@ -169,15 +194,14 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
"""
success = True
for item in self._items:
preferences_page = item.data(0, QtCore.Qt.UserRole)
for preferences_page in list(self._modified_pages):
ok = preferences_page.savePreferences()
# if page.savePreferences() returns None, assume success
if ok is not None and not ok:
success = False
if success:
self._applyButton.setEnabled(False)
self._modified = False
self._modified_pages = set()
return success
def reject(self):
@@ -185,10 +209,12 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
Closes this dialog.
"""
if self._modified:
if len(self._modified_pages) > 0:
# Get the title of pages with modifications
pages_title = ', '.join([page.windowTitle() for page in self._modified_pages])
reply = QtWidgets.QMessageBox.warning(self,
"Preferences",
"You have unsaved preferences.\n\nContinue without saving?",
"You have unsaved preferences in {}.\n\nContinue without saving?".format(pages_title),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:

View File

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import shutil
from gns3.qt import QtWidgets
from gns3.local_config import LocalConfig
from gns3.ui.profile_select_dialog_ui import Ui_ProfileSelectDialog
import logging
log = logging.getLogger(__name__)
class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
"""
This dialog allow user to choose a profile of settings
"""
def __init__(self, parent=None):
if parent is None:
self._main = QtWidgets.QMainWindow()
self._main.hide()
parent = self._main
super().__init__(parent)
self.setupUi(self)
self.uiNewPushButton.clicked.connect(self._newPushButtonSlot)
self.uiDeletePushButton.clicked.connect(self._deletePushButtonSlot)
# Center on screen
screen = QtWidgets.QApplication.desktop().screenGeometry()
self.move(screen.center() - self.rect().center())
if sys.platform.startswith("win"):
appdata = os.path.expandvars("%APPDATA%")
path = os.path.join(appdata, "GNS3")
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3")
self.profiles_path = os.path.join(path, "profiles")
self.uiShowAtStartupCheckBox.setChecked(LocalConfig.instance().multiProfiles())
self._refresh()
def _refresh(self):
self.uiProfileSelectComboBox.clear()
self.uiProfileSelectComboBox.addItem("default")
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)
except OSError:
pass
def profile(self):
return self.uiProfileSelectComboBox.currentText()
def accept(self):
LocalConfig.instance().setMultiProfiles(self.uiShowAtStartupCheckBox.isChecked())
super().accept()
def _newPushButtonSlot(self):
profile, ok = QtWidgets.QInputDialog.getText(self.parent(), "New profile", "Profile name:")
if ok:
self.uiProfileSelectComboBox.addItem(profile)
self.uiProfileSelectComboBox.setCurrentText(profile)
self.accept()
def _deletePushButtonSlot(self):
profile = self.uiProfileSelectComboBox.currentText()
if profile == "default":
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", "You can't delete the default profile")
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))
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
dialog = ProfileSelectDialog()
dialog.show()
exit_code = app.exec_()

View File

@@ -0,0 +1,308 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from ..qt import QtCore, QtGui, QtWidgets, qslot
from ..ui.project_dialog_ui import Ui_ProjectDialog
from ..controller import Controller
from ..topology import Topology
import logging
log = logging.getLogger(__name__)
class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
"""
New project dialog.
"""
def __init__(self, parent, default_project_name="untitled", show_open_options=True):
"""
:param parent: parent widget.
:param default_project_name: Project name by default
:param show_open_options: If true allow to open a project from the dialog
otherwise it's just for create a project
"""
super().__init__(parent)
self.setupUi(self)
self._main_window = parent
self._project_settings = {}
self.uiNameLineEdit.setText(default_project_name)
self.uiLocationLineEdit.setText(os.path.join(Topology.instance().projectsDirPath(), default_project_name))
self.uiNameLineEdit.textEdited.connect(self._projectNameSlot)
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
self.uiSettingsPushButton.clicked.connect(self._settingsClickedSlot)
if show_open_options:
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
else:
self.uiOpenProjectGroupBox.hide()
self.uiProjectTabWidget.removeTab(1)
# If the controller is remote we hide option for local file system
if Controller.instance().isRemote():
self.uiLocationLabel.setVisible(False)
self.uiLocationLineEdit.setVisible(False)
self.uiLocationBrowserToolButton.setVisible(False)
self.uiOpenProjectPushButton.setVisible(False)
self.uiProjectsTreeWidget.itemDoubleClicked.connect(self._projectsTreeWidgetDoubleClickedSlot)
self.uiDeleteProjectButton.clicked.connect(self._deleteProjectSlot)
self.uiDuplicateProjectPushButton.clicked.connect(self._duplicateProjectSlot)
self.uiRefreshProjectsPushButton.clicked.connect(Controller.instance().refreshProjectList)
Controller.instance().project_list_updated_signal.connect(self._updateProjectListSlot)
self._updateProjectListSlot()
Controller.instance().refreshProjectList()
def _settingsClickedSlot(self):
"""
When the user click on the settings button
"""
self.reject()
self._main_window.preferencesActionSlot()
def _projectsTreeWidgetDoubleClickedSlot(self, item, column):
self.done(True)
@qslot
def _deleteProjectSlot(self, *args):
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
QtWidgets.QMessageBox.critical(self, "Delete project", "No project selected")
return
projects_to_delete = set()
for project in self.uiProjectsTreeWidget.selectedItems():
project_id = project.data(0, QtCore.Qt.UserRole)
project_name = project.data(1, QtCore.Qt.UserRole)
reply = QtWidgets.QMessageBox.warning(self,
"Delete project",
'Delete project "{}"?\nThis cannot be reverted.'.format(project_name),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
projects_to_delete.add(project_id)
for project_id in projects_to_delete:
Controller.instance().deleteProject(project_id)
def _duplicateProjectSlot(self):
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
QtWidgets.QMessageBox.critical(self, "Duplicate project", "No project selected")
return
if len(self.uiProjectsTreeWidget.selectedItems()) > 1:
QtWidgets.QMessageBox.critical(self, "Duplicate project", "Please select only one project to duplicate")
return
for project in self.uiProjectsTreeWidget.selectedItems():
project_id = project.data(0, QtCore.Qt.UserRole)
project_name = project.data(1, QtCore.Qt.UserRole)
new_project_name = project_name + "-1"
existing_project_name = [p["name"] for p in Controller.instance().projects()]
i = 1
while new_project_name in existing_project_name:
new_project_name = "{}-{}".format(project_name, i)
i += 1
name, reply = QtWidgets.QInputDialog.getText(self,
"Duplicate project",
'Duplicate project "{}"?.'.format(project_name),
QtWidgets.QLineEdit.Normal,
new_project_name)
name = name.strip()
if reply and len(name) > 0:
if Controller.instance().isRemote():
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
self._duplicateCallback,
body={"name": name})
else:
project_location = os.path.join(Topology.instance().projectsDirPath(), name)
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
self._duplicateCallback,
body={"name": name, "path": project_location})
def _duplicateCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while duplicate project: {}".format(result["message"]))
return
Controller.instance().refreshProjectList()
@qslot
def _updateProjectListSlot(self, *args):
self.uiProjectsTreeWidget.clear()
self.uiDeleteProjectButton.setEnabled(False)
self.uiProjectsTreeWidget.setUpdatesEnabled(False)
items = []
for project in Controller.instance().projects():
path = os.path.join(project["path"], project["filename"])
item = QtWidgets.QTreeWidgetItem([project["name"], project["status"], path])
item.setData(0, QtCore.Qt.UserRole, project["project_id"])
item.setData(1, QtCore.Qt.UserRole, project["name"])
item.setData(2, QtCore.Qt.UserRole, path)
items.append(item)
self.uiProjectsTreeWidget.addTopLevelItems(items)
if len(Controller.instance().projects()):
self.uiDeleteProjectButton.setEnabled(True)
self.uiProjectsTreeWidget.header().setResizeContentsPrecision(100) # How many row is checked for the resize for performance reason
self.uiProjectsTreeWidget.resizeColumnToContents(0)
self.uiProjectsTreeWidget.resizeColumnToContents(1)
self.uiProjectsTreeWidget.resizeColumnToContents(2)
self.uiProjectsTreeWidget.sortItems(0, QtCore.Qt.AscendingOrder)
self.uiProjectsTreeWidget.setUpdatesEnabled(True)
def keyPressEvent(self, e):
"""
Event handler in order to properly handle escape.
"""
if e.key() == QtCore.Qt.Key_Escape:
self.close()
def _projectNameSlot(self, text):
project_dir = Topology.instance().projectsDirPath()
if os.path.dirname(self.uiLocationLineEdit.text()) == project_dir:
self.uiLocationLineEdit.setText(os.path.join(project_dir, text))
def _projectPathSlot(self):
"""
Slot to select the a new project location.
"""
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Project location", os.path.join(Topology.instance().projectsDirPath(),
self.uiNameLineEdit.text()))
if path:
self.uiNameLineEdit.setText(os.path.basename(path))
self.uiLocationLineEdit.setText(path)
def getProjectSettings(self):
return self._project_settings
def _menuTriggeredSlot(self, action):
"""
Closes this dialog when a recent project
has been opened.
:param action: ignored.
"""
self.reject()
def _openProjectActionSlot(self):
"""
Opens a project and closes this dialog.
"""
self._main_window.openProjectActionSlot()
self.reject()
def _showRecentProjectsSlot(self):
"""
lot to show all the recent projects in a menu.
"""
menu = QtWidgets.QMenu()
menu.triggered.connect(self._menuTriggeredSlot)
if Controller.instance().isRemote():
for action in self._main_window.recent_project_actions:
menu.addAction(action)
else:
for action in self._main_window.recent_file_actions:
menu.addAction(action)
menu.exec_(QtGui.QCursor.pos())
def _overwriteProjectCallback(self, result, error=False, **kwargs):
if error:
# A 404 could arrive if someone else as deleted the project
if "status" not in result or result["status"] != 404:
return
elif "message" in result:
QtWidgets.QMessageBox.critical(self,
"New Project",
"Error while overwrite project: {}".format(result["message"]))
Controller.instance().refreshProjectList()
self.done(True)
def _newProject(self):
self._project_settings["project_name"] = self.uiNameLineEdit.text().strip()
if Controller.instance().isRemote():
self._project_settings.pop("project_path", None)
self._project_settings.pop("project_files_dir", None)
else:
project_location = self.uiLocationLineEdit.text().strip()
if not project_location:
QtWidgets.QMessageBox.critical(self, "New project", "Project location is empty")
return False
self._project_settings["project_path"] = os.path.join(project_location, self._project_settings["project_name"] + ".gns3")
self._project_settings["project_files_dir"] = project_location
if len(self._project_settings["project_name"]) == 0:
QtWidgets.QMessageBox.critical(self, "New project", "Project name is empty")
return False
for existing_project in Controller.instance().projects():
if self._project_settings["project_name"] == existing_project["name"] \
or ("project_files_dir" in self._project_settings and self._project_settings["project_files_dir"] == existing_project["path"]):
if existing_project["status"] == "opened":
QtWidgets.QMessageBox.critical(self,
"New project",
"Project {} is open you can not overwrite it".format(self._project_settings["project_name"]))
return False
reply = QtWidgets.QMessageBox.warning(self,
"New project",
"Project {} already exists, overwrite it?".format(existing_project["name"]),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
Controller.instance().deleteProject(existing_project["project_id"], self._overwriteProjectCallback)
# In all cases we cancel the new project and if project success to delete
# we will call done again
return False
return True
def done(self, result):
if result:
if self.uiProjectTabWidget.currentIndex() == 0:
if not self._newProject():
return
else:
current = self.uiProjectsTreeWidget.currentItem()
if current is None:
QtWidgets.QMessageBox.critical(self, "Open project", "No project selected")
return
self._project_settings["project_id"] = current.data(0, QtCore.Qt.UserRole)
self._project_settings["project_name"] = current.data(1, QtCore.Qt.UserRole)
super().done(result)

View File

@@ -17,19 +17,23 @@
import sys
import os
import psutil
import shutil
from gns3.qt import QtCore, QtWidgets, QtGui
from gns3.servers import Servers
from ..gns3_vm import GNS3VM
from ..dialogs.preferences_dialog import PreferencesDialog
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork, qslot
from gns3.controller import Controller
from gns3.local_server import LocalServer
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
from ..settings import DEFAULT_LOCAL_SERVER_HOST
from ..ui.setup_wizard_ui import Ui_SetupWizard
from ..utils.progress_dialog import ProgressDialog
from ..utils.wait_for_vm_worker import WaitForVMWorker
from ..utils.wait_for_connection_worker import WaitForConnectionWorker
from ..version import __version__
import logging
log = logging.getLogger(__name__)
class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
@@ -41,12 +45,23 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
super().__init__(parent)
self.setupUi(self)
self._gns3_vm_settings = {
"enable": True,
"headless": False,
"when_exit": "stop",
"engine": "vmware",
"vcpus": 1,
"ram": 2048,
"vmname": "GNS3 VM"
}
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._server = Servers.instance().localServer()
self.uiLocalServerToolButton.clicked.connect(self._localServerBrowserSlot)
self.uiGNS3VMDownloadLinkUrlLabel.setText('')
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
@@ -61,11 +76,44 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
self.uiVmwareRadioButton.setChecked(False)
self.uiVirtualBoxRadioButton.setChecked(False)
# Mandatory fields
self.uiLocalServerWizardPage.registerField("path*", self.uiLocalServerPathLineEdit)
# load all available addresses
for address in QtNetwork.QNetworkInterface.allAddresses():
address_string = address.toString()
if address.protocol() != QtNetwork.QAbstractSocket.IPv6Protocol:
self.uiLocalServerHostComboBox.addItem(address_string, address.toString())
if sys.platform.startswith("darwin"):
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.jpg"))
else:
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.jpg"))
if sys.platform.startswith("linux"):
self.uiVMRadioButton.setText("Run the topologies in an isolated and standard VM")
self.uiLocalRadioButton.setText("Run the topologies on my computer")
self.uiLocalRadioButton.setChecked(True)
self.uiLocalLabel.setVisible(False)
Controller.instance().connected_signal.connect(self._refreshLocalServerStatusSlot)
Controller.instance().connection_failed_signal.connect(self._refreshLocalServerStatusSlot)
def _localServerBrowserSlot(self):
"""
Slot to open a file browser and select a local server.
"""
filter = ""
if sys.platform.startswith("win"):
filter = "Executable (*.exe);;All files (*.*)"
server_path = shutil.which("gns3server")
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select the local server", server_path, filter)
if not path:
return
self.uiLocalServerPathLineEdit.setText(path)
def _VMwareBannerButtonClickedSlot(self):
if sys.platform.startswith("darwin"):
url = "http://send.onenetworkdirect.net/z/616461/CD225091/"
@@ -84,7 +132,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
from gns3.modules import VMware
settings = VMware.instance().settings()
if not os.path.exists(settings["vmrun_path"]):
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://www.vmware.com/support/developer/vix-api/")
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://www.vmware.com/support/developer/vix-api/. After installation you need to restart GNS3.")
return
self._refreshVMListSlot()
@@ -100,7 +148,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
from gns3.modules import VirtualBox
settings = VirtualBox.instance().settings()
if not os.path.exists(settings["vboxmanage_path"]):
QtWidgets.QMessageBox.critical(self, "VirtualBox", "VBoxManage could not be found, VirtualBox is probably not installed")
QtWidgets.QMessageBox.critical(self, "VirtualBox", "VBoxManage could not be found, VirtualBox is probably not installed. After installation you need to restart GNS3.")
return
self._refreshVMListSlot()
@@ -119,6 +167,13 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
dialog.uiTreeWidget.setCurrentItem(child_pane)
return dialog.uiStackedWidget.currentWidget()
def _getSettingsCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
log.error("Error while get gettings: {}".format(result["message"]))
return
self._gns3_vm_settings = result
def initializePage(self, page_id):
"""
Initialize Wizard pages.
@@ -127,107 +182,172 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
super().initializePage(page_id)
if self.page(page_id) == self.uiVMWizardPage:
# limit the number of vCPUs to the number of physical cores (hyper thread CPUs are excluded)
# because this is likely to degrade performances.
cpu_count = psutil.cpu_count(logical=False)
self.uiCPUSpinBox.setValue(cpu_count)
# we want to allocate half of the available physical memory
ram = int(psutil.virtual_memory().total / (1024 * 1024) / 2)
# value must be a multiple of 4 (VMware requirement)
ram -= ram % 4
self.uiRAMSpinBox.setValue(ram)
if self.page(page_id) == self.uiServerWizardPage:
Controller.instance().setDisplayError(False)
Controller.instance().get("/gns3vm", self._getSettingsCallback)
elif self.page(page_id) == self.uiVMWizardPage:
if self._GNS3VMSettings()["engine"] == "vmware":
self.uiVmwareRadioButton.setChecked(True)
self._listVMwareVMsSlot()
elif self._GNS3VMSettings()["engine"] == "virtualbox":
self.uiVirtualBoxRadioButton.setChecked(True)
self._listVirtualBoxVMsSlot()
self.uiCPUSpinBox.setValue(self._GNS3VMSettings()["vcpus"])
self.uiRAMSpinBox.setValue(self._GNS3VMSettings()["ram"])
elif self.page(page_id) == self.uiLocalServerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
self.uiLocalServerPathLineEdit.setText(local_server_settings["path"])
index = self.uiLocalServerHostComboBox.findData(local_server_settings["host"])
if index != -1:
self.uiLocalServerHostComboBox.setCurrentIndex(index)
self.uiLocalServerPortSpinBox.setValue(local_server_settings["port"])
elif self.page(page_id) == self.uiRemoteControllerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
if local_server_settings["host"] is None:
self.uiRemoteMainServerHostLineEdit.setText(DEFAULT_LOCAL_SERVER_HOST)
self.uiRemoteMainServerAuthCheckBox.setChecked(False)
self.uiRemoteMainServerUserLineEdit.setText("")
self.uiRemoteMainServerPasswordLineEdit.setText("")
else:
self.uiRemoteMainServerHostLineEdit.setText(local_server_settings["host"])
self.uiRemoteMainServerAuthCheckBox.setChecked(local_server_settings["auth"])
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
self.uiRemoteMainServerPasswordLineEdit.setText(local_server_settings["password"])
self.uiRemoteMainServerPortSpinBox.setValue(local_server_settings["port"])
self.uiRemoteMainServerProtocolComboBox.setCurrentText(local_server_settings["protocol"])
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
self._refreshLocalServerStatusSlot()
elif self.page(page_id) == self.uiSummaryWizardPage:
self.uiSummaryTreeWidget.clear()
if self.uiLocalRadioButton.isChecked():
local_server_settings = LocalServer.instance().localServerSettings()
self._addSummaryEntry("Server type:", "Local")
self._addSummaryEntry("Path:", local_server_settings["path"])
self._addSummaryEntry("Host:", local_server_settings["host"])
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
elif self.uiRemoteControllerRadioButton.isChecked():
local_server_settings = LocalServer.instance().localServerSettings()
self._addSummaryEntry("Server type:", "Remote")
self._addSummaryEntry("Host:", local_server_settings["host"])
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
self._addSummaryEntry("User:", local_server_settings["user"])
else:
self._addSummaryEntry("Server type:", "GNS3 Virtual Machine")
self._addSummaryEntry("VM engine:", self._GNS3VMSettings()["engine"].capitalize())
self._addSummaryEntry("VM name:", self._GNS3VMSettings()["vmname"])
self._addSummaryEntry("VM vCPUs:", str(self._GNS3VMSettings()["vcpus"]))
self._addSummaryEntry("VM RAM:", str(self._GNS3VMSettings()["ram"]) + " MB")
@qslot
def _refreshLocalServerStatusSlot(self):
"""
Refresh the local server status page
"""
if Controller.instance().connected():
self.uiLocalServerStatusLabel.setText("Connection to local server successful")
Controller.instance().get("/gns3vm", self._getSettingsCallback)
elif Controller.instance().connecting():
self.uiLocalServerStatusLabel.setText("Please wait connection to the GNS3 server")
else:
local_server_settings = LocalServer.instance().localServerSettings()
self.uiLocalServerStatusLabel.setText("Connection to local server failed.\n* Make sure GNS3 is allowed in your firewall.\n* Go back and try to change the server port\n* Please check with a browser if you can connect to {protocol}://{host}:{port}.\n* Try to run {path} in a terminal to see if you have an error if the above does not work.".format(protocol=local_server_settings["protocol"], host=local_server_settings["host"], port=local_server_settings["port"], path=local_server_settings["path"]))
def _GNS3VMSettings(self):
return self._gns3_vm_settings
def _setGNS3VMSettings(self, settings):
Controller.instance().put("/gns3vm", self._saveSettingsCallback, settings, timeout=60 * 5)
def _saveSettingsCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while save settings: {}".format(result["message"]))
return
def _addSummaryEntry(self, name, value):
item = QtWidgets.QTreeWidgetItem(self.uiSummaryTreeWidget, [name, value])
item.setText(0, name)
font = item.font(0)
font.setBold(True)
item.setFont(0, font)
def validateCurrentPage(self):
"""
Validates the settings.
"""
gns3_vm = GNS3VM.instance()
servers = Servers.instance()
Controller.instance().setDisplayError(True)
if self.currentPage() == self.uiVMWizardPage:
vmname = self.uiVMListComboBox.currentText()
if vmname:
# save the GNS3 VM settings
vm_settings = {"auto_start": True,
"vmname": vmname,
"vmx_path": self.uiVMListComboBox.currentData()}
vm_settings = self._GNS3VMSettings()
vm_settings["enable"] = True
vm_settings["vmname"] = vmname
if self.uiVmwareRadioButton.isChecked():
vm_settings["virtualization"] = "VMware"
vm_settings["engine"] = "vmware"
elif self.uiVirtualBoxRadioButton.isChecked():
vm_settings["virtualization"] = "VirtualBox"
gns3_vm.setSettings(vm_settings)
servers.save()
vm_settings["engine"] = "virtualbox"
# set the vCPU count and RAM
vpcus = self.uiCPUSpinBox.value()
ram = self.uiRAMSpinBox.value()
if ram < 1024:
QtWidgets.QMessageBox.warning(self, "GNS3 VM memory", "It is recommended to allocate a minimum of 1024 MB of RAM to the GNS3 VM")
available_ram = int(psutil.virtual_memory().available / (1024 * 1024))
if ram > available_ram:
QtWidgets.QMessageBox.warning(self, "GNS3 VM memory", "You have probably allocated too much memory for the GNS3 VM! (available memory is {} MB)".format(available_ram))
if gns3_vm.setvCPUandRAM(vpcus, ram) is False:
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Could not configure vCPUs and RAM amounts for the GNS3 VM")
QtWidgets.QMessageBox.warning(self, "GNS3 VM memory", "It is recommended to allocate a minimum of 1024 MB of memory to the GNS3 VM")
vm_settings["vcpus"] = vpcus
vm_settings["ram"] = ram
# start the GNS3 VM
servers.initVMServer()
worker = WaitForVMWorker()
progress_dialog = ProgressDialog(worker, "GNS3 VM", "Starting the GNS3 VM...", "Cancel", busy=True, parent=self, delay=5)
progress_dialog.show()
if progress_dialog.exec_():
previous_local_server_ip = servers.localServer().host()
new_local_server_ip = gns3_vm.adjustLocalServerIP()
self.uiShowCheckBox.setChecked(True)
# restart the local server if necessary
if new_local_server_ip != previous_local_server_ip:
servers.stopLocalServer(wait=True)
if servers.startLocalServer():
worker = WaitForConnectionWorker(new_local_server_ip, servers.localServer().port())
dialog = ProgressDialog(worker, "Local server", "Connecting...", "Cancel", busy=True, parent=self)
dialog.show()
dialog.exec_()
self._setGNS3VMSettings(vm_settings)
else:
if not self.uiVmwareRadioButton.isChecked() and not self.uiVirtualBoxRadioButton.isChecked():
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Please select VMware or VirtualBox")
else:
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Please select a VM. If no VM is listed, check if the GNS3 VM is correctly imported and press refresh.")
return False
elif self.currentPage() == self.uiAddVMsWizardPage:
elif self.currentPage() == self.uiLocalServerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
local_server_settings["auto_start"] = True
local_server_settings["path"] = self.uiLocalServerPathLineEdit.text().strip()
local_server_settings["host"] = self.uiLocalServerHostComboBox.itemData(self.uiLocalServerHostComboBox.currentIndex())
local_server_settings["port"] = self.uiLocalServerPortSpinBox.value()
use_local_server = self.uiLocalRadioButton.isChecked()
if use_local_server:
if not os.path.isfile(local_server_settings["path"]):
QtWidgets.QMessageBox.critical(self, "Local server", "Could not find local server {}".format(local_server_settings["path"]))
return False
if not os.access(local_server_settings["path"], os.X_OK):
QtWidgets.QMessageBox.critical(self, "Local server", "{} is not an executable".format(local_server_settings["path"]))
return False
LocalServer.instance().updateLocalServerSettings(local_server_settings)
LocalServer.instance().localServerAutoStartIfRequire()
elif self.currentPage() == self.uiRemoteControllerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
local_server_settings["auto_start"] = False
local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
local_server_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText()
local_server_settings["user"] = self.uiRemoteMainServerUserLineEdit.text()
local_server_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
local_server_settings["auth"] = self.uiRemoteMainServerAuthCheckBox.isChecked()
LocalServer.instance().updateLocalServerSettings(local_server_settings)
elif self.currentPage() == self.uiSummaryWizardPage:
if self.uiLocalRadioButton.isChecked():
# deactivate the GNS3 VM if using the local server
vm_settings = {"auto_start": False}
gns3_vm.setSettings(vm_settings)
servers.save()
self.uiShowCheckBox.setChecked(True)
vm_settings = self._GNS3VMSettings()
vm_settings["enable"] = False
self._setGNS3VMSettings(vm_settings)
from gns3.modules import Dynamips
Dynamips.instance().setSettings({"use_local_server": use_local_server})
if sys.platform.startswith("linux"):
# IOU only works on Linux
from gns3.modules import IOU
IOU.instance().setSettings({"use_local_server": use_local_server})
from gns3.modules import Qemu
Qemu.instance().setSettings({"use_local_server": use_local_server})
from gns3.modules import VPCS
VPCS.instance().setSettings({"use_local_server": use_local_server})
elif self.currentPage() == self.uiLocalServerStatusWizardPage:
if not Controller.instance().connected():
return False
dialog = PreferencesDialog(self)
if self.uiAddIOSRouterCheckBox.isChecked():
self._setPreferencesPane(dialog, "Dynamips").uiNewIOSRouterPushButton.clicked.emit(False)
if self.uiAddIOUDeviceCheckBox.isChecked():
self._setPreferencesPane(dialog, "IOS on UNIX").uiNewIOUDevicePushButton.clicked.emit(False)
if self.uiAddQemuVMcheckBox.isChecked():
self._setPreferencesPane(dialog, "QEMU").uiNewQemuVMPushButton.clicked.emit(False)
if self.uiAddVirtualBoxVMcheckBox.isChecked():
self._setPreferencesPane(dialog, "VirtualBox").uiNewVirtualBoxVMPushButton.clicked.emit(False)
if self.uiAddVMwareVMcheckBox.isChecked():
self._setPreferencesPane(dialog, "VMware").uiNewVMwareVMPushButton.clicked.emit(False)
if self.uiAddDockerVMCheckBox.isChecked():
self._setPreferencesPane(dialog, "Docker").uiNewDockerVMPushButton.clicked.emit(False)
dialog.exec_()
return True
def _refreshVMListSlot(self):
@@ -235,11 +355,10 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
Refresh the list of VM available in VMware or VirtualBox.
"""
server = Servers.instance().localServer()
if self.uiVmwareRadioButton.isChecked():
server.get("/vmware/vms", self._getVMsFromServerCallback)
Controller.instance().get("/gns3vm/engines/vmware/vms", self._getVMsFromServerCallback, progressText="Retrieving VMware VM list from server...")
elif self.uiVirtualBoxRadioButton.isChecked():
server.get("/virtualbox/vms", self._getVMsFromServerCallback)
Controller.instance().get("/gns3vm/engines/virtualbox/vms", self._getVMsFromServerCallback, progressText="Retrieving VirtualBox VM list from server...")
def _getVMsFromServerCallback(self, result, error=False, **kwargs):
"""
@@ -255,9 +374,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
else:
self.uiVMListComboBox.clear()
for vm in result:
self.uiVMListComboBox.addItem(vm["vmname"], vm.get("vmx_path", ""))
gns3_vm = Servers.instance().vmSettings()
index = self.uiVMListComboBox.findText(gns3_vm["vmname"])
self.uiVMListComboBox.addItem(vm["vmname"])
index = self.uiVMListComboBox.findText(self._GNS3VMSettings()["vmname"])
if index != -1:
self.uiVMListComboBox.setCurrentIndex(index)
else:
@@ -274,8 +393,17 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
:param result: ignored
"""
Controller.instance().setDisplayError(True)
settings = self.parentWidget().settings()
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
if result:
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()
self.parentWidget().setSettings(settings)
super().done(result)
@@ -285,7 +413,21 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
current_id = self.currentId()
if self.page(current_id) == self.uiServerWizardPage and not self.uiVMRadioButton.isChecked():
# skip the GNS3 VM page if using the local server.
return self.uiServerWizardPage.nextId() + 1
if self.page(current_id) == self.uiLocalServerStatusWizardPage and not self.uiVMRadioButton.isChecked():
return self._pageId(self.uiSummaryWizardPage)
if self.page(current_id) == self.uiServerWizardPage and self.uiRemoteControllerRadioButton.isChecked():
return self._pageId(self.uiRemoteControllerWizardPage)
if self.page(current_id) == self.uiVMWizardPage:
return self._pageId(self.uiSummaryWizardPage)
return QtWidgets.QWizard.nextId(self)
def _pageId(self, page):
"""
Return id of the page
"""
for id in self.pageIds():
if self.page(id) == page:
return id
raise KeyError

View File

@@ -19,17 +19,14 @@
Dialog to manage the snapshots.
"""
import shutil
import re
import time
import os
from ..qt import QtCore, QtWidgets
from ..utils.progress_dialog import ProgressDialog
from ..utils.process_files_worker import ProcessFilesWorker
from ..ui.snapshots_dialog_ui import Ui_SnapshotsDialog
from ..topology import Topology
from ..node import Node
from ..controller import Controller
from datetime import datetime
import logging
log = logging.getLogger(__name__)
class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
@@ -40,46 +37,38 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
:param parent: parent widget
"""
def __init__(self, parent, project_path, project_files_dir):
def __init__(self, parent, project):
super().__init__(parent)
self.setupUi(self)
self._project_path = project_path
self._project_files_dir = os.path.join(project_files_dir, "project-files")
self._project = project
self.uiCreatePushButton.clicked.connect(self._createSnapshotSlot)
self.uiDeletePushButton.clicked.connect(self._deleteSnapshotSlot)
self.uiRestorePushButton.clicked.connect(self._restoreSnapshotSlot)
self.uiSnapshotsList.itemDoubleClicked.connect(self._snapshotDoubleClickedSlot)
self._listSnaphosts()
self._listSnapshots()
def _listSnaphosts(self):
def _listSnapshots(self):
"""
Lists all available snapshots.
"""
self.uiSnapshotsList.clear()
snapshot_dir = os.path.join(self._project_files_dir, "snapshots")
if not os.path.isdir(snapshot_dir):
if self._project:
Controller.instance().get("/projects/{}/snapshots".format(self._project.id()), self._listSnapshotsCallback)
def _listSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])
return
snapshots = []
for snapshot in os.listdir(snapshot_dir):
match = re.search(r"^(.*)_([0-9]+)_([0-9]+)", snapshot)
if match:
snapshot_name = match.group(1)
snapshot_date = match.group(2)[:2] + '/' + match.group(2)[2:4] + '/' + match.group(2)[4:]
snapshot_time = match.group(3)[:2] + ':' + match.group(3)[2:4] + ':' + match.group(3)[4:]
snapshots.append((snapshot_name, snapshot_date, snapshot_time))
# Sort by date
snapshots = sorted(snapshots, key=(lambda v: v[1] + v[2]))
for snapshot_name, snapshot_date, snapshot_time in snapshots:
for snapshot in result:
item = QtWidgets.QListWidgetItem(self.uiSnapshotsList)
item.setText("{} on {} at {}".format(snapshot_name, snapshot_date, snapshot_time))
item.setData(QtCore.Qt.UserRole, os.path.join(snapshot_dir, snapshot))
item.setText("{} on {}".format(snapshot["name"], datetime.fromtimestamp(snapshot["created_at"]).strftime("%d/%m/%y at %H:%M:%S")))
item.setData(QtCore.Qt.UserRole, snapshot["snapshot_id"])
if self.uiSnapshotsList.count():
self.uiSnapshotsList.setCurrentRow(0)
@@ -95,16 +84,15 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
"""
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
if ok and snapshot_name:
from ..main_window import MainWindow
MainWindow.instance().saveProject(self._project_path)
snapshot_name = "{name}_{date}".format(name=snapshot_name, date=time.strftime("%d%m%y_%H%M%S"))
snapshot_dir = os.path.join(self._project_files_dir, "snapshots", snapshot_name)
worker = ProcessFilesWorker(os.path.dirname(self._project_path), snapshot_dir, skip_dirs=["snapshots"])
progress_dialog = ProgressDialog(worker, "Creating snapshot", "Copying project files...", "Cancel", parent=self)
progress_dialog.show()
progress_dialog.exec_()
self._listSnaphosts()
if ok and snapshot_name and self._project:
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()), self._createSnapshotsCallback, {"name": snapshot_name})
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])
return
self._listSnapshots()
def _deleteSnapshotSlot(self):
"""
@@ -113,9 +101,15 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
item = self.uiSnapshotsList.currentItem()
if item:
snapshot_path = item.data(QtCore.Qt.UserRole)
shutil.rmtree(snapshot_path, ignore_errors=True)
self._listSnaphosts()
snapshot_id = item.data(QtCore.Qt.UserRole)
Controller.instance().delete("/projects/{}/snapshots/{}".format(self._project.id(), snapshot_id), self._deleteSnapshotsCallback)
def _deleteSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])
return
self._listSnapshots()
def _restoreSnapshotSlot(self):
"""
@@ -124,63 +118,26 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
item = self.uiSnapshotsList.currentItem()
if item:
snapshot_path = item.data(QtCore.Qt.UserRole)
self._restoreSnapshot(snapshot_path)
snapshot_id = item.data(QtCore.Qt.UserRole)
self._restoreSnapshot(snapshot_id)
def _restoreSnapshot(self, snapshot_path):
def _restoreSnapshot(self, snapshot_id):
"""
Restores a snapshot.
:param snapshot_path: path to the snapshot
:param snapshot_id: id of the snapshot
"""
match = re.search(r"^(.*)_([0-9]+)_([0-9]+)", os.path.basename(snapshot_path))
if match:
snapshot_name = match.group(1)
else:
snapshot_name = "Unknown"
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot \"{}\" was taken?".format(snapshot_name),
QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
if reply == QtWidgets.QMessageBox.Cancel:
return
# stop all the nodes
topology = Topology.instance()
for node in topology.nodes():
if hasattr(node, "start") and node.status() == Node.started:
node.stop()
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback)
project_name, _ = os.path.splitext(os.path.basename(self._project_path))
legacy_project_files_dir = os.path.join(snapshot_path, "{}-files".format(project_name))
if os.path.exists(legacy_project_files_dir):
# support for pre 1.3 snapshots
for root, dirs, _ in os.walk(self._project_files_dir):
dirs[:] = [d for d in dirs if d not in "snapshots"]
for project_subdir in dirs:
project_subdir_path = os.path.join(root, project_subdir)
shutil.rmtree(project_subdir_path, ignore_errors=True)
dirs = os.listdir(legacy_project_files_dir)
for snapshot_subdir in dirs:
snapshot_subdir_path = os.path.join(legacy_project_files_dir, snapshot_subdir)
worker = ProcessFilesWorker(snapshot_subdir_path, os.path.join(self._project_files_dir, snapshot_subdir))
progress_dialog = ProgressDialog(worker, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
progress_dialog.show()
progress_dialog.exec_()
try:
os.remove(self._project_path)
shutil.copy(os.path.join(snapshot_path, os.path.basename(self._project_path)), self._project_path)
except OSError as e:
QtWidgets.QMessageBox.critical(self, "Restore snapshot", "Cannot restore snapshot: {}".format(e))
else:
worker = ProcessFilesWorker(snapshot_path, os.path.dirname(self._project_path), skip_dirs=["snapshots"])
progress_dialog = ProgressDialog(worker, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
progress_dialog.show()
progress_dialog.exec_()
from ..main_window import MainWindow
MainWindow.instance().loadSnapshot(self._project_path)
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
if result:
log.error(result["message"])
return
self.accept()
def _snapshotDoubleClickedSlot(self, item):
@@ -188,5 +145,5 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
Slot to restore a snapshot when it is double clicked.
"""
snapshot_path = item.data(QtCore.Qt.UserRole)
self._restoreSnapshot(snapshot_path)
snapshot_id = item.data(QtCore.Qt.UserRole)
self._restoreSnapshot(snapshot_id)

View File

@@ -20,11 +20,15 @@ Dialog to change node symbols.
"""
import os
import pathlib
from ..qt import QtCore, QtGui, QtWidgets
from ..qt import QtCore, QtGui, QtWidgets, qpartial
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from ..ui.symbol_selection_dialog_ui import Ui_SymbolSelectionDialog
from ..servers import Servers
from ..local_server import LocalServer
from ..controller import Controller
from ..symbol import Symbol
import logging
log = logging.getLogger(__name__)
@@ -39,6 +43,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
:param items: list of items
"""
_symbols_dir = None
def __init__(self, parent, items=None, symbol=None):
super().__init__(parent)
@@ -51,8 +57,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
self.uiBuiltInSymbolRadioButton.toggled.connect(self._builtInSymbolToggledSlot)
self.uiSearchLineEdit.textChanged.connect(self._searchTextChangedSlot)
self.uiBuiltinSymbolOnlyCheckBox.toggled.connect(self._builtinSymbolOnlyToggledSlot)
self._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
self._symbols_path = Servers.instance().localServerSettings()["symbols_path"]
if not SymbolSelectionDialog._symbols_dir:
SymbolSelectionDialog._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
if not self._items:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
@@ -60,38 +66,40 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
self.uiBuiltInSymbolRadioButton.setChecked(True)
self.uiSymbolListWidget.setFocus()
self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
symbol_resources = QtCore.QResource(":/symbols")
self._symbol_items = []
symbols = symbol_resources.children()
try:
for file in os.listdir(self._symbols_path):
symbols.append(file)
except OSError:
pass
Controller.instance().get("/symbols", self._listSymbolsCallback)
symbols.sort()
for symbol in symbols:
if symbol.endswith(".svg") or symbol.endswith(".png"):
name = os.path.splitext(symbol)[0]
item = QtWidgets.QListWidgetItem(self.uiSymbolListWidget)
self._symbol_items.append(item)
item.setText(name)
def _listSymbolsCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while listing symbols: {}".format(result["message"]))
return
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)
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)
def render(item, path):
svg_renderer = QImageSvgRenderer(path)
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
# Set the ARGB to 0 to prevent rendering artifacts
image.fill(0x00000000)
if os.path.exists(os.path.join(self._symbols_path, symbol)):
svg_renderer = QImageSvgRenderer(os.path.join(self._symbols_path, symbol))
else:
resource_path = ":/symbols/" + symbol
svg_renderer = QImageSvgRenderer(resource_path)
svg_renderer.render(QtGui.QPainter(image))
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
item.setIcon(icon)
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
self.adjustSize()
def _builtinSymbolOnlyToggledSlot(self, checked):
@@ -106,7 +114,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
"""
text = self.uiSearchLineEdit.text()
for item in self._symbol_items:
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not QtCore.QResource(":/symbols/{}.svg".format(item.text())).isValid():
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not item.data(QtCore.Qt.UserRole).builtin():
item.setHidden(True)
else:
if len(text.strip()) == 0 or text.strip().lower() in item.text().lower():
@@ -148,18 +156,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
"""
symbol_path = self.getSymbol()
pixmap = QtGui.QPixmap(symbol_path)
if not pixmap.isNull():
for item in self._items:
renderer = QImageSvgRenderer(symbol_path)
renderer.setObjectName(symbol_path)
if renderer.isValid():
item.setSharedRenderer(renderer)
else:
QtWidgets.QMessageBox.critical(self, "Custom pixmap symbol", "Invalid image")
return False
for item in self._items:
item.setSymbol(symbol_path)
return True
def getSymbol(self):
@@ -167,27 +165,27 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
if self.uiSymbolListWidget.isEnabled():
current = self.uiSymbolListWidget.currentItem()
if current:
name = current.text()
if QtCore.QResource(":/symbols/{}.svg".format(name)).isValid():
return ":/symbols/{}.svg".format(name)
else:
symbol_path = os.path.join(self._symbols_path, "{}.svg".format(name))
if not os.path.exists(symbol_path):
symbol_path = os.path.join(self._symbols_path, "{}.png".format(name))
return symbol_path
return current.data(QtCore.Qt.UserRole).id()
else:
return self.uiSymbolLineEdit.text()
return os.path.basename(self.uiSymbolLineEdit.text())
return None
def _symbolBrowserSlot(self):
# supported image file formats
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.pbm *.pgm *.png *.ppm *.xbm *.xpm *.gif);;All files (*.*)"
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", self._symbols_dir, file_formats)
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", SymbolSelectionDialog._symbols_dir, file_formats)
if not path:
return
SymbolSelectionDialog._symbols_dir = os.path.dirname(path)
self._symbols_dir = os.path.dirname(path)
symbol_id = os.path.basename(path)
Controller.instance().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))
return
self.uiSymbolLineEdit.clear()
self.uiSymbolLineEdit.setText(path)
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(path))
@@ -199,10 +197,9 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
:param result: boolean (accepted or rejected)
"""
if result:
if not self.uiSymbolListWidget.isEnabled() and not os.path.exists(self.uiSymbolLineEdit.text()):
QtWidgets.QMessageBox.critical(self, "Custom symbol", "Invalid path to custom symbol: {}".format(self.uiSymbolLineEdit.text()))
result = 0
elif result and self._items and not self._applyPreferencesSlot():
result = 0
if result and self._items and not self._applyPreferencesSlot():
result = 0
super().done(result)

View File

@@ -19,7 +19,7 @@
Text editor to edit Note items.
"""
from ..qt import QtCore, QtWidgets
from ..qt import QtCore, QtWidgets, qslot
from ..ui.text_editor_dialog_ui import Ui_TextEditorDialog
@@ -70,16 +70,19 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
color.blue(),
color.alpha()))
def _setFontSlot(self):
@qslot
def _setFontSlot(self, *args):
"""
Slot to select the font.
"""
selected_font, ok = QtWidgets.QFontDialog.getFont(self.uiPlainTextEdit.font(), self)
selected_font, ok = QtWidgets.QFontDialog.getFont(self.uiPlainTextEdit.font(), self,
options=QtWidgets.QFontDialog.DontUseNativeDialog)
if ok:
self.uiPlainTextEdit.setFont(selected_font)
def _setColorSlot(self):
@qslot
def _setColorSlot(self, *args):
"""
Slot to select the color.
"""
@@ -88,7 +91,8 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
if color.isValid():
self._setColor(color)
def _applyPreferencesSlot(self):
@qslot
def _applyPreferencesSlot(self, *args):
"""
Applies the new text settings.
"""

View File

@@ -18,7 +18,7 @@
from .vm_wizard import VMWizard
from gns3.qt import QtWidgets
from gns3.servers import Servers
from gns3.controller import Controller
class VMWithImagesWizard(VMWizard):
@@ -26,18 +26,17 @@ class VMWithImagesWizard(VMWizard):
Base class for VM wizard with image management (Qemu, IOU...)
:param devices: List of existing device for this type
:param use_local_server: Value the use_local_server settings for this module
:param parent: parent widget
"""
def __init__(self, devices, use_local_server, parent):
def __init__(self, devices, parent):
# The list of images combo box (Qemu support multiple images)
self._images_combo_boxes = set()
# The list of radio button for existing image or new images
self._radio_existing_images_buttons = set()
super().__init__(devices, use_local_server, parent)
super().__init__(devices, parent)
def refreshImageStepsButtons(self):
"""
@@ -89,9 +88,7 @@ class VMWithImagesWizard(VMWizard):
self._radio_existing_images_buttons.add(radio_button)
def _imageCreateSlot(self, line_edit, create_image_wizard, image_suffix):
server = Servers.instance().getServerFromString(self.getSettings()["server"])
create_dialog = create_image_wizard(self, server, self.uiNameLineEdit.text() + image_suffix)
create_dialog = create_image_wizard(self, self.getSettings()["server"], self.uiNameLineEdit.text() + image_suffix)
if QtWidgets.QDialog.Accepted == create_dialog.exec_():
line_edit.setText(create_dialog.uiLocationLineEdit.text())
@@ -100,8 +97,7 @@ class VMWithImagesWizard(VMWizard):
Slot to open a file browser and select an image.
"""
server = Servers.instance().getServerFromString(self.getSettings()["server"])
path = image_selector(self, server)
path = image_selector(self, self._compute_id)
if not path:
return
line_edit.clear()
@@ -146,7 +142,7 @@ class VMWithImagesWizard(VMWizard):
:param endpoint: server endpoint with the list of Images
"""
self._server.get(endpoint, self._getImagesFromServerCallback)
Controller.instance().getCompute(endpoint, self._compute_id, self._getImagesFromServerCallback)
def _getImagesFromServerCallback(self, result, error=False, **kwargs):
"""
@@ -185,10 +181,8 @@ class VMWithImagesWizard(VMWizard):
for vm in result:
combo_box.addItem(vm["path"], vm)
def _widgetOnCurrentPage(self, widget):
"""
:returns Boolean True if widget is current active Wizard page
"""
return self.currentPage().findChild(widget.__class__, widget.objectName()) is not None

View File

@@ -18,8 +18,8 @@
import sys
from gns3.qt import QtWidgets
from gns3.servers import Servers
from gns3.gns3_vm import GNS3VM
from gns3.compute_manager import ComputeManager
from gns3.controller import Controller
class VMWizard(QtWidgets.QWizard):
@@ -27,18 +27,17 @@ class VMWizard(QtWidgets.QWizard):
Base class for VM wizard.
:param devices: List of existing device for this type
:param use_local_server: Value the use_local_server settings for this module
:param parent: parent widget
"""
def __init__(self, devices, use_local_server, parent):
def __init__(self, devices, parent):
super().__init__(parent)
self.setupUi(self)
self.setModal(True)
self._devices = devices
self._use_local_server = use_local_server
self._local_server_disable = False
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
if sys.platform.startswith("darwin"):
@@ -50,14 +49,16 @@ class VMWizard(QtWidgets.QWizard):
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
if Controller.instance().isRemote():
self.uiLocalRadioButton.setText("Run device on the main server")
# By default we use the local server
self._server = Servers.instance().localServer()
self._compute_id = "local"
self.uiLocalRadioButton.setChecked(True)
self._localToggledSlot(True)
if Servers.instance().isNonLocalServerConfigured() is False:
# skip the server page if we use the local server
if len(ComputeManager.instance().computes()) == 1:
# skip the server page if we use the first server
self.setStartId(1)
def _vmToggledSlot(self, checked):
@@ -92,44 +93,45 @@ class VMWizard(QtWidgets.QWizard):
self.uiRemoteServersGroupBox.setEnabled(False)
self.uiRemoteServersGroupBox.hide()
def setStartId(self, index):
"""
Which page should we use when starting the Wizard
"""
super().setStartId(index)
# If we skip the initial page (choosing a server)
# we check the settings
if index != 0:
self.uiLocalRadioButton.setChecked(True)
def initializePage(self, page_id):
if self.page(page_id) == self.uiServerWizardPage:
self.uiRemoteServersComboBox.clear()
if len(Servers.instance().remoteServers().values()) == 0:
self.uiRemoteRadioButton.setEnabled(False)
else:
for server in Servers.instance().remoteServers().values():
self.uiRemoteServersComboBox.addItem(server.url(), server)
if hasattr(self, "uiVMRadioButton") and not GNS3VM.instance().isRunning():
self.uiRemoteRadioButton.setEnabled(False)
if hasattr(self, "uiVMRadioButton"):
self.uiVMRadioButton.setEnabled(False)
if hasattr(self, "uiVMRadioButton") and GNS3VM.instance().isRunning():
self.uiVMRadioButton.setChecked(True)
elif self._use_local_server and self.uiLocalRadioButton.isEnabled():
self.uiLocalRadioButton.setEnabled(False)
for compute in ComputeManager.instance().computes():
if compute.id() == "local":
self.uiLocalRadioButton.setEnabled(True)
elif compute.id() == "vm":
if hasattr(self, "uiVMRadioButton"):
self.uiVMRadioButton.setEnabled(True)
else:
self.uiRemoteRadioButton.setEnabled(True)
self.uiRemoteServersComboBox.addItem(compute.name(), compute.id())
if self.uiLocalRadioButton.isEnabled() and not self._local_server_disable:
self.uiLocalRadioButton.setChecked(True)
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isEnabled():
self.uiVMRadioButton.setChecked(True)
else:
if self.uiRemoteRadioButton.isEnabled():
self.uiRemoteRadioButton.setChecked(True)
else:
self.uiLocalRadioButton.setChecked(True)
def validateCurrentPage(self):
def _disableLocalServer(self):
"""
Validates the server.
Turn off the local server
"""
self._local_server_disable = True
self.uiLocalRadioButton.hide()
self.uiLocalRadioButton.setEnabled(False)
self.setStartId(0)
def validateCurrentPage(self):
if hasattr(self, "uiNameWizardPage") and self.currentPage() == self.uiNameWizardPage:
name = self.uiNameLineEdit.text()
for device in self._devices.values():
@@ -137,17 +139,22 @@ class VMWizard(QtWidgets.QWizard):
QtWidgets.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
return False
elif self.currentPage() == self.uiServerWizardPage:
# If the local button is not visible it's because it's not supported
if self.uiLocalRadioButton.isChecked() and self.uiLocalRadioButton.isHidden():
QtWidgets.QMessageBox.critical(self, "New device", "Please configure before the GNS3 VM in order to use this device.")
return False
if self.uiRemoteRadioButton.isChecked():
if not Servers.instance().remoteServers():
if self.uiRemoteServersComboBox.count() == 0:
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
return False
self._server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
gns3_vm_server = Servers.instance().vmServer()
if gns3_vm_server is None:
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "The GNS3 VM is not running")
return False
self._server = gns3_vm_server
self._compute_id = "vm"
else:
self._server = Servers.instance().localServer()
if self.uiLocalRadioButton.isEnabled():
self._compute_id = "local"
else:
QtWidgets.QMessageBox.critical(self, "Server", "No available server support this type of node. You probably need to setup the GNS3 VM")
return False
return True

View File

@@ -1,325 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Manages the GNS3 VM.
"""
import os
import sys
import subprocess
import codecs
import shutil
from .qt import QtNetwork
from collections import OrderedDict
from .servers import Servers
import logging
log = logging.getLogger(__name__)
class GNS3VM:
"""
GNS3 VM management class.
"""
def __init__(self):
self._is_running = False
# The current running vboxmanage and vmrun process
self._running_process = None
def settings(self):
"""
Returns the GNS3 VM settings.
:returns: GNS3 VM settings (dict)
"""
return Servers.instance().vmSettings()
def setSettings(self, settings):
"""
Set new GNS3 VM settings.
:param settings: GNS3 VM settings (dict)
"""
Servers.instance().setVMsettings(settings)
def killRunningProcess(self):
"""
Kill the VBoxManage or vmrun process if running
"""
if self._running_process is not None:
self._running_process.kill()
self._running_process.wait()
self._running_process = None
def _process_check_output(self, command, timeout=None):
# Original code from Python's subprocess.check_output
# https://github.com/python/cpython/blob/3.4/Lib/subprocess.py
with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=os.environ) as process:
self._running_process = process
try:
output, unused_err = process.communicate(None, timeout=timeout)
except subprocess.TimeoutExpired:
process.kill()
output, unused_err = process.communicate()
self._running_process = None
raise subprocess.TimeoutExpired(process.args, timeout, output=output)
except:
self.killRunningProcess()
raise
retcode = process.poll()
if retcode:
self._running_process = None
raise subprocess.CalledProcessError(retcode, process.args, output=output)
self._running_process = None
return output.decode("utf-8", errors="ignore").strip()
def execute_vmrun(self, subcommand, args, timeout=60):
from gns3.modules.vmware import VMware
vmware_settings = VMware.instance().settings()
vmrun_path = vmware_settings["vmrun_path"]
if sys.platform.startswith("darwin"):
command = [vmrun_path, "-T", "fusion", subcommand]
else:
host_type = vmware_settings["host_type"]
command = [vmrun_path, "-T", host_type, subcommand]
command.extend(args)
log.debug("Executing vmrun with command: {}".format(command))
return self._process_check_output(command, timeout=timeout)
def execute_vboxmanage(self, subcommand, args, timeout=60):
from gns3.modules.virtualbox import VirtualBox
virtualbox_settings = VirtualBox.instance().settings()
vboxmanage_path = virtualbox_settings["vboxmanage_path"]
command = [vboxmanage_path, "--nologo", subcommand]
command.extend(args)
log.debug("Executing VBoxManage with command: {}".format(command))
return self._process_check_output(command, timeout=timeout)
@staticmethod
def parse_vmx_file(path):
"""
Parses a VMX file.
:param path: path to the VMX file
:returns: dict
"""
pairs = OrderedDict()
encoding = "utf-8"
# get the first line to read the .encoding value
with open(path, "rb") as f:
line = f.readline().decode(encoding, errors="ignore")
if line.startswith("#!"):
# skip the shebang
line = f.readline().decode(encoding, errors="ignore")
try:
key, value = line.split('=', 1)
if key.strip().lower() == ".encoding":
file_encoding = value.strip('" ')
try:
codecs.lookup(file_encoding)
encoding = file_encoding
except LookupError:
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
except ValueError:
log.warning("Couldn't find file encoding in {}, using {}...".format(path, encoding))
# read the file with the correct encoding
with open(path, encoding=encoding, errors="ignore") as f:
for line in f.read().splitlines():
try:
key, value = line.split('=', 1)
pairs[key.strip().lower()] = value.strip('" ')
except ValueError:
continue
return pairs
@staticmethod
def write_vmx_file(path, pairs):
"""
Write a VMware VMX file.
:param path: path to the VMX file
:param pairs: settings to write
"""
encoding = "utf-8"
if ".encoding" in pairs:
file_encoding = pairs[".encoding"]
try:
codecs.lookup(file_encoding)
encoding = file_encoding
except LookupError:
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
with open(path, "w", encoding=encoding, errors="ignore") as f:
if sys.platform.startswith("linux"):
# write the shebang on the first line on Linux
vmware_path = shutil.which("vmware")
if vmware_path:
f.write("#!{}\n".format(vmware_path))
for key, value in pairs.items():
entry = '{} = "{}"\n'.format(key, value)
f.write(entry)
def autoStart(self):
"""
Automatically start the GNS3 VM at startup.
:returns: boolean
"""
vm_settings = Servers.instance().vmSettings()
return vm_settings["auto_start"]
def isRemote(self):
"""
Checks if the GNS3 VM is remote.
:returns: boolean
"""
vm_settings = Servers.instance().vmSettings()
if vm_settings["virtualization"] == "remote":
return True
return False
def adjustLocalServerIP(self):
"""
Adjust the local server IP address to be in the same subnet as the GNS3 VM.
:returns: the local server IP/host address
"""
servers = Servers.instance()
local_server_settings = servers.localServerSettings()
if Servers.instance().vmSettings()["adjust_local_server_ip"]:
vm_server = servers.vmServer()
vm_ip_address = vm_server.host()
log.debug("GNS3 VM IP address is {}".format(vm_ip_address))
for interface in QtNetwork.QNetworkInterface.allInterfaces():
for address in interface.addressEntries():
ip = address.ip().toString()
prefix_length = address.prefixLength()
subnet = QtNetwork.QHostAddress.parseSubnet("{}/{}".format(ip, prefix_length))
if QtNetwork.QHostAddress(vm_ip_address).isInSubnet(subnet):
if local_server_settings["host"] != ip:
log.info("Adjust local server IP address to {}".format(ip))
servers.setLocalServerSettings({"host": ip})
servers.registerLocalServer()
servers.save()
return ip
return local_server_settings["host"]
def setRunning(self, value):
"""
Sets either the GNS3 VM is running or not.
:param value: boolean
"""
self._is_running = value
def isRunning(self):
"""
Returns either the GNS3 VM is running or not.
:returns: boolean
"""
return self._is_running
def setvCPUandRAM(self, vcpus, ram):
"""
Set the vCPU cores and RAM amount for the GNS3 VM.
:param vcpus: number of vCPU cores
:param ram: amount of memory
:returns: boolean
"""
vm_settings = self.settings()
if vm_settings["virtualization"] == "VMware":
try:
pairs = self.parse_vmx_file(vm_settings["vmx_path"])
pairs["numvcpus"] = str(vcpus)
pairs["memsize"] = str(ram)
self.write_vmx_file(vm_settings["vmx_path"], pairs)
except OSError as e:
log.error('Could not read/write VMware VMX file "{}": {}'.format(vm_settings["vmx_path"], e))
return False
elif vm_settings["virtualization"] == "VirtualBox":
try:
self.execute_vboxmanage("modifyvm", [vm_settings["vmname"], "--cpus", str(vcpus)], timeout=3)
self.execute_vboxmanage("modifyvm", [vm_settings["vmname"], "--memory", str(ram)], timeout=3)
except OSError as e:
log.error("Could not execute VBoxManage: {}".format(e), True)
return False
except subprocess.SubprocessError as e:
log.error("Could not execute VBoxManage: {} with output '{}'".format(e, e.output.decode("utf-8", errors="ignore").strip()), True)
return False
except subprocess.TimeoutExpired:
log.error("VBoxmanage timeout expired", True)
return False
log.info("GNS3 VM vCPU count set to {} and RAM to {} MB".format(vcpus, ram))
return True
def shutdown(self, force=False):
"""
Gracefully shutdowns the GNS3 VM.
"""
vm_settings = self.settings()
if self._is_running and (vm_settings["auto_stop"] or force):
try:
if vm_settings["virtualization"] == "VMware":
if vm_settings["vmx_path"] is None:
log.error("No vm path configured, can't stop the VM")
return
self.execute_vmrun("stop", [vm_settings["vmx_path"], "soft"])
elif vm_settings["virtualization"] == "VirtualBox":
self.execute_vboxmanage("controlvm", [vm_settings["vmname"], "acpipowerbutton"], timeout=3)
except (OSError, subprocess.SubprocessError):
pass
except subprocess.TimeoutExpired:
log.warning("Could not ACPI shutdown the VM (timeout expired)")
self._is_running = False
@staticmethod
def instance():
"""
Singleton to return only on instance of GNS3VM
:returns: instance of GNS3VM
"""
if not hasattr(GNS3VM, "_instance") or GNS3VM._instance is None:
GNS3VM._instance = GNS3VM()
return GNS3VM._instance

File diff suppressed because it is too large Load Diff

View File

@@ -15,19 +15,19 @@
# 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
import json
import http
import copy
import ipaddress
import http
import uuid
import urllib.request
import pathlib
import base64
import datetime
import ipaddress
import urllib.request
from .version import __version__, __version_info__
from .qt import QtCore, QtNetwork, qpartial
from .network_client import getNetworkUrl
from .qt import QtCore, QtNetwork, qpartial, sip_is_deleted
from .utils import parse_version
import logging
@@ -49,75 +49,54 @@ class HTTPClient(QtCore.QObject):
:param network_manager: A QT network manager
"""
_instance_count = 1
# How many times we need to retry a connection
MAX_RETRY_CONNECTION = 5
# Callback class used for displaying progress
_progress_callback = None
connection_connected_signal = QtCore.Signal()
connection_closed_signal = QtCore.Signal()
system_usage_updated_signal = QtCore.Signal()
connection_error_signal = QtCore.Signal(str)
connection_disconnected_signal = QtCore.Signal()
def __init__(self, settings, network_manager):
def __init__(self, settings, network_manager=None):
super().__init__()
self._version = ""
self._scheme = settings.get("protocol", "http")
self._protocol = settings.get("protocol", "http")
self._host = settings["host"]
if "http_host" in settings:
self._http_host = settings["http_host"]
else:
self._http_host = settings["host"]
try:
if self._host is None or self._host == "0.0.0.0":
self._host = "127.0.0.1"
elif ":" in self._host and ipaddress.IPv6Address(self._host) and str(ipaddress.IPv6Address(self._host)) == "::":
self._host = "::1"
except ipaddress.AddressValueError:
log.error("Invalid host name %s", self._host)
self._port = int(settings["port"])
self._http_port = int(settings["port"])
self._user = settings.get("user", None)
self._password = settings.get("password", None)
# How many time we have retry connection
self._retry = 0
self._connected = False
self._local = True
self._cloud = False
self._gns3_vm = False
self._shutdown = False # Shutdown in progress
self._accept_insecure_certificate = settings.get("accept_insecure_certificate", None)
self._usage = None
self._network_manager = network_manager
# In order to detect computer hibernation we detect the date of the last
# query and disconnect if time is too long between two query
self._last_query_timestamp = None
self._max_time_difference_between_queries = None
if network_manager:
self._network_manager = network_manager
else:
self._network_manager = QtNetwork.QNetworkAccessManager()
# A buffer used by progress download
self._buffer = {}
# create an unique ID
self._id = HTTPClient._instance_count
HTTPClient._instance_count += 1
# List of query waiting for the connection
self._query_waiting_connections = []
def settings(self):
"""
Return a dictionnary with server settings
"""
settings = {"protocol": self.protocol(),
"host": self.host(),
"port": self.port(),
"user": self.user(),
"password": self._password}
if self.protocol() == "https":
settings["accept_insecure_certificate"] = self.acceptInsecureCertificate()
return settings
def acceptInsecureCertificate(self, certificate=None):
"""
Does the server accept this insecure SSL certificate digest
:param: Certificate digest
"""
return self._accept_insecure_certificate
def setAcceptInsecureCertificate(self, certificate):
"""
Does the server accept this insecure SSL certificate digest
:param: Certificate digest
"""
self._accept_insecure_certificate = certificate
def setMaxTimeDifferenceBetweenQueries(self, value):
self._max_time_difference_between_queries = value
def host(self):
"""
@@ -127,7 +106,6 @@ class HTTPClient(QtCore.QObject):
def setHost(self, host):
self._host = host
self._http_host = host
def port(self):
"""
@@ -137,13 +115,20 @@ class HTTPClient(QtCore.QObject):
def setPort(self, port):
self._port = port
self._http_port = port
def protocol(self):
"""
Transport protocol
"""
return self._scheme
return self._protocol
def setAcceptInsecureCertificate(self, certificate):
"""
Does the server accept this insecure SSL certificate digest
:param: Certificate digest
"""
self._accept_insecure_certificate = certificate
def user(self):
"""
@@ -151,8 +136,23 @@ class HTTPClient(QtCore.QObject):
"""
return self._user
def setUser(self, user):
self._user = user
def url(self):
"""Returns current server url"""
if ":" in self.host():
return "{}://[{}]:{}".format(self.protocol(), self.host(), self.port())
return "{}://{}:{}".format(self.protocol(), self.host(), self.port())
def fullUrl(self):
"""Returns current server url including user and password"""
host = self.host()
if ":" in self.host():
host = "[{}]".format(host)
if self._user:
return "{}://{}:{}@{}:{}".format(self.protocol(), self._user, self._password, host, self.port())
else:
return "{}://{}:{}".format(self.protocol(), host, self.port())
def password(self):
return self._password
@@ -160,40 +160,46 @@ class HTTPClient(QtCore.QObject):
def setPassword(self, password):
self._password = password
def notify_progress_start_query(self, query_id, progress_text, response):
def shutdown(self):
"""
Stop the server and stop to accept queries
"""
self.createHTTPQuery("POST", "/shutdown", None, showProgress=False)
self._shutdown = True
def _notify_progress_start_query(self, query_id, progress_text, response):
"""
Called when a query start
"""
if HTTPClient._progress_callback:
if not sip_is_deleted(HTTPClient._progress_callback):
if progress_text:
HTTPClient._progress_callback.add_query_signal.emit(query_id, progress_text, response)
else:
if self._local:
HTTPClient._progress_callback.add_query_signal.emit(query_id, "Waiting for local GNS3 server", response)
else:
HTTPClient._progress_callback.add_query_signal.emit(query_id, "Waiting for {}".format(self.url()), response)
HTTPClient._progress_callback.add_query_signal.emit(query_id, "Waiting for {}".format(self.url()), response)
def notify_progress_end_query(cls, query_id):
def _notify_progress_end_query(cls, query_id):
"""
Called when a query is over
"""
if HTTPClient._progress_callback:
if not sip_is_deleted(HTTPClient._progress_callback):
HTTPClient._progress_callback.remove_query_signal.emit(query_id)
def notify_progress_upload(self, query_id, sent, total):
def _notify_progress_upload(self, query_id, sent, total):
"""
Called when a query upload progress
"""
if HTTPClient._progress_callback:
if not sip_is_deleted(HTTPClient._progress_callback):
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
def notify_progress_download(self, query_id, sent, total):
def _notify_progress_download(self, query_id, sent, total):
"""
Called when a query download progress
"""
if HTTPClient._progress_callback:
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
if not sip_is_deleted(HTTPClient._progress_callback):
# abs() for maxium because sometimes the system send negative
# values
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, abs(total))
@classmethod
def setProgressCallback(cls, progress_callback):
@@ -203,59 +209,6 @@ class HTTPClient(QtCore.QObject):
cls._progress_callback = progress_callback
@staticmethod
def reset():
"""Reset HTTP client internal variables"""
HTTPClient._instance_count = 0
def url(self):
"""Returns current server url"""
return getNetworkUrl(self.protocol(), self.host(), self.port(), self.user(), self.settings())
def id(self):
"""
Returns this HTTP Client identifier.
:returns: HTTP client identifier (string)
"""
return self._id
def setLocal(self, value):
"""
Sets either this is a connection to a local server or not.
:param value: boolean
"""
self._local = value
def isLocal(self):
"""
Returns either this is a connection to a local server or not.
:returns: boolean
"""
return self._local
def setGNS3VM(self, value):
"""
Sets either this is a connection to the GNS3 VM or not.
:param value: boolean
"""
self._gns3_vm = value
def isGNS3VM(self):
"""
Returns either this is a connection to the GNS3 VM or not.
:returns: boolean
"""
return self._gns3_vm
def connected(self):
"""
Returns if the client is connected.
@@ -268,117 +221,8 @@ class HTTPClient(QtCore.QObject):
"""
Closes the connection with the server.
"""
log.info("Connection to %s closed", self.url())
self._connected = False
self.connection_closed_signal.emit()
def isLocalServerRunning(self):
"""
Synchronous check if a server is already running on this host.
:returns: boolean
"""
status, json_data = self.getSynchronous("version", timeout=2)
if json_data is None or status != 200:
return False
else:
version = json_data.get("version", None)
if version is None:
log.debug("Server is not a GNS3 server")
return False
return True
def getSynchronous(self, endpoint, timeout=2):
"""
Synchronous check if a server is running
:returns: Tuple (Status code, json of anwser). Status 0 is a non HTTP error
"""
try:
url = "{protocol}://{host}:{port}/v1/{endpoint}".format(protocol=self._scheme, host=self._http_host, port=self._http_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()
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.warn("Invalid local server url: {}".format(e))
return 0, None
except urllib.error.URLError:
# Connection refused. It's a normal behavior if server is not started
return 0, None
except urllib.error.HTTPError as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return e.code, None
except (OSError, http.client.BadStatusLine, ValueError) as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return 0, None
def get(self, path, callback, **kwargs):
"""
HTTP GET on the remote server
:param path: Remote path
:param callback: callback method to call when the server replies
Full arg list in createHTTPQuery
"""
return self.createHTTPQuery("GET", path, callback, **kwargs)
def put(self, path, callback, **kwargs):
"""
HTTP PUT on the remote server
:param path: Remote path
:param callback: callback method to call when the server replies
Full arg list in createHTTPQuery
"""
self.createHTTPQuery("PUT", path, callback, **kwargs)
def post(self, path, callback, **kwargs):
"""
HTTP POST on the remote server
:param path: Remote path
:param callback: callback method to call when the server replies
Full arg list in createHTTPQuery
"""
self.createHTTPQuery("POST", path, callback, **kwargs)
def delete(self, path, callback, **kwargs):
"""
HTTP DELETE on the remote server
:param path: Remote path
:param callback: callback method to call when the server replies
Full arg list in createHTTPQuery
"""
self.createHTTPQuery("DELETE", path, callback, **kwargs)
self._progress_callback.reset()
def _request(self, url):
"""
@@ -391,15 +235,25 @@ class HTTPClient(QtCore.QObject):
return QtNetwork.QNetworkRequest(url)
def _connect(self, query):
def _connect(self, query, server):
"""
Initialize the connection
:param query: The query to execute when all network stack is ready
:param query: The Server to connect
"""
self.executeHTTPQuery("GET", "/version", query, {}, timeout=5)
def createHTTPQuery(self, method, path, callback, body={}, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, timeout=120, **kwargs):
def createHTTPQuery(self, method, path, callback, body={}, context={},
downloadProgressCallback=None,
showProgress=True,
ignoreErrors=False,
progressText=None,
timeout=120,
server=None,
prefix="/v2",
params={},
networkManager=None,
**kwargs):
"""
Call the remote server, if not connected, check connection before
@@ -410,42 +264,78 @@ class HTTPClient(QtCore.QObject):
:param context: Pass a context to the response callback
:param downloadProgressCallback: Callback called when received something, it can be an incomplete response
:param showProgress: Display progress to the user
:params progressText: Text display to user in the progress dialog. None for auto generated
:param progressText: Text display to user in the progress dialog. None for auto generated
:param ignoreErrors: Ignore connection error (usefull to not closing a connection when notification feed is broken)
:param server: The server where the query will run
:param timeout: Delay in seconds before raising a timeout
:param prefix: Prefix to the path
:param networkManager: QNetworkAccessManager None use the default
:param params: Query arguments parameters
:returns: QNetworkReply
"""
if self._connected:
return self.executeHTTPQuery(method, path, qpartial(callback), body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText, timeout=timeout)
else:
log.info("Connection to {}".format(self.url()))
query = qpartial(self._callbackConnect, method, path, qpartial(callback), body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText, timeout=timeout)
self._connect(query)
if "dev" in __version__:
assert QtCore.QThread.currentThread() == self.thread(), "HTTP request not started from the main thread"
def _connectionError(self, callback, msg=""):
# Shutdown in progress do not execute the query
if self._shutdown:
return
# 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
request = qpartial(self._executeHTTPQuery, method, path, qpartial(callback), body, context,
downloadProgressCallback=downloadProgressCallback,
showProgress=showProgress,
ignoreErrors=ignoreErrors,
progressText=progressText,
networkManager=networkManager,
server=server,
timeout=timeout,
prefix=prefix,
params=params)
if self._connected:
return request()
else:
self._query_waiting_connections.append((request, callback))
# If we are not connected and we enqueue the first query we open the conection
if len(self._query_waiting_connections) == 1:
log.info("Connection to {}".format(self.url()))
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5, showProgress=False)
def _connectionError(self, callback, msg="", server=None):
"""
Return an error to user if connection failed
:param callback: User callback
:param msg: An optional additional message for the callback
:param server: Server where the query is execute
"""
if self.isLocal():
server = "local server {}".format(self.url())
else:
server = "remote server {}".format(self.url())
if len(msg) > 0:
msg = "Cannot connect to {}: {}".format(server, msg)
msg = "Cannot connect to server {}: {}".format(self.url(), msg)
else:
if self.isLocal():
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall.".format(server)
else:
msg = "Cannot connect to {}".format(server)
log.error(msg)
if callback is not None:
callback({"message": msg}, error=True, server=self)
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall. And that server version is {}.".format(self.url(), __version__)
for request, callback in self._query_waiting_connections:
if callback is not None:
callback({"message": msg}, error=True, server=server)
self._query_waiting_connections = []
def _callbackConnect(self, method, path, callback, body, original_context, params, error=False, server=None, **kwargs):
def _retryConnection(self, server=None):
log.debug("Retry connection to {}".format(self.url()))
self._retry += 1
QtCore.QTimer.singleShot(1000, qpartial(self._executeHTTPQuery, "GET", "/version", self._callbackConnect, {}, server=server, timeout=5))
def _callbackConnect(self, params, error=False, server=None, **kwargs):
"""
Callback after /version response. Continue execution of query
@@ -457,47 +347,50 @@ class HTTPClient(QtCore.QObject):
"""
if error is not False:
self._connectionError(callback)
if self._retry < self.MAX_RETRY_CONNECTION:
self._retryConnection(server=server)
return
for request, callback in self._query_waiting_connections:
if callback is not None:
self._connectionError(callback)
return
if "version" not in params or "local" not in params:
if self._retry < self.MAX_RETRY_CONNECTION:
self._retryConnection(server=server)
return
msg = "The remote server {} is not a GNS3 server".format(self.url())
log.error(msg)
if callback is not None:
callback({"message": msg}, error=True, server=self)
for request, callback in self._query_waiting_connections:
if callback is not None:
callback({"message": msg}, error=True, server=server)
self._query_waiting_connections = []
return
if params["version"] != __version__:
if params["version"].split("-")[0] != __version__.split("-")[0]:
msg = "Client version {} differs with server version {}".format(__version__, params["version"])
log.error(msg)
# Stable release
if __version_info__[3] == 0:
if callback is not None:
callback({"message": msg}, error=True, server=self)
for request, callback in self._query_waiting_connections:
if callback is not None:
callback({"message": msg}, error=True, server=server)
return
# We don't allow different major version to interact even with dev build
elif parse_version(__version__)[:2] != parse_version(params["version"])[:2]:
if callback is not None:
callback({"message": msg}, error=True, server=self)
for request, callback in self._query_waiting_connections:
if callback is not None:
callback({"message": msg}, error=True, server=server)
return
print(msg)
print("WARNING: Use a different client and server version can create bugs. Use it at your own risk.")
if params["local"] != self.isLocal():
if self.isLocal():
msg = "Running server is not a GNS3 local server (not started with --local)"
else:
msg = "Remote running server is started with --local. It is forbidden for security reasons"
log.error(msg)
if callback is not None:
callback({"message": msg}, error=True, server=self)
return
log.warning("Use a different client and server version can create bugs. Use it at your own risk.")
self._connected = True
self._retry = 0
self.connection_connected_signal.emit()
kwargs["context"] = original_context
self.executeHTTPQuery(method, path, callback, body, **kwargs)
self._version = params["version"]
for request, callback in self._query_waiting_connections:
if request:
request()
self._query_waiting_connections = []
def _addBodyToRequest(self, body, request):
"""
@@ -537,7 +430,7 @@ class HTTPClient(QtCore.QObject):
else:
return None
def addAuth(self, request):
def _addAuth(self, request):
"""
If require add basic auth header
"""
@@ -548,7 +441,7 @@ class HTTPClient(QtCore.QObject):
request.setRawHeader(b"Authorization", auth_string.encode())
return request
def executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, timeout=120, **kwargs):
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, networkManager=None, **kwargs):
"""
Call the remote server
@@ -559,71 +452,78 @@ class HTTPClient(QtCore.QObject):
:param context: Pass a context to the response callback
:param downloadProgressCallback: Callback called when received something, it can be an incomplete response
:param showProgress: Display progress to the user
:param networkManager: The network manager to use. If None use default
:param progressText: Text display to user in progress dialog. None for auto generated
:param ignoreErrors: Ignore connection error (usefull to not closing a connection when notification feed is broken)
:param server: The server where the query is executed
:param timeout: Delay in seconds before raising a timeout
:param params: Query arguments parameters
:returns: QNetworkReply
"""
try:
ip = self._http_host.rsplit('%', 1)[0]
ip = self._host.rsplit('%', 1)[0]
ipaddress.IPv6Address(ip) # remove any scope ID
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
host = "[{}]".format(ip)
except ipaddress.AddressValueError:
host = self._http_host
host = self._host
log.debug("{method} {protocol}://{host}:{port}/v1{path} {body}".format(method=method, protocol=self._scheme, host=host, port=self._http_port, path=path, body=body))
if self._user:
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}/v1{path}".format(protocol=self._scheme, user=self._user, host=host, port=self._http_port, path=path))
if params == {}:
query_string = ""
else:
url = QtCore.QUrl("{protocol}://{host}:{port}/v1{path}".format(protocol=self._scheme, host=host, port=self._http_port, path=path))
query_string = "?" + urllib.parse.urlencode(params)
log.debug("{method} {protocol}://{host}:{port}{prefix}{path} {body}{query_string}".format(method=method, protocol=self._protocol, host=host, port=self._port, path=path, body=body, prefix=prefix, query_string=query_string))
if self._user:
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}{prefix}{path}{query_string}".format(protocol=self._protocol, user=self._user, host=host, port=self._port, path=path, prefix=prefix, query_string=query_string))
else:
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{path}{query_string}".format(protocol=self._protocol, host=host, port=self._port, path=path, prefix=prefix, query_string=query_string))
request = self._request(url)
request = self.addAuth(request)
request = self._addAuth(request)
request.setRawHeader(b"User-Agent", "GNS3 QT Client v{version}".format(version=__version__).encode())
# By default QT doesn't support GET with body even if it's in the RFC that's why we need to use sendCustomRequest
body = self._addBodyToRequest(body, request)
response = self._network_manager.sendCustomRequest(request, method.encode(), body)
if not networkManager:
networkManager = self._network_manager
response = networkManager.sendCustomRequest(request, method.encode(), body)
context = copy.copy(context)
context["query_id"] = str(uuid.uuid4())
response.finished.connect(qpartial(self._processResponse, response, callback, context, body, ignoreErrors))
response.finished.connect(qpartial(self._processResponse, response, server, callback, context, body, ignoreErrors))
response.error.connect(qpartial(self._processError, response, server, callback, context, body, ignoreErrors))
if downloadProgressCallback is not None:
response.downloadProgress.connect(qpartial(self._processDownloadProgress, response, downloadProgressCallback, context))
response.readyRead.connect(qpartial(self._readyReadySlot, response, downloadProgressCallback, context, server))
if not sip_is_deleted(HTTPClient._progress_callback) and HTTPClient._progress_callback.progress_dialog():
request_canceled = qpartial(self._requestCanceled, response, context)
HTTPClient._progress_callback.progress_dialog().canceled.connect(request_canceled)
if showProgress:
response.uploadProgress.connect(qpartial(self.notify_progress_upload, context["query_id"]))
response.downloadProgress.connect(qpartial(self.notify_progress_download, context["query_id"]))
response.uploadProgress.connect(qpartial(self._notify_progress_upload, context["query_id"]))
response.downloadProgress.connect(qpartial(self._notify_progress_download, context["query_id"]))
# Should be the last operation otherwise we have race condition in Qt
# where query start before finishing connect to everything
self.notify_progress_start_query(context["query_id"], progressText, response)
self._notify_progress_start_query(context["query_id"], progressText, response)
if timeout is not None:
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response))
return response
def _timeoutSlot(self, response):
"""
Beware it's call for all request you need to check the status of the response
"""
# We check if we received HTTP headers
if not len(response.rawHeaderList()) > 0:
response.abort()
def _processDownloadProgress(self, response, callback, context, bytesReceived, bytesTotal):
def _readyReadySlot(self, response, callback, context, server, *args):
"""
Process a packet receive on the notification feed.
The feed can contains qpartial JSON. If we found a
part of a JSON we keep it for the next packet
"""
if response.error() != QtNetwork.QNetworkReply.NoError:
return
@@ -642,53 +542,60 @@ class HTTPClient(QtCore.QObject):
while True:
content = content.lstrip(" \r\n\t")
answer, index = json.JSONDecoder().raw_decode(content)
callback(answer, server=self, context=context)
callback(answer, server=server, context=context)
content = content[index:]
except ValueError: # Partial JSON
self._buffer[context["query_id"]] = content
else:
callback(content, server=self, context=context)
callback(content, server=server, context=context)
if HTTPClient._progress_callback and HTTPClient._progress_callback.progress_dialog():
request_canceled = qpartial(self._requestCanceled, response, context)
HTTPClient._progress_callback.progress_dialog().canceled.connect(request_canceled)
def _timeoutSlot(self, response):
"""
Beware it's call for all request you need to check the status of the response
"""
# We check if we received HTTP headers
if not sip.isdeleted(response) and response.isRunning() and not len(response.rawHeaderList()) > 0:
if not response.error() != QtNetwork.QNetworkReply.NoError:
log.warn("Timeout request {}".format(response.url().toString()))
response.abort()
def disconnect(self):
"""
Disconnect from the remote server
"""
self.connection_disconnected_signal.emit()
self.close()
def _requestCanceled(self, response, context):
if response.isRunning():
log.warn("Aborting request for {}".format(response.url()))
if response.isRunning() and not response.error() != QtNetwork.QNetworkReply.NoError:
log.warn("Aborting request for {}".format(response.url().toString()))
response.abort()
if "query_id" in context:
self.notify_progress_end_query(context["query_id"])
self._notify_progress_end_query(context["query_id"])
def _processResponse(self, response, callback, context, request_body, ignore_errors):
if request_body is not None:
request_body.close()
status = None
body = None
if "query_id" in context:
self.notify_progress_end_query(context["query_id"])
if response.error() != QtNetwork.QNetworkReply.NoError:
error_code = response.error()
def _processError(self, response, server, callback, context, request_body, ignore_errors, error_code):
if error_code != QtNetwork.QNetworkReply.NoError:
error_message = response.errorString()
if not ignore_errors:
log.info("Response error: %s (error: %d)", error_message, error_code)
log.debug("Response error: %s for %s (error: %d)", error_message, response.url().toString(), error_code)
if error_code < 200:
if not ignore_errors:
self.close()
if callback is not None:
callback({"message": error_message}, error=True, server=self, context=context)
if "query_id" in context:
self._notify_progress_end_query(context["query_id"])
if error_code < 200 or error_code == 403:
if error_code == QtNetwork.QNetworkReply.OperationCanceledError: # It's legit to cancel do not disconnect
error_message = "Operation timeout" # It's more clear than cancel, because cancel is trigger by us when we timeout
elif not ignore_errors:
self.disconnect()
if callback is not None:
callback({"message": error_message}, error=True, server=server, context=context)
return
else:
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
if status == 401:
print(error_message)
log.error(error_message)
try:
body = bytes(response.readAll()).decode("utf-8").strip("\0")
@@ -698,19 +605,34 @@ class HTTPClient(QtCore.QObject):
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
if callback is not None:
if not body or content_type != "application/json":
callback({"message": error_message}, error=True, server=self, context=context)
callback({"message": error_message}, error=True, server=server, context=context)
else:
log.debug(body)
try:
callback(json.loads(body), error=True, server=self, context=context)
callback(json.loads(body), error=True, server=server, context=context)
except ValueError:
# It happens when an antivirus catch the communication and send is error page without changing the Content Type
callback({"message": error_message}, error=True, server=self, context=context)
else:
callback({"message": error_message}, error=True, server=server, context=context)
else:
# Because nothing is configured to handle the error we display it to the user
try:
log.error(json.loads(body)["message"])
except (ValueError, KeyError):
log.error(error_message)
def _processResponse(self, response, server, callback, context, request_body, ignore_errors):
if request_body is not None:
request_body.close()
if "query_id" in context:
self._notify_progress_end_query(context["query_id"])
if response.error() == QtNetwork.QNetworkReply.NoError:
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
log.debug("Decoding response from {} response {}".format(response.url().toString(), status))
try:
body = bytes(response.readAll()).decode("utf-8").strip("\0")
raw_body = bytes(response.readAll())
body = raw_body.decode("utf-8").strip("\0")
# Some time anti-virus intercept our query and reply with garbage content
except UnicodeDecodeError:
body = None
@@ -722,47 +644,57 @@ class HTTPClient(QtCore.QObject):
params = {}
if callback is not None:
if status >= 400:
callback(params, error=True, server=self, context=context)
callback(params, error=True, server=server, context=context)
else:
callback(params, server=self, context=context, raw_body=body)
# response.deleteLater()
if status == 400:
try:
params = json.loads(body)
e = HttpBadRequest(body)
e.fingerprint = params["path"]
# If something goes wrong for a any reason just raise the bad request
except Exception:
e = HttpBadRequest(body)
raise e
callback(params, server=server, context=context, raw_body=raw_body)
if status == 400:
try:
params = json.loads(body)
e = HttpBadRequest(body)
e.fingerprint = params["path"]
# If something goes wrong for a any reason just raise the bad request
except Exception:
e = HttpBadRequest(body)
raise e
def dump(self):
def getSynchronous(self, endpoint, timeout=2):
"""
Returns a representation of this server.
:returns: dictionary
Synchronous check if a server is running
:returns: Tuple (Status code, json of anwser). 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)
server = self.settings()
server["id"] = self._id
server["local"] = self._local
server["vm"] = self._gns3_vm
#server["cloud"] = self._cloud
if "user" in server and self._local:
del server["user"]
if "password" in server:
del server["password"]
if server["protocol"] == "https":
server["accept_insecure_certificate"] = self._accept_insecure_certificate
return server
def systemUsage(self):
"""
Get information about current system usage
:returns: None or dict
"""
return self._usage
def setSystemUsage(self, usage):
self._usage = usage
self.system_usage_updated_signal.emit()
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()
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.warn("Invalid local server url: {}".format(e))
return 0, None
except urllib.error.URLError:
# Connection refused. It's a normal behavior if server is not started
return 0, None
except urllib.error.HTTPError as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return e.code, None
except (OSError, http.client.BadStatusLine, ValueError) as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return 0, None

View File

@@ -16,11 +16,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import copy
import pathlib
import glob
from gns3.servers import Servers
from gns3.qt import QtWidgets
from gns3.local_server_config import LocalServerConfig
from gns3.settings import LOCAL_SERVER_SETTINGS
from gns3.controller import Controller
from gns3.utils.file_copy_worker import FileCopyWorker
from gns3.utils.progress_dialog import ProgressDialog
@@ -31,7 +33,7 @@ class ImageManager:
# Remember if we already ask the user about this image for this server
self._asked_for_this_image = {}
def askCopyUploadImage(self, parent, path, server, vm_type):
def askCopyUploadImage(self, parent, path, server, node_type):
"""
Ask user for copying the image to the default directory or upload
it to remote server.
@@ -39,14 +41,14 @@ class ImageManager:
:param parent: Parent window
:param path: File path on computer
:param server: The server where the images should be located
:param vm_type: Remote upload endpoint
:param node_type: Remote upload endpoint
:returns path: Final path
"""
if server and not server.isLocal():
return self._uploadImageToRemoteServer(path, server, vm_type)
if (server and server != "local") or Controller.instance().isRemote():
return self._uploadImageToRemoteServer(path, server, node_type)
else:
destination_directory = self.getDirectoryForType(vm_type)
destination_directory = self.getDirectoryForType(node_type)
if os.path.normpath(os.path.dirname(path)) != destination_directory:
# the IOS image is not in the default images directory
reply = QtWidgets.QMessageBox.question(parent,
@@ -73,56 +75,30 @@ class ImageManager:
path = destination_path
return path
def _uploadImageToRemoteServer(self, path, server, vm_type):
def _uploadImageToRemoteServer(self, path, server, node_type):
"""
Upload image to remote server
:param path: File path on computer
:param server: The server where the images should be located
:param vm_type: Image vm_type
:param node_type: Image node_type
:returns path: Final path
"""
if vm_type == 'QEMU':
upload_endpoint = '/qemu/vms'
elif vm_type == 'IOU':
upload_endpoint = '/iou/vms'
elif vm_type == 'DYNAMIPS':
upload_endpoint = '/dynamips/vms'
if node_type == 'QEMU':
upload_endpoint = '/qemu/images'
elif node_type == 'IOU':
upload_endpoint = '/iou/images'
elif node_type == 'DYNAMIPS':
upload_endpoint = '/dynamips/images'
else:
raise Exception('Invalid image vm_type')
raise Exception('Invalid node type')
filename = self._getRelativeImagePath(path, vm_type).replace("\\", "/")
server.post('{}/{}'.format(upload_endpoint, filename), None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
filename = self._getRelativeImagePath(path, node_type).replace("\\", "/")
Controller.instance().postCompute('{}/{}'.format(upload_endpoint, filename), server, None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
return filename
def addMissingImage(self, filename, server, vm_type):
"""
Add a missing image to the queue of images require to be upload on remote server
:param filename: Filename of the image
:param server: Server where image should be uploaded
:param vm_type: Type of the image
"""
if self._asked_for_this_image.setdefault(server.id(), {}).setdefault(filename, False):
return
self._asked_for_this_image[server.id()][filename] = True
if server.isLocal():
return
path = os.path.join(self.getDirectoryForType(vm_type), filename)
if os.path.exists(path):
if self._askForUploadMissingImage(filename, server):
if filename.endswith(".vmdk"):
# A vmdk file could be split in multiple vmdk file
search = glob.escape(path).replace(".vmdk", "-*.vmdk")
for file in glob.glob(search):
self._uploadImageToRemoteServer(file, server, vm_type)
self._uploadImageToRemoteServer(path, server, vm_type)
del self._asked_for_this_image[server.id()][filename]
def _askForUploadMissingImage(self, filename, server):
from gns3.main_window import MainWindow
parent = MainWindow.instance()
@@ -135,20 +111,20 @@ class ImageManager:
return True
return False
def _getRelativeImagePath(self, path, vm_type):
def _getRelativeImagePath(self, path, node_type):
"""
Get a path relative to images directory path
or just filename if the path is not located inside
image directory
:param path: file path
:param vm_type: Type of vm
:param node_type: Type of vm
:return: file path
"""
if not path:
return ""
img_directory = self.getDirectoryForType(vm_type)
img_directory = self.getDirectoryForType(node_type)
path = os.path.abspath(path)
if os.path.commonprefix([img_directory, path]) == img_directory:
return os.path.relpath(path, img_directory)
@@ -161,19 +137,19 @@ class ImageManager:
:returns: path to the default images directory
"""
return Servers.instance().localServerSettings()['images_path']
return copy.copy(LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)['images_path'])
def getDirectoryForType(self, vm_type):
def getDirectoryForType(self, node_type):
"""
Return the path of local directory of the images
of a specific vm_type
of a specific node_type
:param vm_type: Type of vm
:param node_type: Type of vm
"""
if vm_type == 'DYNAMIPS':
if node_type == 'DYNAMIPS':
return os.path.join(self.getDirectory(), 'IOS')
else:
return os.path.join(self.getDirectory(), vm_type)
return os.path.join(self.getDirectory(), node_type)
@staticmethod
def instance():

View File

@@ -1,189 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2015 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 shutil
import json
import os
from datetime import datetime
try:
from gns3.qt import QtGui, QtWidgets
except ImportError:
raise SystemExit("Can't import Qt modules: Qt and/or PyQt is probably not installed correctly...")
import logging
log = logging.getLogger(__name__)
from gns3.version import __version__
from gns3.local_config import LocalConfig
from gns3.ui.iouvm_converter_wizard_ui import Ui_IOUVMConverterWizard
class IOUVMConverterWizard(QtWidgets.QWizard, Ui_IOUVMConverterWizard):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
# set the window icon
self.setWindowIcon(QtGui.QIcon(":/images/gns3.ico")) # this info is necessary for QSettings
config = self._loadConfig()
self.uiPushButtonBrowse.clicked.connect(self._browseTopologiesSlot)
self.uiLineEditTopologiesPath.setText(config['Servers']['local_server']['projects_path'])
def _browseTopologiesSlot(self):
path = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select a directory')
self.uiLineEditTopologiesPath.setText(path)
def validateCurrentPage(self):
"""
Validates the settings.
"""
if self.currentPage() == self.uiWizardPageIOURCCheck:
return self._checkIOURC()
elif self.currentPage() == self.uiWizardUpdateConfiguration:
return self._updateConfig()
elif self.currentPage() == self.uiWizardPagePatchTopologies:
return self._patchTopologies()
return True
def _checkIOURC(self):
"""
Validate if the IOURC contain an entry for the IOUVM
"""
config = self._loadConfig()
iourc_path = config.get("IOU", {}).get("iourc_path", "")
if len(iourc_path) == 0:
QtWidgets.QMessageBox.critical(self, "Error", "The IOURC is not configured")
return False
try:
with open(iourc_path) as f:
if 'gns3vm' not in f.read():
QtWidgets.QMessageBox.critical(self, "Error", "The gns3vm doesn't exist in your iourc file".format(iourc_path))
except OSError:
QtWidgets.QMessageBox.critical(self, "Error", "IOURC file {} doesn't exist or not accessible".format(iourc_path))
return True
def _updateConfig(self):
"""
Update the config file to use the GNS3 VM instead of IOU VM
"""
config = self._loadConfig()
if "devices" in config["IOU"]:
for device in config["IOU"]["devices"]:
device["path"] = os.path.basename(device["path"])
device["server"] = "vm"
config["Servers"]["remote_servers"] = []
self._writeConfig(config)
return True
def _patchTopologies(self):
"""
Patch topologies to use the GNS3 VM
"""
path = self.uiLineEditTopologiesPath.text()
try:
for (dirpath, dirnames, filenames) in os.walk(path):
for filename in filenames:
if filename.endswith(".gns3"):
self._patchTopology(os.path.join(dirpath, filename))
except OSError as e:
QtWidgets.QMessageBox.critical(self, "Error", "Can't open {}: {}".format(path, str(e)))
return False
return True
def _patchTopology(self, path):
"""
Path a specific topology
"""
try:
shutil.copy(path, "{}.{}.backup".format(path, datetime.now().isoformat()))
with open(path) as f:
topo = json.load(f)
if "topology" in topo and "servers" in topo["topology"]:
for server in topo["topology"]["servers"]:
if server["local"] is False:
server["vm"] = True
with open(path, 'w+') as f:
topo = json.dump(topo, f)
except OSError as e:
QtWidgets.QMessageBox.critical(self, "Error", "Can't open {}: {}".format(path, str(e)))
def _loadConfig(self):
with open(self._configurationFile()) as f:
return json.load(f)
def _writeConfig(self, config):
shutil.copy(self._configurationFile(), "{}.{}.backup".format(self._configurationFile(), datetime.now().isoformat()))
with open(self._configurationFile(), 'w+') as f:
json.dump(config, f, indent=4)
def _configurationFile(self):
if sys.platform.startswith("win"):
filename = "gns3_gui.ini"
else:
filename = "gns3_gui.conf"
directory = LocalConfig.configDirectory()
return os.path.join(directory, filename)
def main():
app = QtWidgets.QApplication(sys.argv)
app.setOrganizationName("GNS3")
app.setOrganizationDomain("gns3.net")
app.setApplicationName("GNS3")
app.setApplicationVersion(__version__)
# We force a full garbage collect before exit
# for unknow reason otherwise Qt Segfault on OSX in some
# conditions
import gc
gc.collect()
# Manage Ctrl + C or kill command
def sigint_handler(*args):
log.info("Signal received exiting the application")
app.closeAllWindows()
# signal.signal(signal.SIGINT, sigint_handler)
# signal.signal(signal.SIGTERM, sigint_handler)
mainwindow = IOUVMConverterWizard()
mainwindow.show()
exit_code = mainwindow.exec_()
# We force a full garbage collect before exit
# for unknow reason otherwise Qt Segfault on OSX in some
# conditions
import gc
gc.collect()
sys.exit(exit_code)
if __name__ == '__main__':
main()

217
gns3/items/drawing_item.py Normal file
View File

@@ -0,0 +1,217 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from ..qt import QtCore, QtWidgets, qslot
import uuid
import logging
import binascii
log = logging.getLogger(__name__)
class DrawingItem:
show_layer = False
"""
Base class for non emulation item
"""
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, rotation=0, **kws):
self._id = drawing_id
if self._id is None:
self._id = str(uuid.uuid4())
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
from ..main_window import MainWindow
self._graphics_view = MainWindow.instance().uiGraphicsView
self._main_window = MainWindow.instance()
self._project = project
# Store a hash of the SVG to avoid him
# to be send if he doesn't change
self._hash_svg = None
if pos:
self.setPos(pos)
if z:
self.setZValue(z)
if rotation:
self.setRotation(rotation)
def drawing_id(self):
return self._id
def create(self):
if self._project:
self._project.post("/drawings", self._createDrawingCallback, body=self.__json__())
def _createDrawingCallback(self, result, error=False, **kwargs):
"""
Callback for create.
:param result: server response
:param error: indicates an error (boolean)
:returns: Boolean success or not
"""
if error:
log.error("Error while setting up drawing: {}".format(result["message"]))
return False
self._id = result["drawing_id"]
self.updateDrawingCallback(result)
def updateDrawing(self):
if self._id:
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__())
@qslot
def updateDrawingCallback(self, result, error=False, **kwargs):
"""
Callback for update.
:param result: server response
:param error: indicates an error (boolean)
:returns: Boolean success or not
"""
if error:
log.error("Error while setting up drawing: {}".format(result["message"]))
return False
self.setPos(QtCore.QPoint(result["x"], result["y"]))
self.setZValue(result["z"])
self.setRotation(result["rotation"])
if "svg" in result:
self.fromSvg(result["svg"])
def handleKeyPressEvent(self, event):
"""
Handles all key press events
:param event: QKeyEvent
:return: Boolean True the event has been captured
"""
key = event.key()
modifiers = event.modifiers()
if key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
if self.rotation() == 0:
self.setRotation(359)
else:
self.setRotation(self.rotation() - 1)
return True
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
if self.rotation() < 360.0:
self.setRotation(self.rotation() + 1)
return True
return False
def keyPressEvent(self, event):
"""
Handles all key press events
:param event: QKeyEvent
"""
if not self.handleKeyPressEvent(event):
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
def __json__(self):
data = {
"drawing_id": self._id,
"x": int(self.pos().x()),
"y": int(self.pos().y()),
"z": int(self.zValue()),
"rotation": int(self.rotation())
}
svg = self.toSvg()
hash_svg = binascii.crc32(svg.encode())
if hash_svg != self._hash_svg:
data["svg"] = svg
self._hash_svg = hash_svg
return data
def setZValue(self, value):
"""
Sets a new Z value.
:param value: Z value
"""
QtWidgets.QGraphicsItem.setZValue(self, value)
if self.zValue() < 0:
self.setFlag(self.ItemIsSelectable, False)
self.setFlag(self.ItemIsMovable, False)
else:
self.setFlag(self.ItemIsSelectable, True)
self.setFlag(self.ItemIsMovable, True)
def delete(self, skip_controller=False):
"""
Deletes this drawing.
:param skip_controller: Do not replicate change on the controller (usefull when it's already deleted on controller)
"""
self.scene().removeItem(self)
from ..topology import Topology
Topology.instance().removeDrawing(self)
if self._id and not skip_controller:
self._project.delete("/drawings/" + self._id, None, body=self.__json__())
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
GRID_SIZE = 75
mid_x = self.boundingRect().width() / 2
tmp_x = (GRID_SIZE * round((self.x() + mid_x) / GRID_SIZE)) - mid_x
mid_y = self.boundingRect().height() / 2
tmp_y = (GRID_SIZE * round((self.y() + mid_y) / GRID_SIZE)) - mid_y
if tmp_x != self.x() and tmp_y != self.y():
self.setPos(tmp_x, tmp_y)
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
if not value:
self.updateDrawing()
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
def updateNode(self):
self.updateDrawing()
def drawLayerInfo(self, painter):
"""
Draws the layer position.
:param painter: QPainter instance
"""
if self.show_layer is False:
return
brect = self.boundingRect()
# don't draw anything if the object is too small
if brect.width() < 20 or brect.height() < 20:
return
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
painter.setBrush(QtCore.Qt.red)
painter.setPen(QtCore.Qt.red)
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
painter.setPen(QtCore.Qt.black)
zval = str(int(self.zValue()))
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)

View File

@@ -19,6 +19,9 @@
Graphical representation of an ellipse on the QGraphicsScene.
"""
import math
import xml.etree.ElementTree as ET
from ..qt import QtCore, QtGui, QtWidgets
from .shape_item import ShapeItem
@@ -29,25 +32,9 @@ class EllipseItem(QtWidgets.QGraphicsEllipseItem, ShapeItem):
Class to draw an ellipse on the scene.
"""
def __init__(self, pos=None, width=200, height=200):
def __init__(self, width=200, height=200, **kws):
super().__init__(width=width, height=height, **kws)
super().__init__()
self.setRect(0, 0, width, height)
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.DashLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
self.setPen(pen)
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
self.setBrush(brush)
if pos:
self.setPos(pos)
def delete(self):
"""
Deletes this ellipse.
"""
self.scene().removeItem(self)
from ..topology import Topology
Topology.instance().removeEllipse(self)
def paint(self, painter, option, widget=None):
"""
@@ -61,16 +48,21 @@ class EllipseItem(QtWidgets.QGraphicsEllipseItem, ShapeItem):
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def duplicate(self):
def toSvg(self):
"""
Duplicates this ellipse item.
:return: EllipseItem instance
Return an SVG version of the shape
"""
svg = ET.Element("svg")
svg.set("width", str(self.rect().width()))
svg.set("height", str(self.rect().height()))
ellipse = ET.SubElement(svg, "ellipse")
ellipse.set("cx", str(math.floor(self.rect().width() / 2)))
ellipse.set("rx", str(math.ceil(self.rect().width() / 2)))
ellipse.set("cy", str(math.floor(self.rect().height() / 2)))
ellipse.set("ry", str(math.ceil(self.rect().height() / 2)))
ellipse = self._styleSvg(ellipse)
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
ellipse_item = EllipseItem(QtCore.QPointF(self.x() + 20, self.y() + 20), self.rect().width(), self.rect().height())
ellipse_item.setPen(self.pen())
ellipse_item.setBrush(self.brush())
ellipse_item.setZValue(self.zValue())
ellipse_item.setRotation(self.rotation())
return ellipse_item

View File

@@ -36,12 +36,11 @@ class EthernetLinkItem(LinkItem):
:param destination_port: destination Port instance
:param link: Link instance (contains back-end stuff for this link)
:param adding_flag: indicates if this link is being added (no destination yet)
:param multilink: used to draw multiple link between the same source and destination
"""
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag, multilink)
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag)
self._source_collision_offset = 0.0
self._destination_collision_offset = 0.0
@@ -116,13 +115,16 @@ class EthernetLinkItem(LinkItem):
if self._source_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
elif self._source_port.status() == Port.suspended:
# port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
point1 = QtCore.QPointF(self.source + self.edge_offset) + QtCore.QPointF((self.dx * self._source_collision_offset) / self.length, (self.dy * self._source_collision_offset) / self.length)
# avoid any collision of the status point with the source node
@@ -137,22 +139,16 @@ class EthernetLinkItem(LinkItem):
self._source_collision_offset -= 10
source_port_label = self._source_port.label()
if source_port_label is None:
source_port_label = NoteItem(self._source_item)
source_port_label.setPlainText(self._source_port.shortName())
source_port_label.setPos(self.mapToItem(self._source_item, point1))
self._source_port.setLabel(source_port_label)
if self._draw_port_labels:
if source_port_label is None:
source_port_label = NoteItem(self._source_item)
if not self._source_port.isStub():
source_port_name = self._source_port.name().replace(self._source_port.longNameType(),
self._source_port.shortNameType())
else:
source_port_name = self._source_port.name()
source_port_label.setPlainText(source_port_name)
source_port_label.setPos(self.mapToItem(self._source_item, point1))
self._source_port.setLabel(source_port_label)
elif source_port_label and not source_port_label.isVisible():
source_port_label.show()
elif source_port_label:
source_port_label.show()
else:
source_port_label.hide()
painter.drawPoint(point1)
@@ -160,13 +156,16 @@ class EthernetLinkItem(LinkItem):
if self._destination_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
elif self._destination_port.status() == Port.suspended:
# port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
point2 = QtCore.QPointF(self.destination - self.edge_offset) - QtCore.QPointF((self.dx * self._destination_collision_offset) / self.length, (self.dy * self._destination_collision_offset) / self.length)
# avoid any collision of the status point with the destination node
@@ -181,24 +180,18 @@ class EthernetLinkItem(LinkItem):
self._destination_collision_offset -= 10
destination_port_label = self._destination_port.label()
if destination_port_label is None:
destination_port_label = NoteItem(self._destination_item)
destination_port_label.setPlainText(self._destination_port.shortName())
destination_port_label.setPos(self.mapToItem(self._destination_item, point2))
self._destination_port.setLabel(destination_port_label)
if self._draw_port_labels:
if destination_port_label is None:
destination_port_label = NoteItem(self._destination_item)
if not self._destination_port.isStub():
destination_port_name = self._destination_port.name().replace(self._destination_port.longNameType(),
self._destination_port.shortNameType())
else:
destination_port_name = self._destination_port.name()
destination_port_label.setPlainText(destination_port_name)
destination_port_label.setPos(self.mapToItem(self._destination_item, point2))
self._destination_port.setLabel(destination_port_label)
elif destination_port_label and not destination_port_label.isVisible():
destination_port_label.show()
elif destination_port_label:
destination_port_label.show()
else:
destination_port_label.hide()
painter.drawPoint(point2)
self._drawCaptureSymbol()
self._drawCaptureSymbol()

View File

@@ -19,41 +19,38 @@
Graphical representation of an image on the QGraphicsScene.
"""
from ..qt import QtWidgets, QtCore
from ..qt import QtSvg
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from .drawing_item import DrawingItem
class ImageItem():
class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
"""
Class to insert an image on the scene.
"""
show_layer = False
def __init__(self, image_path=None, pos=None, svg=None, **kws):
def __init__(self, image_path, pos=None):
self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)
self._image_path = image_path
# Because we call the Qt C++ code we need to handle the case of pos is None otherwise we will get a conversion error
if pos:
self.setPos(pos)
super().__init__(pos=pos, **kws)
else:
super().__init__(**kws)
def filePath(self):
"""
Return image file
"""
return self._image_path
if self._image_path:
renderer = QImageSvgRenderer(image_path)
self.setSharedRenderer(renderer)
def delete(self):
"""
Deletes this image item.
"""
# By default center the image
if pos is None:
x = self.pos().x() - (self.boundingRect().width() / 2)
y = self.pos().y() - (self.boundingRect().height() / 2)
self.setPos(x, y)
self.scene().removeItem(self)
from ..topology import Topology
try:
Topology.instance().removeImage(self)
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "Image", "Cannot delete the image: {}".format(str(e)))
if svg:
svg = self.fromSvg(svg)
def paint(self, painter, option, widget=None):
"""
@@ -65,68 +62,14 @@ class ImageItem():
"""
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
if self.show_layer is False:
return
def fromSvg(self, svg):
renderer = QImageSvgRenderer(svg)
self.setSharedRenderer(renderer)
brect = self.boundingRect()
# don't draw anything if the object is too small
if brect.width() < 20 or brect.height() < 20:
return
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
painter.setBrush(QtCore.Qt.red)
painter.setPen(QtCore.Qt.red)
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
painter.setPen(QtCore.Qt.black)
zval = str(int(self.zValue()))
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
def setZValue(self, value):
def toSvg(self):
"""
Sets a new Z value.
:param value: Z value
Return an SVG version of the shape
"""
super().setZValue(value)
if self.zValue() < 0:
self.setFlag(self.ItemIsSelectable, False)
self.setFlag(self.ItemIsMovable, False)
else:
self.setFlag(self.ItemIsSelectable, True)
self.setFlag(self.ItemIsMovable, True)
def dump(self):
"""
Returns a representation of this image item.
:returns: dictionary
"""
image_info = {"path": self._image_path,
"x": self.x(),
"y": self.y()}
if self.zValue() != 0:
image_info["z"] = self.zValue()
return image_info
def load(self, image_info):
"""
Loads an image representation
(from a topology file).
:param image_info: representation of the image item (dictionary)
"""
# load mandatory properties
x = image_info["x"]
y = image_info["y"]
self.setPos(x, y)
# load optional properties
z = image_info.get("z")
if z is not None:
self.setZValue(z)
return self.renderer().svg()

View File

@@ -21,11 +21,9 @@ Link items are graphical representation of a link on the QGraphicsScene
"""
import math
import struct
import sys
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
from ..node import Node
from ..packet_capture import PacketCapture
class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
@@ -51,16 +49,16 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
:param destination_port: destination Port instance
:param link: Link instance (contains back-end stuff for this link)
:param adding_flag: indicates if this link is being added (no destination yet)
:param multilink: used to draw multiple link between the same source and destination
"""
_draw_port_labels = False
delete_link_item_signal = QtCore.pyqtSignal(str)
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
super().__init__()
self.setAcceptHoverEvents(True)
self.setZValue(-1)
self.setZValue(-0.5)
self._link = None
from ..main_window import MainWindow
@@ -77,10 +75,6 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
# default pen size
self._pen_width = 2.0
# indicates the link position when there are multiple links
# between the same source and destination
self._multilink = multilink
# source & destination items and ports
self._source_item = source_item
self._destination_item = destination_item
@@ -96,6 +90,8 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
if not self._adding_flag:
# there is a destination
self._link = link
self._link.updated_link_signal.connect(self._drawCaptureSymbol)
self._link.delete_link_signal.connect(self._linkDeletedSlot)
self.setFlag(self.ItemIsFocusable)
source_item.addLink(self)
destination_item.addLink(self)
@@ -107,20 +103,10 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self.adjust()
def delete(self):
"""
Delete this link
"""
if not self._source_port.isHotPluggable() and self._source_item.node().status() == Node.started:
self._source_item.node().stop()
QtWidgets.QMessageBox.critical(self._main_window, "Connection", "{} has been stopped because it doesn't support hot unlink.".format(self._source_item.node().name()))
if not self._destination_port.isHotPluggable() and self._destination_item.node().status() == Node.started:
self._destination_item.node().stop()
QtWidgets.QMessageBox.critical(self._main_window, "Connection", "{} has been stopped because it doesn't support hot unlink.".format(self._destination_item.node().name()))
@qslot
def _linkDeletedSlot(self, link_id, *args):
# first delete the port labels if any
if self._source_port.label():
self._source_port.label().setParentItem(None)
self.scene().removeItem(self._source_port.label())
@@ -128,11 +114,15 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self._destination_port.label().setParentItem(None)
self.scene().removeItem(self._destination_port.label())
self._source_item.removeLink(self)
self._destination_item.removeLink(self)
if self.scene():
if self in self.scene().items():
self.scene().removeItem(self)
def delete(self):
"""
Delete this link
"""
self._link.deleteLink()
if self in self.scene().items():
self.scene().removeItem(self)
def link(self):
"""
@@ -212,14 +202,14 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
:param menu: QMenu instance
"""
if not self._source_port.capturing() or not self._destination_port.capturing():
if not self._link.capturing():
# start capture
start_capture_action = QtWidgets.QAction("Start capture", menu)
start_capture_action.setIcon(QtGui.QIcon(':/icons/capture-start.svg'))
start_capture_action.triggered.connect(self._startCaptureActionSlot)
menu.addAction(start_capture_action)
if self._source_port.capturing() or self._destination_port.capturing():
if self._link.capturing():
# stop capture
stop_capture_action = QtWidgets.QAction("Stop capture", menu)
stop_capture_action.setIcon(QtGui.QIcon(':/icons/capture-stop.svg'))
@@ -232,8 +222,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
start_wireshark_action.triggered.connect(self._startWiresharkActionSlot)
menu.addAction(start_wireshark_action)
if sys.platform.startswith("win") and struct.calcsize("P") * 8 == 64:
# Windows 64-bit only (Solarwinds RTV limitation).
if PacketCapture.instance().packetAnalyzerAvailable():
analyze_action = QtWidgets.QAction("Analyze capture", menu)
analyze_action.setIcon(QtGui.QIcon(':/icons/rtv.png'))
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
@@ -295,26 +284,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
contextual menu.
"""
ports = {}
if self._source_port.packetCaptureSupported() and not self._source_port.capturing():
for dlt_name, dlt in self._source_port.dataLinkTypes().items():
port = "{} port {} ({} encapsulation: {})".format(self._source_item.node().name(), self._source_port.name(), dlt_name, dlt)
ports[port] = [self._source_item.node(), self._source_port, dlt]
if self._destination_port.packetCaptureSupported() and not self._destination_port.capturing():
for dlt_name, dlt in self._destination_port.dataLinkTypes().items():
port = "{} port {} ({} encapsulation: {})".format(self._destination_item.node().name(), self._destination_port.name(), dlt_name, dlt)
ports[port] = [self._destination_item.node(), self._destination_port, dlt]
if not ports:
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Packet capture is not supported on this link")
return
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", list(ports.keys()), 0, False)
if ok:
if selection in ports:
node, port, dlt = ports[selection]
node.startPacketCapture(port, port.captureFileName(node.name()), dlt)
PacketCapture.instance().startCapture(self._link)
def _stopCaptureActionSlot(self):
"""
@@ -322,21 +292,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
contextual menu.
"""
if self._source_port.capturing() and self._destination_port.capturing():
ports = {}
source_port = "{} port {}".format(self._source_item.node().name(), self._source_port.name())
ports[source_port] = [self._source_item.node(), self._source_port]
destination_port = "{} port {}".format(self._destination_item.node().name(), self._destination_port.name())
ports[destination_port] = [self._destination_item.node(), self._destination_port]
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", list(ports.keys()), 0, False)
if ok:
if selection in ports:
node, port = ports[selection]
node.stopPacketCapture(port)
elif self._source_port.capturing():
self._source_item.node().stopPacketCapture(self._source_port)
elif self._destination_port.capturing():
self._destination_item.node().stopPacketCapture(self._destination_port)
PacketCapture.instance().stopCapture(self._link)
def _startWiresharkActionSlot(self):
"""
@@ -344,22 +300,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
contextual menu.
"""
try:
if self._source_port.capturing() and self._destination_port.capturing():
ports = ["{} port {}".format(self._source_item.node().name(), self._source_port.name()),
"{} port {}".format(self._destination_item.node().name(), self._destination_port.name())]
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", ports, 0, False)
if ok:
if selection.endswith(self._source_port.name()):
self._source_port.startPacketCaptureReader(self._source_item.node().name())
else:
self._destination_port.startPacketCaptureReader(self._destination_item.node().name())
elif self._source_port.capturing():
self._source_port.startPacketCaptureReader(self._source_item.node().name())
elif self._destination_port.capturing():
self._destination_port.startPacketCaptureReader(self._destination_item.node().name())
except OSError as e:
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Cannot start Wireshark: {}".format(e))
PacketCapture.instance().startPacketCaptureReader(self._link)
def _analyzeCaptureActionSlot(self):
"""
@@ -368,19 +309,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
"""
try:
if self._source_port.capturing() and self._destination_port.capturing():
ports = ["{} port {}".format(self._source_item.node().name(), self._source_port.name()),
"{} port {}".format(self._destination_item.node().name(), self._destination_port.name())]
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Capture analyzer", "Please select a port:", ports, 0, False)
if ok:
if selection.endswith(self._source_port.name()):
self._source_port.startPacketCaptureAnalyzer()
else:
self._destination_port.startPacketCaptureAnalyzer()
elif self._source_port.capturing():
self._source_port.startPacketCaptureAnalyzer()
elif self._destination_port.capturing():
self._destination_port.startPacketCaptureAnalyzer()
PacketCapture.instance().startPacketCaptureAnalyzer(self._link)
except OSError as e:
QtWidgets.QMessageBox.critical(self._main_window, "Capture analyzer", "Cannot start the packet capture analyzer program: {}".format(e))
@@ -415,6 +344,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self.setHovered(False)
@qslot
def adjust(self):
"""
Computes the source point and destination point.
@@ -424,7 +354,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
# links must always be below node items on the scene
if not self._adding_flag:
min_zvalue = min([self._source_item.zValue(), self._destination_item.zValue()])
self.setZValue(min_zvalue - 1)
self.setZValue(min_zvalue - 0.5)
self.prepareGeometryChange()
source_rect = self._source_item.boundingRect()
@@ -442,15 +372,54 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
# compute the length of the line
self.length = math.sqrt(self.dx * self.dx + self.dy * self.dy)
multilink = self._computeMultiLink()
# multi-link management
if not self._adding_flag and self._multilink and self.length:
if not self._adding_flag and multilink and self.length:
angle = math.radians(90)
self.dxrot = math.cos(angle) * self.dx - math.sin(angle) * self.dy
self.dyrot = math.sin(angle) * self.dx + math.cos(angle) * self.dy
offset = QtCore.QPointF((self.dxrot * (self._multilink * 5)) / self.length, (self.dyrot * (self._multilink * 5)) / self.length)
offset = QtCore.QPointF((self.dxrot * (multilink * 5)) / self.length, (self.dyrot * (multilink * 5)) / self.length)
self.source = QtCore.QPointF(self.source + offset)
self.destination = QtCore.QPointF(self.destination + offset)
def _computeMultiLink(self):
# Multi-link management
#
# multi is the offset of the link
# +------+ multi = -1 Link 2 +-------+
# | +-----------------------------+ |
# | R1 | | R2 |
# | | multi = 0 Link 1 | |
# | +-----------------------------+ |
# | | multi = 1 Link 3 | |
# +------+-----------------------------+-------+
if self._source_item == self._destination_item:
multi = 0
elif not hasattr(self._destination_item, "node"): # Could be temporary a qpointf during link creation
multi = 0
else:
multi = 0
link_items = self._source_item.links()
for link_item in link_items:
if link_item == self:
break
if link_item.destinationItem().node().id() == self._destination_item.node().id():
multi += 1
if link_item.sourceItem().node().id() == self._destination_item.node().id():
multi += 1
# MAX 7 links on the scene between 2 nodes
if multi > 7:
multi = 0
# Pair item represent the bottom links
elif multi % 2 == 0:
multi = multi // 2
else:
multi = -multi // 2
return multi
def setMousePoint(self, scene_point):
"""
Sets new mouse point coordinates.
@@ -463,13 +432,14 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self.adjust()
self.update()
def _drawCaptureSymbol(self):
@qslot
def _drawCaptureSymbol(self, *args):
"""
Draws a capture symbol in the middle of the link to indicate a capture is active.
"""
if not self._adding_flag:
if (self._source_port.capturing() or self._destination_port.capturing()) and self.length >= 150:
if self._link.capturing() and self.length >= 150:
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
if self._capturing_item is None:
self._capturing_item = SvgCaptureItem(':/icons/inspect.svg', self)

View File

@@ -19,14 +19,20 @@
Graphical representation of a node on the QGraphicsScene.
"""
from ..qt import QtCore, QtGui, QtWidgets
import sip
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from .note_item import NoteItem
from ..symbol import Symbol
from ..controller import Controller
import logging
log = logging.getLogger(__name__)
class NodeItem():
class NodeItem(QtSvg.QGraphicsSvgItem):
"""
Node for the scene.
@@ -35,25 +41,35 @@ class NodeItem():
"""
show_layer = False
GRID_SIZE = 75
def __init__(self, node):
super().__init__()
# attached node
self._node = node
# link items connected to this node item.
self._links = []
self._symbol = None
# says if the attached node has been initialized
# by the server.
self._initialized = False
# node label
self._node_label = None
# link items connected to this node item.
self._links = []
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
self.setZValue(self._node.z())
# Temporary symbol during loading
renderer = QImageSvgRenderer(":/icons/reload.svg")
renderer.setObjectName("symbol_loading")
self.setSharedRenderer(renderer)
effect = QtWidgets.QGraphicsColorizeEffect()
effect.setColor(QtGui.QColor("black"))
effect.setStrength(0.8)
#effect = QtWidgets.QGraphicsDropShadowEffect()
# effect.setColor(QtGui.QColor("darkGray"))
# effect.setBlurRadius(0)
#effect.setOffset(3, 3)
self.setGraphicsEffect(effect)
self.graphicsEffect().setEnabled(False)
@@ -63,7 +79,6 @@ class NodeItem():
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)
self.setZValue(1)
# connect signals to know about some events
# e.g. when the node has been started, stopped or suspended etc.
@@ -73,33 +88,71 @@ class NodeItem():
node.suspended_signal.connect(self.suspendedSlot)
node.updated_signal.connect(self.updatedSlot)
node.deleted_signal.connect(self.deletedSlot)
node.delete_links_signal.connect(self.deleteLinksSlot)
node.error_signal.connect(self.errorSlot)
node.server_error_signal.connect(self.serverErrorSlot)
# used when a port has been selected from the contextual menu
self._selected_port = None
# says if the attached node has been initialized
# by the server.
self._initialized = False
# contains the last error message received
# from the server.
self._last_error = None
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()
def setUnsavedState(self):
"""
Indicates the project is in a unsaved state.
"""
if node.initialized():
self.createdSlot(node.id())
from ..main_window import MainWindow
main_window = MainWindow.instance()
main_window.setUnsavedState()
def _snapToGrid(self):
mid_x = self.boundingRect().width() / 2
x = (self.GRID_SIZE * round((self.x() + mid_x) / self.GRID_SIZE)) - mid_x
mid_y = self.boundingRect().height() / 2
y = (self.GRID_SIZE * round((self.y() + mid_y) / self.GRID_SIZE)) - mid_y
self.setPos(x, y)
def updateNode(self):
"""
Sync change to the node
"""
if self._initialized:
self._node.setGraphics(self)
@qslot
def setSymbol(self, symbol):
"""
:param symbol: Change the symbol path
"""
# create renderer using symbols path/resource
if symbol is None:
symbol = self._node.defaultSymbol()
if self._symbol != symbol:
self._symbol = symbol
# Temporary symbol during loading
renderer = QImageSvgRenderer(":/icons/reload.svg")
renderer.setObjectName("symbol_loading")
self.setSharedRenderer(renderer)
Controller.instance().getStatic(Symbol(symbol_id=symbol).url(), self._symbolLoadedCallback)
def symbol(self):
return self._symbol
@qslot
def _symbolLoadedCallback(self, path, *args):
renderer = QImageSvgRenderer(path, fallback=":/icons/cancel.svg")
renderer.setObjectName(path)
self.setSharedRenderer(renderer)
if self._node.settings().get("symbol") != self._symbol:
self.updateNode()
if not self._initialized:
self._showLabel()
self._initialized = True
self.updateNode()
def node(self):
"""
@@ -110,27 +163,44 @@ class NodeItem():
return self._node
def addLink(self, link):
def setPos(self, *args):
super().setPos(*args)
self._node.setSettingValue("x", int(self.x()))
self._node.setSettingValue("y", int(self.y()))
@qslot
def addLink(self, link_item, *args):
"""
Adds a link items to this node item.
:param link: LinkItem instance
"""
self._links.append(link)
self._node.updated_signal.emit()
self.setUnsavedState()
if not sip.isdeleted(link_item):
self._links.append(link_item)
link_item.link().delete_link_signal.connect(self._removeLink)
link_item.link().updated_link_signal.connect(self._linkUpdatedSlot)
self._node.updated_signal.emit()
def removeLink(self, link):
@qslot
def _linkUpdatedSlot(self, *args):
"""
When a link change we also notify the listener of the node
"""
self._node.updated_signal.emit()
@qslot
def _removeLink(self, link_id, *args):
"""
Removes a link items from this node item.
:param link: LinkItem instance
"""
if link in self._links:
self._links.remove(link)
self.setUnsavedState()
for link_item in self._links:
if link_item.link().id() == link_id:
self._links.remove(link_item)
return
def links(self):
"""
@@ -141,119 +211,102 @@ class NodeItem():
return self._links
def createdSlot(self, node_id):
@qslot
def createdSlot(self, base_node_id, *args):
"""
Slot to receive events from the attached Node instance
when a the node has been created/initialized.
:param node_id: node identifier (integer)
:param base_node_id: base node identifier (integer)
"""
if self is None:
return
self._initialized = True
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
self.setSymbol(self._node.symbol())
self.update()
self._showLabel()
def startedSlot(self):
@qslot
def startedSlot(self, *args):
"""
Slot to receive events from the attached Node instance
when a the node has started.
"""
if self is None:
return
for link in self._links:
link.update()
def stoppedSlot(self):
@qslot
def stoppedSlot(self, *args):
"""
Slot to receive events from the attached Node instance
when a the node has stopped.
"""
if self is None:
return
for link in self._links:
link.update()
def suspendedSlot(self):
@qslot
def suspendedSlot(self, *args):
"""
Slot to receive events from the attached Node instance
when a the node has suspended.
"""
if self is None:
return
for link in self._links:
link.update()
def updatedSlot(self):
@qslot
def updatedSlot(self, *args):
"""
Slot to receive events from the attached Node instance
when a the node has been updated.
"""
if self is None:
return
if self._node_label:
if self._node_label.toPlainText() != self._node.name():
self._node_label.setPlainText(self._node.name())
self._centerLabel()
self.setUnsavedState()
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._updateLabel()
# update the link tooltips in case the
# node name has changed
for link in self._links:
link.setCustomToolTip()
def deleteLinksSlot(self):
"""
Slot to receive events from the attached Node instance
when a all the links must be deleted.
"""
if self is None:
return
for link in self._links.copy():
link.delete()
def deletedSlot(self):
@qslot
def deletedSlot(self, *args):
"""
Slot to receive events from the attached Node instance
when the node has been deleted.
"""
if self is None:
if not self.scene():
return
self._node.removeAllocatedName()
if self in self.scene().items():
self.scene().removeItem(self)
self.setUnsavedState()
def serverErrorSlot(self, node_id, message):
@qslot
def serverErrorSlot(self, base_node_id, message, *args):
"""
Slot to receive events from the attached Node instance
when the node has received an error from the server.
:param node_id: node identifier
:param base_node_id: base node identifier
:param message: error message
"""
if self:
self._last_error = "{message}".format(message=message)
self._last_error = "{message}".format(message=message)
def errorSlot(self, node_id, message):
@qslot
def errorSlot(self, base_node_id, message, *args):
"""
Slot to receive events from the attached Node instance
when the node wants to report an error.
:param node_id: node identifier
:param base_node_id: base node identifier
:param message: error message
"""
if self:
self._last_error = "{message}".format(message=message)
self._last_error = "{message}".format(message=message)
def setCustomToolTip(self):
"""
@@ -278,14 +331,11 @@ class NodeItem():
return self._node_label
def setLabel(self, label):
def _labelUnselectedSlot(self):
"""
Sets the node label.
:param label: NoteItem instance.
Called when user unselect the label
"""
self._node_label = label
self.updateNode()
def _centerLabel(self):
"""
@@ -299,6 +349,7 @@ class NodeItem():
label_x_pos = node_middle.x() - text_middle.x()
label_y_pos = -25
self._node_label.setPos(label_x_pos, label_y_pos)
return
def _showLabel(self):
"""
@@ -307,9 +358,29 @@ class NodeItem():
if not self._node_label:
self._node_label = NoteItem(self)
self._node_label.item_unselected_signal.connect(self._labelUnselectedSlot)
self._node_label.setEditable(False)
self._node_label.setPlainText(self._node.name())
self._updateLabel()
self._node.setSettingValue("label", self._node_label.dump())
def _updateLabel(self):
"""
Update the label using the informations stored in the node
"""
if not self._node_label:
return
self._node_label.setPlainText(self._node.name())
label_data = self._node.settings().get("label")
if self._node_label.toPlainText() != label_data["text"]:
self._node_label.setPlainText(label_data["text"])
self._node_label.setStyle(label_data["style"])
self._node_label.setRotation(label_data["rotation"])
if label_data["x"] is None:
self._centerLabel()
self.updateNode()
else:
self._node_label.setPos(label_data["x"], label_data["y"])
def connectToPort(self, unavailable_ports=[]):
"""
@@ -340,7 +411,6 @@ class NodeItem():
ports_dict[port.portNumber()] = port
else:
ports_dict[port.name()] = port
try:
ports = sorted(ports_dict.keys(), key=int)
except ValueError:
@@ -387,14 +457,11 @@ class NodeItem():
:param value: value of the change
"""
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
GRID_SIZE = 75
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
mid_x = self.boundingRect().width() / 2
tmp_x = (GRID_SIZE * round((self.x() + mid_x) / GRID_SIZE)) - mid_x
value.setX((self.GRID_SIZE * round((value.x() + mid_x) / self.GRID_SIZE)) - mid_x)
mid_y = self.boundingRect().height() / 2
tmp_y = (GRID_SIZE * round((self.y() + mid_y) / GRID_SIZE)) - mid_y
if tmp_x != self.x() and tmp_y != self.y():
self.setPos(tmp_x, tmp_y)
value.setY((self.GRID_SIZE * round((value.y() + mid_y) / self.GRID_SIZE)) - mid_y)
# dynamically change the renderer when this node item is selected/unselected.
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
@@ -402,14 +469,14 @@ class NodeItem():
self.graphicsEffect().setEnabled(True)
else:
self.graphicsEffect().setEnabled(False)
self.updateNode()
# adjust link item positions when this node is moving or has changed.
if change == QtWidgets.QGraphicsItem.ItemPositionChange or change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
self.setUnsavedState()
for link in self._links:
link.adjust()
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
return super().itemChange(change, value)
def paint(self, painter, option, widget=None):
"""
@@ -483,3 +550,11 @@ class NodeItem():
if not self.isSelected():
self.graphicsEffect().setEnabled(False)
def mouseRelease(self):
"""
Handle all mouse release for this item.
It the item is select but mouse is not on it the event
is send also
"""
self.updateNode()

View File

@@ -20,6 +20,7 @@ Graphical representation of a note on the QGraphicsScene.
"""
from ..qt import QtCore, QtWidgets, QtGui
from .utils import colorFromSvg
class NoteItem(QtWidgets.QGraphicsTextItem):
@@ -28,6 +29,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
:param parent: optional parent
"""
item_unselected_signal = QtCore.Signal()
show_layer = False
@@ -170,7 +172,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
painter.setPen(QtCore.Qt.black)
zval = str(int(self.zValue()))
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
painter.drawText(QtCore.QPointF(center.x(), center.y()), zval)
def setZValue(self, value):
"""
@@ -187,6 +189,54 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
self.setFlag(self.ItemIsSelectable, True)
self.setFlag(self.ItemIsMovable, True)
def setStyle(self, styles):
"""
Set text style using a SVG style
"""
font = QtGui.QFont()
for style in styles.split(";"):
if ":" in style:
key, val = style.split(":")
key = key.strip()
val = val.strip()
if key == "font-size":
font.setPointSize(int(val))
elif key == "font-family":
font.setFamily(val)
elif key == "font-style" and val == "italic":
font.setItalic(True)
elif key == "font-weight" and val == "bold":
font.setBold(True)
elif key == "text-decoration" and val == "underline":
font.setUnderline(True)
elif key == "text-decoration" and val == "line-through":
font.setStrikeOut(True)
elif key == "fill":
new_color = colorFromSvg(val)
color = self.defaultTextColor()
color.setBlue(new_color.blue())
color.setRed(new_color.red())
color.setGreen(new_color.green())
self.setDefaultTextColor(color)
elif key == "fill-opacity":
color = self.defaultTextColor()
color.setAlphaF(float(val))
self.setDefaultTextColor(color)
self.setFont(font)
def itemChange(self, change, value):
"""
Notifies this node item that some part of the item's state changes.
:param change: GraphicsItemChange type
:param value: value of the change
"""
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
if value == 0:
self.item_unselected_signal.emit()
return super().itemChange(change, value)
def dump(self):
"""
Returns a representation of this note.
@@ -195,63 +245,29 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
"""
note_info = {"text": self.toPlainText(),
"x": self.x(),
"y": self.y()}
"x": int(self.x()),
"y": int(self.y()),
"rotation": int(self.rotation())}
note_info["font"] = self.font().toString()
note_info["color"] = self.defaultTextColor().name(QtGui.QColor.HexArgb)
if self.rotation() != 0:
note_info["rotation"] = self.rotation()
if self.zValue() != 2:
note_info["z"] = self.zValue()
style = ""
style += "font-family: {};".format(self.font().family())
style += "font-size: {};".format(self.font().pointSize())
if self.font().italic():
style += "font-style: italic;"
if self.font().bold():
style += "font-weight: bold;"
if self.font().strikeOut():
style += "text-decoration: line-through;"
elif self.font().underline():
style += "text-decoration: underline;"
style += "fill: {};".format("#" + hex(self.defaultTextColor().rgba())[4:])
style += "fill-opacity: {};".format(self.defaultTextColor().alphaF())
note_info["style"] = style
return note_info
def load(self, note_info):
"""
Loads a note representation
(from a topology file).
:param note_info: representation of the note (dictionary)
"""
# load mandatory properties
text = note_info["text"]
x = note_info["x"]
y = note_info["y"]
self.setPlainText(text)
self.setPos(x, y)
# load optional properties
font = note_info.get("font")
color = note_info.get("color")
rotation = note_info.get("rotation")
z = note_info.get("z")
if font:
qt_font = QtGui.QFont()
if qt_font.fromString(font):
self.setFont(qt_font)
if color:
self.setDefaultTextColor(QtGui.QColor(color))
if rotation is not None:
self.setRotation(float(rotation))
if z is not None:
self.setZValue(z)
def duplicate(self):
"""
Duplicates this node item.
:return: NoteItem instance
"""
note_item = NoteItem(self.parent())
note_item.setPlainText(self.toPlainText())
note_item.setPos(self.x() + 20, self.y() + 20)
note_item.setZValue(self.zValue())
note_item.setFont(self.font())
note_item.setDefaultTextColor(self.defaultTextColor())
note_item.setRotation(self.rotation())
return note_item

View File

@@ -1,47 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Graphical representation of a Pixmap image on the QGraphicsScene.
"""
from ..qt import QtCore, QtWidgets
from .image_item import ImageItem
class PixmapImageItem(ImageItem, QtWidgets.QGraphicsPixmapItem):
"""
Class to insert an pixmap image on the scene.
"""
def __init__(self, pixmap, image_path, pos=None):
QtWidgets.QGraphicsPixmapItem.__init__(self, pixmap)
ImageItem.__init__(self, image_path, pos)
self.setTransformationMode(QtCore.Qt.SmoothTransformation)
def duplicate(self):
"""
Duplicates this image item.
:return: PixmapImageItem instance
"""
image_item = PixmapImageItem(self.pixmap(), self._image_path, QtCore.QPointF(self.x() + 20, self.y() + 20))
image_item.setZValue(self.zValue())
return image_item

View File

@@ -19,6 +19,8 @@
Graphical representation of a rectangle on the QGraphicsScene.
"""
import xml.etree.ElementTree as ET
from ..qt import QtCore, QtGui, QtWidgets
from .shape_item import ShapeItem
@@ -29,25 +31,8 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
Class to draw a rectangle on the scene.
"""
def __init__(self, pos=None, width=200, height=100):
super().__init__()
self.setRect(0, 0, width, height)
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
self.setPen(pen)
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
self.setBrush(brush)
if pos:
self.setPos(pos)
def delete(self):
"""
Deletes this rectangle.
"""
self.scene().removeItem(self)
from ..topology import Topology
Topology.instance().removeRectangle(self)
def __init__(self, width=200, height=100, **kws):
super().__init__(width=width, height=height, **kws)
def paint(self, painter, option, widget=None):
"""
@@ -61,16 +46,19 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def duplicate(self):
def toSvg(self):
"""
Duplicates this rectangle item.
:return: RectangleItem instance
Return an SVG version of the shape
"""
svg = ET.Element("svg")
svg.set("width", str(int(self.rect().width())))
svg.set("height", str(int(self.rect().height())))
rect = ET.SubElement(svg, "rect")
rect.set("width", str(int(self.rect().width())))
rect.set("height", str(int(self.rect().height())))
rect = self._styleSvg(rect)
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
rectangle_item = RectangleItem(QtCore.QPointF(self.x() + 20, self.y() + 20), self.rect().width(), self.rect().height())
rectangle_item.setPen(self.pen())
rectangle_item.setBrush(self.brush())
rectangle_item.setZValue(self.zValue())
rectangle_item.setRotation(self.rotation())
return rectangle_item

View File

@@ -37,12 +37,11 @@ class SerialLinkItem(LinkItem):
:param destination_port: destination Port instance
:param link: Link instance (contains back-end stuff for this link)
:param adding_flag: indicates if this link is being added (no destination yet)
:param multilink: used to draw multiple link between the same source and destination
"""
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag, multilink)
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag)
def adjust(self):
"""
@@ -117,32 +116,28 @@ class SerialLinkItem(LinkItem):
# source point color
if self._source_port.status() == Port.started:
# port is active
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.green
elif self._source_port.status() == Port.suspended:
# port is suspended
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.yellow
else:
shape = QtCore.Qt.SquareCap
color = QtCore.Qt.red
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
source_port_label = self._source_port.label()
if source_port_label is None:
source_port_label = NoteItem(self._source_item)
source_port_label.setPlainText(self._source_port.shortName())
source_port_label.setPos(self.mapToItem(self._source_item, self.source))
self._source_port.setLabel(source_port_label)
if self._draw_port_labels:
if source_port_label is None:
source_port_label = NoteItem(self._source_item)
if not self._source_port.isStub():
source_port_name = self._source_port.name().replace(self._source_port.longNameType(),
self._source_port.shortNameType())
else:
source_port_name = self._source_port.name()
source_port_label.setPlainText(source_port_name)
source_port_label.setPos(self.mapToItem(self._source_item, self.source))
self._source_port.setLabel(source_port_label)
elif source_port_label and not source_port_label.isVisible():
source_port_label.show()
elif source_port_label:
source_port_label.show()
else:
source_port_label.hide()
painter.drawPoint(self.source_point)
@@ -151,31 +146,28 @@ class SerialLinkItem(LinkItem):
if self._destination_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
elif self._destination_port.status() == Port.suspended:
# port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
destination_port_label = self._destination_port.label()
if destination_port_label is None:
destination_port_label = NoteItem(self._destination_item)
destination_port_label.setPlainText(self._destination_port.shortName())
destination_port_label.setPos(self.mapToItem(self._destination_item, self.destination))
self._destination_port.setLabel(destination_port_label)
if self._draw_port_labels:
if destination_port_label is None:
destination_port_label = NoteItem(self._destination_item)
if not self._destination_port.isStub():
destination_port_name = self._destination_port.name().replace(self._destination_port.longNameType(),
self._destination_port.shortNameType())
else:
destination_port_name = self._destination_port.name()
destination_port_label.setPlainText(destination_port_name)
destination_port_label.setPos(self.mapToItem(self._destination_item, self.destination))
self._destination_port.setLabel(destination_port_label)
elif destination_port_label and not destination_port_label.isVisible():
destination_port_label.show()
elif destination_port_label:
destination_port_label.show()
else:
destination_port_label.hide()
painter.drawPoint(self.destination_point)

View File

@@ -19,46 +19,48 @@
Base class for shape items (Rectangle, ellipse etc.).
"""
import xml.etree.ElementTree as ET
from ..qt import QtCore, QtGui, QtWidgets
from .drawing_item import DrawingItem
from .utils import colorFromSvg
import logging
log = logging.getLogger(__name__)
class ShapeItem:
class ShapeItem(DrawingItem):
# Map QT stroke to SVG style
QT_DASH_TO_SVG = {
QtCore.Qt.SolidLine: "",
QtCore.Qt.NoPen: None,
QtCore.Qt.DashLine: "25, 25",
QtCore.Qt.DotLine: "5, 25",
QtCore.Qt.DashDotLine: "5, 25, 25",
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
}
"""
Base class to draw shapes on the scene.
"""
show_layer = False
def __init__(self, width=200, height=200, svg=None, **kws):
def __init__(self, **kws):
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable)
super().__init__(svg=svg, **kws)
self.setAcceptHoverEvents(True)
self._border = 5
self._edge = None
from ..main_window import MainWindow
self._graphics_view = MainWindow.instance().uiGraphicsView
def keyPressEvent(self, event):
"""
Handles all key press events
:param event: QKeyEvent
"""
key = event.key()
modifiers = event.modifiers()
if key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
if self.rotation() > -360.0:
self.setRotation(self.rotation() - 1)
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
if self.rotation() < 360.0:
self.setRotation(self.rotation() + 1)
if svg is None:
self.setRect(0, 0, width, height)
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
self.setPen(pen)
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
self.setBrush(brush)
else:
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
self.fromSvg(svg)
if self._id is None:
self.create()
def mousePressEvent(self, event):
"""
@@ -178,131 +180,67 @@ class ShapeItem:
if self.zValue() >= 0:
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
def drawLayerInfo(self, painter):
def _styleSvg(self, element):
"""
Draws the layer position.
:param painter: QPainter instance
Add style from the shape item to the SVG element that we will
export
"""
if self.show_layer is False:
return
brect = self.boundingRect()
# don't draw anything if the object is too small
if brect.width() < 20 or brect.height() < 20:
return
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
painter.setBrush(QtCore.Qt.red)
painter.setPen(QtCore.Qt.red)
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
painter.setPen(QtCore.Qt.black)
zval = str(int(self.zValue()))
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
def setZValue(self, value):
"""
Sets a new Z value.
:param value: Z value
"""
QtWidgets.QGraphicsItem.setZValue(self, value)
if self.zValue() < 0:
self.setFlag(self.ItemIsSelectable, False)
self.setFlag(self.ItemIsMovable, False)
else:
self.setFlag(self.ItemIsSelectable, True)
self.setFlag(self.ItemIsMovable, True)
def dump(self):
"""
Returns a representation of this shape item.
:returns: dictionary
"""
shape_info = {"width": self.rect().width(),
"height": self.rect().height(),
"x": self.x(),
"y": self.y()}
brush = self.brush()
if brush.color() != QtCore.Qt.white:
shape_info["color"] = brush.color().name()
if brush.color().alpha() != 255:
shape_info["transparency"] = brush.color().alpha()
style = ""
pen = self.pen()
if pen.color() != QtCore.Qt.black:
shape_info["border_color"] = pen.color().name()
if pen.color().alpha() != 255:
shape_info["border_transparency"] = pen.color().alpha()
if pen.width() != 2:
shape_info["border_width"] = pen.width()
if pen.style() != QtCore.Qt.SolidLine:
shape_info["border_style"] = pen.style()
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
element.set("fill-opacity", str(self.brush().color().alphaF()))
if self.rotation() != 0:
shape_info["rotation"] = self.rotation()
if self.zValue() != 0:
shape_info["z"] = self.zValue()
dasharray = self.QT_DASH_TO_SVG[pen.style()]
if dasharray is None: # No border to the element
return element
elif dasharray == "":
pass # Solid line
else:
element.set("stroke-dasharray", dasharray)
element.set("stroke-width", str(pen.width()))
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
return element
return shape_info
def load(self, shape_info):
def fromSvg(self, svg):
"""
Loads a representation of this shape item.
(from a topology file).
:param shape_info: representation of the shape item (dictionary)
Import element informations from an SVG
"""
# load mandatory properties
width = shape_info["width"]
height = shape_info["height"]
x = shape_info["x"]
y = shape_info["y"]
svg = ET.fromstring(svg)
width = float(svg.get("width", self.rect().width()))
height = float(svg.get("height", self.rect().height()))
self.setRect(0, 0, width, height)
self.setPos(x, y)
# load optional properties
z = shape_info.get("z")
color = shape_info.get("color")
if not color and shape_info.get("fill_color"):
# compatibility with old 1.0 projects
color = shape_info.get("fill_color")
transparency = shape_info.get("transparency")
border_color = shape_info.get("border_color")
border_transparency = shape_info.get("border_transparency")
border_width = shape_info.get("border_width")
border_style = shape_info.get("border_style")
rotation = shape_info.get("rotation")
pen = QtGui.QPen()
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
if color:
color = QtGui.QColor(color)
else:
color = QtGui.QColor(255, 255, 255)
if transparency is not None:
color.setAlpha(transparency)
self.setBrush(QtGui.QBrush(color))
if len(svg):
if svg[0].get("stroke-width"):
pen.setWidth(int(svg[0].get("stroke-width")))
if svg[0].get("stroke"):
pen.setColor(colorFromSvg(svg[0].get("stroke")))
if svg[0].get("fill"):
new_color = colorFromSvg(svg[0].get("fill"))
color = brush.color()
color.setBlue(new_color.blue())
color.setRed(new_color.red())
color.setGreen(new_color.green())
brush.setColor(color)
if svg[0].get("fill-opacity"):
color = brush.color()
color.setAlphaF(float(svg[0].get("fill-opacity")))
brush.setColor(color)
# Map SVG stroke style (border of the element to the Qt version)
if not svg[0].get("stroke"):
pen.setStyle(QtCore.Qt.NoPen)
else:
pen.setStyle(QtCore.Qt.SolidLine)
stroke = svg[0].get("stroke-dasharray")
if stroke:
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
if svg_stroke == stroke:
pen.setStyle(qt_stroke)
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
if border_color:
border_color = QtGui.QColor(border_color)
else:
border_color = pen.color()
if border_transparency:
border_color.setAlpha(border_transparency)
pen.setColor(border_color)
if border_width is not None:
pen.setWidth(int(border_width))
if border_style is not None:
pen.setStyle(QtCore.Qt.PenStyle(border_style))
self.setPen(pen)
if rotation is not None:
self.setRotation(rotation)
if z is not None:
self.setZValue(z)
self.setBrush(brush)
self.update()

View File

@@ -1,47 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Graphical representation of a SVG image on the QGraphicsScene.
"""
from ..qt import QtCore, QtSvg
from .image_item import ImageItem
class SvgImageItem(ImageItem, QtSvg.QGraphicsSvgItem):
"""
Class to insert a SVG image on the scene.
"""
def __init__(self, renderer, image_path, pos=None):
QtSvg.QGraphicsSvgItem.__init__(self)
ImageItem.__init__(self, image_path, pos)
self.setSharedRenderer(renderer)
def duplicate(self):
"""
Duplicates this image item.
:return: SvgImageItem instance
"""
image_item = SvgImageItem(self.renderer(), self._image_path, QtCore.QPointF(self.x() + 20, self.y() + 20))
image_item.setZValue(self.zValue())
return image_item

View File

@@ -1,51 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Graphical representation of a SVG node on the QGraphicsScene.
"""
from ..qt import QtSvg
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from .node_item import NodeItem
import logging
log = logging.getLogger(__name__)
class SvgNodeItem(NodeItem, QtSvg.QGraphicsSvgItem):
"""
SVG node for the scene.
:param node: Node instance
:param symbol: symbol for the node representation on the scene
"""
def __init__(self, node, symbol=None):
QtSvg.QGraphicsSvgItem.__init__(self)
NodeItem.__init__(self, node)
# create renderer using symbols path/resource
if symbol:
renderer = QImageSvgRenderer(symbol)
if symbol != node.defaultSymbol():
renderer.setObjectName(symbol)
else:
renderer = QImageSvgRenderer(node.defaultSymbol())
self.setSharedRenderer(renderer)

191
gns3/items/text_item.py Normal file
View File

@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Graphical representation of a note on the QGraphicsScene.
"""
import xml.etree.ElementTree as ET
from ..qt import QtCore, QtWidgets, QtGui
from .drawing_item import DrawingItem
from .utils import colorFromSvg
import logging
log = logging.getLogger(__name__)
class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
"""
Text item for the QGraphicsView.
"""
def __init__(self, svg=None, **kws):
super().__init__(**kws)
from ..main_window import MainWindow
main_window = MainWindow.instance()
view_settings = main_window.uiGraphicsView.settings()
qt_font = QtGui.QFont()
qt_font.fromString(view_settings["default_label_font"])
self.setDefaultTextColor(QtGui.QColor(view_settings["default_label_color"]))
self.setFont(qt_font)
if svg:
try:
svg = self.fromSvg(svg)
except ET.ParseError as e:
log.warning(str(e))
if self._id is None:
self.create()
def editText(self):
"""
Edit mode for this note.
"""
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
self.setSelected(True)
self.setFocus()
cursor = self.textCursor()
cursor.select(QtGui.QTextCursor.Document)
self.setTextCursor(cursor)
def mouseDoubleClickEvent(self, event):
"""
Handles all mouse double click events.
:param event: QMouseEvent instance
"""
self.editText()
def focusOutEvent(self, event):
"""
Handles all focus out events.
:param event: QFocusEvent instance
"""
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False)
cursor = self.textCursor()
if cursor.hasSelection():
cursor.clearSelection()
self.setTextCursor(cursor)
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
if not self.toPlainText():
# delete the note if empty
self.delete()
return
else:
self.updateDrawing()
return super().focusOutEvent(event)
def paint(self, painter, option, widget=None):
"""
Paints the contents of an item in local coordinates.
:param painter: QPainter instance
:param option: QStyleOptionGraphicsItem instance
:param widget: QWidget instance
"""
super().paint(painter, option, widget)
self.drawLayerInfo(painter)
def toSvg(self):
"""
Return an SVG version of the text
"""
svg = ET.Element("svg")
svg.set("width", str(int(self.boundingRect().width())))
svg.set("height", str(int(self.boundingRect().height())))
text = ET.SubElement(svg, "text")
text.set("font-family", self.font().family())
text.set("font-size", str(self.font().pointSize()))
if self.font().italic():
text.set("font-style", "italic")
if self.font().bold():
text.set("font-weight", "bold")
if self.font().strikeOut():
text.set("text-decoration", "line-through")
elif self.font().underline():
text.set("text-decoration", "underline")
text.set("fill", "#" + hex(self.defaultTextColor().rgba())[4:])
text.set("fill-opacity", str(self.defaultTextColor().alphaF()))
text.text = self.toPlainText()
svg = ET.tostring(svg, encoding="utf-8").decode("utf-8")
return svg
def fromSvg(self, svg):
svg = ET.fromstring(svg)
text = svg[0]
font = QtGui.QFont()
color = text.get("fill")
if color:
new_color = colorFromSvg(color)
color = self.defaultTextColor()
color.setBlue(new_color.blue())
color.setRed(new_color.red())
color.setGreen(new_color.green())
self.setDefaultTextColor(color)
opacity = text.get("fill-opacity")
if opacity:
color = self.defaultTextColor()
color.setAlphaF(float(opacity))
self.setDefaultTextColor(color)
font.setPointSize(int(text.get("font-size", self.font().pointSize())))
font.setFamily(text.get("font-family", self.font().family()))
if text.get("font-style") == "italic":
font.setItalic(True)
if text.get("font-weight") == "bold":
font.setBold(True)
if text.get("text-decoration") == "underline":
font.setUnderline(True)
if text.get("text-decoration") == "line-through":
font.setStrikeOut(True)
self.setFont(font)
self.setPlainText(text.text)
def editable(self):
"""
Returns either the note is editable or not.
:return: boolean
"""
return True
def keyPressEvent(self, event):
"""
Handles all key press events
:param event: QKeyEvent
"""
if not self.handleKeyPressEvent(event):
super().keyPressEvent(event)

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
#
# Copyright (C) 2014 GNS3 Technologies Inc.
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -15,13 +15,17 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Base class for NIOs (Network Input/Output).
"""
from ..qt import QtGui
class NIO:
def __init__(self):
pass
def colorFromSvg(value):
"""
Transform a color coming from a SVG file to a Qcolor
"""
value = value.strip('#')
if value == "":
value = "000000"
if len(value) == 6: # If alpha channel is missing
value = "ff" + value
value = int(value, base=16)
return QtGui.QColor.fromRgba(value)

View File

@@ -19,10 +19,14 @@
Manages and stores everything needed for a connection between 2 devices.
"""
import os
import re
import sip
import uuid
from .qt import QtCore, QtWidgets
from .controller import Controller
from .qt import QtCore
from .nios.nio_udp import NIOUDP
from .nios.nio_vmnet import NIOVMNET
import logging
log = logging.getLogger(__name__)
@@ -37,17 +41,21 @@ class Link(QtCore.QObject):
:param source_port: source Port instance
:param destination_node: destination Node instance
:param destination_port: destination Port instance
:param stub: indicates if the link is connected to a stub device like a Cloud
"""
# signals used to let the GUI view know about link
# additions and deletions.
add_link_signal = QtCore.Signal(int)
delete_link_signal = QtCore.Signal(int)
updated_link_signal = QtCore.Signal(int)
error_link_signal = QtCore.Signal(int)
_instance_count = 1
def __init__(self, source_node, source_port, destination_node, destination_port):
def __init__(self, source_node, source_port, destination_node, destination_port, link_id=None, **link_data):
"""
:param link_data: Link information from the API
"""
super().__init__()
@@ -64,56 +72,166 @@ class Link(QtCore.QObject):
self._source_port = source_port
self._destination_node = destination_node
self._destination_port = destination_port
self._source_nio = None
self._destination_nio = None
self._source_nio_active = False
self._destination_nio_active = False
self._source_label = None
self._destination_label = None
self._link_id = link_id
self._capturing = False
self._capture_file_path = None
self._capture_file = None
self._initialized = False
if source_port.isStub() or destination_port.isStub():
self._stub = True
# Boolean if True we are creatin the first instance of this node
# if false the node already exist in the topology
# use to avoid erasing informations when reloading
self._creator = False
self._nodes = []
self._source_node.addLink(self)
self._destination_node.addLink(self)
body = self._prepareParams()
if self._link_id:
link_data["link_id"] = self._link_id
self._linkCreatedCallback(link_data)
else:
self._stub = False
# we must request UDP information if the NIO is a NIO UDP and before
# it can be created.
if not self._stub:
# connect signals used when a NIO has been created by a node
# and this NIO need to be attached to a port connected to this link
source_node.nio_signal.connect(self.newNIOSlot)
destination_node.nio_signal.connect(self.newNIOSlot)
self._link_id = str(uuid.uuid4())
self._creator = True
Controller.instance().post("/projects/{project_id}/links".format(project_id=source_node.project().id()), self._linkCreatedCallback, body=body)
# currently, we support only NIO_UDP and NIO_VMNET for normal connections (non-stub).
if source_port.defaultNio() == NIOUDP:
assert destination_port.defaultNio() == NIOUDP
self._source_udp = None
self._destination_udp = None
def _parseResponse(self, result):
self._capturing = result.get("capturing", False)
# connect signals used to receive a UDP port and host allocated by a node
source_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
destination_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
# request the UDP info for each node
source_node.allocateUDPPort(self._source_port.id())
destination_node.allocateUDPPort(self._destination_port.id())
elif source_port.defaultNio() == NIOVMNET:
assert destination_port.defaultNio() == NIOVMNET
source_node.allocate_vmnet_nio_signal.connect(self.VMnetInterfaceAllocatedSlot)
source_node.allocateVMnetInterface(self._source_port.id())
else:
raise NotImplementedError()
# If the controller is remote the capture path should be rewrite to something local
if Controller.instance().isRemote():
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
self._capture_file = QtCore.QTemporaryFile()
self._capture_file.open(QtCore.QFile.WriteOnly)
self._capture_file.setAutoRemove(True)
self._capture_file_path = self._capture_file.fileName()
Controller.instance().get(
"/projects/{project_id}/links/{link_id}/pcap".format(
project_id=self.project().id(),
link_id=self._link_id),
None,
showProgress=False,
downloadProgressCallback=self._downloadPcapProgress,
ignoreErrors=True, # If something is wrong avoid disconnect us from server
timeout=None)
else:
# handle stub connections (to a cloud for instance).
if not source_port.isStub() and destination_port.isStub():
source_node.nio_signal.connect(self.newNIOSlot)
self._source_nio = self._destination_port.defaultNio()
self._source_node.nio_cancel_signal.connect(self.cancelNIOSlot)
self._source_node.addNIO(self._source_port, self._source_nio)
elif not destination_port.isStub() and source_port.isStub():
destination_node.nio_signal.connect(self.newNIOSlot)
self._destination_nio = self._source_port.defaultNio()
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
self._destination_node.addNIO(self._destination_port, self._destination_nio)
self._capture_file_path = result["capture_file_path"]
if "nodes" in result:
self._nodes = result["nodes"]
self._updateLabels()
self.updated_link_signal.emit(self._id)
def creator(self):
return self._creator
def initialized(self):
return self._initialized
def addPortLabel(self, port, label):
if port.adapterNumber() == self._source_port.adapterNumber() and port.portNumber() == self._source_port.portNumber() and port.destinationNode() == self._destination_node:
self._source_label = label
else:
self._destination_label = label
label.item_unselected_signal.connect(self.update)
if self.creator():
self.update()
else:
self._updateLabels()
def update(self):
if not self._link_id:
return
body = self._prepareParams()
Controller.instance().put("/projects/{project_id}/links/{link_id}".format(project_id=self._source_node.project().id(), link_id=self._link_id), self.updateLinkCallback, body=body)
def updateLinkCallback(self, result, error=False, *args, **kwargs):
if error:
QtWidgets.QMessageBox.warning(None, "Update link", "Error while updating link: {}".format(result["message"]))
return
self._parseResponse(result)
def _updateLabels(self):
for node in self._nodes:
if node["node_id"] == self._source_node.node_id() and node["adapter_number"] == self._source_port.adapterNumber() and node["port_number"] == self._source_port.portNumber():
self._updateLabel(self._source_label, node["label"])
elif node["node_id"] == self._destination_node.node_id() and node["adapter_number"] == self._destination_port.adapterNumber() and node["port_number"] == self._destination_port.portNumber():
self._updateLabel(self._destination_label, node["label"])
else:
log.error("both ports are stub!")
raise NotImplementedError
def _updateLabel(self, label, label_data):
if not label or sip.isdeleted(label):
return
label.setPlainText(label_data["text"])
label.setPos(label_data["x"], label_data["y"])
label.setStyle(label_data["style"])
label.setRotation(label_data["rotation"])
def _prepareParams(self):
body = {
"nodes": [
{
"node_id": self._source_node.node_id(),
"adapter_number": self._source_port.adapterNumber(),
"port_number": self._source_port.portNumber(),
},
{
"node_id": self._destination_node.node_id(),
"adapter_number": self._destination_port.adapterNumber(),
"port_number": self._destination_port.portNumber()
}
]
}
if self._source_port.label():
body["nodes"][0]["label"] = self._source_port.label().dump()
if self._destination_port.label():
body["nodes"][1]["label"] = self._destination_port.label().dump()
return body
def _linkCreatedCallback(self, result, error=False, **kwargs):
if error:
QtWidgets.QMessageBox.warning(None, "Create link", "Error while creating link: {}".format(result["message"]))
self.deleteLink(skip_controller=True)
return
self._initialized = True
# let the GUI know about this link has been created
self.add_link_signal.emit(self._id)
self._source_port.setLinkId(self._id)
self._source_port.setLink(self)
self._source_port.setDestinationNode(self._destination_node)
self._source_port.setDestinationPort(self._destination_port)
self._destination_port.setLinkId(self._id)
self._destination_port.setLink(self)
self._destination_port.setDestinationNode(self._source_node)
self._destination_port.setDestinationPort(self._source_port)
self._link_id = result["link_id"]
self._parseResponse(result)
def link_id(self):
return self._link_id
def capturing(self):
"""
Is a capture running on the link?
"""
return self._capturing
def capture_file_path(self):
"""
Path of the capture file
"""
return self._capture_file_path
def project(self):
return self._source_node.project()
@classmethod
def reset(cls):
@@ -130,7 +248,18 @@ class Link(QtCore.QObject):
self._destination_node.name(),
self._destination_port.name())
def deleteLink(self):
def capture_file_name(self):
"""
:returns: File name for a capture on this link
"""
capture_file_name = "{}_{}_to_{}_{}".format(
self._source_node.name(),
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name())
return re.sub("[^0-9A-Za-z_-]", "", capture_file_name)
def deleteLink(self, skip_controller=False):
"""
Deletes this link.
"""
@@ -140,19 +269,92 @@ class Link(QtCore.QObject):
self._destination_node.name(),
self._destination_port.name()))
# delete the NIOs on both source and destination nodes
if self._source_port.nio():
self._source_node.deleteNIO(self._source_port)
if skip_controller:
self._linkDeletedCallback({})
else:
Controller.instance().delete("/projects/{project_id}/links/{link_id}".format(project_id=self.project().id(),
link_id=self._link_id), self._linkDeletedCallback)
def _linkDeletedCallback(self, result, error=False, **kwargs):
"""
Called after the link is remove from the topology
"""
if error:
log.error("Error while deleting link: {}".format(result["message"]))
return
self._source_port.setFree()
self._source_node.deleteLink(self)
self._source_node.updated_signal.emit()
if self._destination_port.nio():
self._destination_node.deleteNIO(self._destination_port)
self._destination_port.setFree()
self._destination_node.deleteLink(self)
self._destination_node.updated_signal.emit()
# let the GUI know about this link has been deleted
self.delete_link_signal.emit(self._id)
def startCapture(self, data_link_type, capture_file_name):
data = {
"capture_file_name": capture_file_name,
"data_link_type": data_link_type
}
Controller.instance().post(
"/projects/{project_id}/links/{link_id}/start_capture".format(
project_id=self.project().id(),
link_id=self._link_id),
self._startCaptureCallback,
body=data)
def _startCaptureCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while starting capture on link: {}".format(result["message"]))
return
self._parseResponse(result)
def _downloadPcapProgress(self, content, server=None, context={}, **kwargs):
"""
Called for each part of the file of the PCAP
"""
if not self._capture_file_path:
return
self._capture_file.write(content)
self._capture_file.flush()
def stopCapture(self):
if Controller.instance().isRemote():
if self._capture_file:
self._capture_file.close()
self._capture_file = None
if self._capture_file_path:
try:
os.remove(self._capture_file_path)
except OSError as e:
log.error("Can't remove file {}".format(self._capture_file_path))
self._capture_file_path = None
Controller.instance().post(
"/projects/{project_id}/links/{link_id}/stop_capture".format(
project_id=self.project().id(),
link_id=self._link_id),
self._stopCaptureCallback)
def _stopCaptureCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while stopping capture on link: {}".format(result["message"]))
return
self._parseResponse(result)
def get(self, path, callback, **kwargs):
"""
HTTP Get from a link
"""
Controller.instance().get(
"/projects/{project_id}/links/{link_id}{path}".format(
project_id=self.project().id(),
link_id=self._link_id,
path=path),
callback,
**kwargs)
def id(self):
"""
Returns this link identifier.
@@ -198,217 +400,12 @@ class Link(QtCore.QObject):
return self._destination_port
def UDPPortAllocatedSlot(self, node_id, port_id, lport):
def getNodePort(self, node):
"""
Slot to receive events from Node instances
when a UDP port has been allocated in order to create a NIO UDP.
Search the port in the link corresponding to this node
:param node_id: node identifier
:param port_id: port identifier
:param lport: local UDP port
:returns: Node instance
"""
if not self:
return
# check that the node is connected to this link as a source
if self._source_node and node_id == self._source_node.id() and port_id == self._source_port.id():
laddr = self._source_node.server().host()
self._source_udp = (lport, laddr)
# disconnect the signal has we don't expect new source UDP info for this link.
self._source_node.allocate_udp_nio_signal.disconnect(self.UDPPortAllocatedSlot)
log.debug("{} has allocated UDP port {} for host {}".format(self._source_node.name(),
lport,
laddr))
# check that the node is connected to this link as a destination
elif self._destination_node and node_id == self._destination_node.id() and port_id == self._destination_port.id():
laddr = self._destination_node.server().host()
self._destination_udp = (lport, laddr)
# disconnect the signal has we don't expect new source UDP info for this link.
self._destination_node.allocate_udp_nio_signal.disconnect(self.UDPPortAllocatedSlot)
log.debug("{} has allocated UDP port {} for host {}".format(self._destination_node.name(),
lport,
laddr))
if self._source_udp and self._destination_udp:
# we got UDP info from both source and destination nodes
# meaning we can proceed with the creation of UDP NIOs
lport, laddr = self._source_udp
rport, raddr = self._destination_udp
self._source_nio = NIOUDP(lport, raddr, rport)
self._destination_nio = NIOUDP(rport, laddr, lport)
self._source_udp = None
self._destination_udp = None
log.debug("creating UDP tunnel from {}:{} to {}:{} ".format(laddr, lport, raddr, rport))
# add the UDP NIOs to the nodes
self._source_node.nio_cancel_signal.connect(self.cancelNIOSlot)
self._source_node.addNIO(self._source_port, self._source_nio)
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
self._destination_node.addNIO(self._destination_port, self._destination_nio)
def VMnetInterfaceAllocatedSlot(self, node_id, port_id, vmnet):
"""
Slot to receive events from Node instances
when a VMnet interface has been allocated in order to create a NIO VMNET.
:param node_id: node identifier
:param port_id: port identifier
:param vmnet: vmnet interface name
"""
# check that the node is connected to this link as a source
# only the source is used to request the server for a vmnet interface
# and then allocate a NIO VMNET to both the source and destination
if node_id == self._source_node.id() and port_id == self._source_port.id():
self._source_node.allocate_vmnet_nio_signal.disconnect(self.VMnetInterfaceAllocatedSlot)
self._source_nio = NIOVMNET(vmnet)
self._destination_nio = NIOVMNET(vmnet)
# add the VMnet NIOs to the nodes
self._source_node.nio_cancel_signal.connect(self.cancelNIOSlot)
self._source_node.addNIO(self._source_port, self._source_nio)
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
self._destination_node.addNIO(self._destination_port, self._destination_nio)
def newNIOSlot(self, node_id, port_id):
"""
Slot to receive events from Node instances
when a NIO has been created on the server
and are active.
:param node_id: node identifier
:param port_id: port identifier
"""
# in very rare cases link is already deleted
if self is None:
return
# check that the node is connected to this link as a source
if node_id == self._source_node.id() and port_id == self._source_port.id():
self._source_nio_active = True
# disconnect the signal has we don't expect new source NIO for this link.
self._source_node.nio_signal.disconnect(self.newNIOSlot)
# check that the node is connected to this link as a destination
elif node_id == self._destination_node.id() and port_id == self._destination_port.id():
self._destination_nio_active = True
# disconnect the signal has we don't expect new destination NIO for this link.
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
if not self._stub and self._source_nio_active and self._destination_nio_active:
# both NIOs are active now.
self._addToSourcePort(self._source_nio)
self._addToDestinationPort(self._destination_nio)
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
self._source_nio_active = False
self._destination_nio_active = False
# let the GUI know about this link has been created
self.add_link_signal.emit(self._id)
elif self._stub and self._source_nio_active:
self._addToSourcePort(self._source_nio)
# add the NIO to destination to show the port is not free.
self._addToDestinationPort(self._source_nio)
self._source_nio_active = False
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
self.add_link_signal.emit(self._id)
elif self._stub and self._destination_nio_active:
# add the NIO to source to show the port is not free.
self._addToSourcePort(self._destination_nio)
self._addToDestinationPort(self._destination_nio)
self._destination_nio_active = False
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
self.add_link_signal.emit(self._id)
def _addToSourcePort(self, nio):
"""
Adds a NIO, a link id and a description to the source port.
:param nio: NIO instance
"""
self._source_port.setNio(nio)
self._source_port.setLinkId(self._id)
self._source_port.setDestinationNode(self._destination_node)
self._source_port.setDestinationPort(self._destination_port)
log.debug("{} attached to {} on port {}".format(nio,
self._source_node.name(),
self._source_port.name()))
def _addToDestinationPort(self, nio):
"""
Adds a NIO, a link id and a description to the destination port.
:param nio: NIO instance
"""
self._destination_port.setNio(nio)
self._destination_port.setLinkId(self._id)
self._destination_port.setDestinationNode(self._source_node)
self._destination_port.setDestinationPort(self._source_port)
log.debug("{} attached to {} on port {}".format(nio,
self._destination_node.name(),
self._destination_port.name()))
def cancelNIOSlot(self, node_id):
"""
Slot to receive events from Node instances
when a NIO has been canceled because of an
error returned by the server.
:param node_id: node identifier
"""
if not self._stub:
try:
# the destination node has canceled its NIO allocation
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
except TypeError:
# ignore TypeError: 'method' object is not connected
pass
try:
# the source node has canceled its NIO allocation
self._source_node.nio_signal.disconnect(self.newNIOSlot)
except TypeError:
# ignore TypeError: 'method' object is not connected
pass
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
else:
if self._source_node.id() == node_id:
self._source_node.nio_signal.disconnect(self.newNIOSlot)
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
else:
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
self._source_nio_active = False
self._destination_nio_active = False
self.deleteLink()
def dump(self):
"""
Returns a representation of this link.
:returns: dictionary
"""
return {"id": self.id(),
"description": str(self),
"source_node_id": self._source_node.id(),
"source_port_id": self._source_port.id(),
"destination_node_id": self._destination_node.id(),
"destination_port_id": self._destination_port.id()}
if self._destination_node == node:
return self._destination_port
return self._source_port

View File

@@ -23,9 +23,10 @@ import copy
import psutil
from .qt import QtCore
from .qt import QtCore, QtWidgets, qslot
from .version import __version__
from .utils import parse_version
from .controller import Controller
import logging
log = logging.getLogger(__name__)
@@ -38,24 +39,54 @@ class LocalConfig(QtCore.QObject):
"""
config_changed_signal = QtCore.Signal()
# When this signal is emit the config is saved on controller
save_on_controller_signal = QtCore.Signal()
def __init__(self, config_file=None):
"""
:param config_file: Path to the config file (override all other config, usefull for tests)
"""
super().__init__()
self._profile = None
self._config_file = config_file
# Security to avoid pushing to the controller settings before
# we get the original settings from controller
self._settings_retrieved_from_controller = False
self._migrateOldConfigPath()
self._resetLoadConfig()
self._monitoring_changes = False
Controller.instance().connected_signal.connect(self.refreshConfigFromController)
self.save_on_controller_signal.connect(self._saveOnController)
def _monitorChanges(self):
"""
Poll the remote server waiting for settings update
"""
if self._monitoring_changes:
return
self._monitoring_changes = True
self._timer = QtCore.QTimer()
self._timer.setInterval(5000)
self._refreshingSettings = False
self._timer.timeout.connect(self.refreshConfigFromController)
self._timer.start()
def _resetLoadConfig(self):
"""
Reload the config from scratch everything is clean
"""
self._settings = {}
self._last_config_changed = None
if sys.platform.startswith("win"):
filename = "gns3_gui.ini"
else:
filename = "gns3_gui.conf"
self._migrateOldConfigPath()
appname = "GNS3"
if sys.platform.startswith("win"):
# On windows, the system wide configuration file location is %COMMON_APPDATA%/GNS3/gns3_gui.conf
common_appdata = os.path.expandvars("%COMMON_APPDATA%")
system_wide_config_file = os.path.join(common_appdata, appname, filename)
@@ -63,10 +94,8 @@ class LocalConfig(QtCore.QObject):
# On UNIX-like platforms, the system wide configuration file location is /etc/xdg/GNS3/gns3_gui.conf
system_wide_config_file = os.path.join("/etc/xdg", appname, filename)
if config_file:
self._config_file = config_file
else:
self._config_file = os.path.join(LocalConfig.configDirectory(), filename)
if not self._config_file:
self._config_file = os.path.join(self.configDirectory(), filename)
# First load system wide settings
if os.path.exists(system_wide_config_file):
@@ -89,10 +118,56 @@ class LocalConfig(QtCore.QObject):
# overwrite system wide settings with user specific ones
self._settings.update(user_settings)
self._migrateOldConfig()
self._writeConfig()
self.writeConfig()
@staticmethod
def configDirectory():
def profile(self):
"""
:returns: Current settings profile
"""
return self._profile
def setProfile(self, profile):
previous_profile = self._profile
if profile == "default":
self._profile = None
else:
self._profile = profile
if previous_profile != self._profile:
self._config_file = None
self._resetLoadConfig()
@qslot
def refreshConfigFromController(self):
"""
Refresh the configuration from the controller
"""
controller = Controller.instance()
if controller.connected():
self._refreshingSettings = True
controller.get("/settings", self._getSettingsCallback, showProgress=False)
self._monitorChanges()
def _getSettingsCallback(self, result, error=False, **kwargs):
self._refreshingSettings = False
if error:
log.error("Can't get settings from controller")
return
if result == {} and self._settings != {}:
self._settings_retrieved_from_controller = True
self.save_on_controller_signal.emit()
return
# The server return an uuid to keep track of settings version
if self._settings.get("modification_uuid") != result.get("modification_uuid"):
self._settings.update(result)
# Update already loaded section
for section in self._settings.keys():
if isinstance(self._settings[section], dict):
self.loadSectionSettings(section, self._settings[section])
self.config_changed_signal.emit()
self._settings_retrieved_from_controller = True
def configDirectory(self):
"""
Get the configuration directory
"""
@@ -102,6 +177,10 @@ class LocalConfig(QtCore.QObject):
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3")
if self._profile is not None:
path = os.path.join(path, "profiles", self._profile)
return os.path.normpath(path)
def _migrateOldConfigPath(self):
@@ -118,13 +197,23 @@ class LocalConfig(QtCore.QObject):
try:
shutil.copytree(old_path, new_path)
except OSError as e:
print("Can't copy the old config: %s", str(e))
log.error("Can't copy the old config: %s", str(e))
def _migrateOldConfig(self):
"""
Migrate pre 1.4 config
"""
# Display an error if settings come from a more recent version of GNS3
# patch level version are compatible (ex 1.5.3 and 1.5.2). But if you open
# 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()))
# Exit immediately not clean but we want to avoid any side effect that could corrupt the file
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", {})
@@ -134,7 +223,7 @@ class LocalConfig(QtCore.QObject):
# 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"] = "/Applications/GNS3.app/Contents/MacOS/gns3server"
servers["local_server"]["path"] = "gns3server"
if "RemoteServers" in self._settings:
servers["remote_servers"] = copy.copy(self._settings["RemoteServers"])
@@ -151,9 +240,29 @@ class LocalConfig(QtCore.QObject):
from .settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, DEFAULT_TELNET_CONSOLE_COMMAND
if "MainWindow" in self._settings:
if self._settings["MainWindow"]["telnet_console_command"] not in PRECONFIGURED_TELNET_CONSOLE_COMMANDS.values():
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
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
# we remove it from the list of available VM
vms = []
for vm in self._settings["Qemu"].get("vms", []):
if vm.get("hda_disk_image") != "core-linux-6.4-internet-0.1.img":
vms.append(vm)
self._settings["Qemu"]["vms"] = vms
# Starting with 2.0.0dev5 IOU licence is stored in the settings
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
if "IOU" in self._settings and "iourc_path" in self._settings["IOU"] and "iourc_content" not in self._settings["IOU"]:
try:
with open(self._settings["IOU"]["iourc_path"], "r") as f:
self._settings["IOU"]["iourc_content"] = f.read().replace("\r\n", "\n")
del self._settings["IOU"]["iourc_path"]
except OSError as e:
log.warn("Can't import IOU licence {}: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
def _readConfig(self, config_path):
"""
@@ -176,7 +285,7 @@ class LocalConfig(QtCore.QObject):
return dict()
def _writeConfig(self):
def writeConfig(self):
"""
Write the configuration file.
"""
@@ -191,6 +300,25 @@ class LocalConfig(QtCore.QObject):
self._last_config_changed = os.stat(self._config_file).st_mtime
except (ValueError, OSError) as e:
log.error("Could not write the config file {}: {}".format(self._config_file, e))
self.save_on_controller_signal.emit()
@qslot
def _saveOnController(self, *args):
"""
Save some settings on controller for the transition from
GUI to a central controller. Will be removed later
"""
if Controller.instance().connected() and self._settings_retrieved_from_controller:
# We save only non user specific sections
section_to_save_on_controller = ["Builtin", "Docker", "IOU", "Qemu", "VMware", "VPCS", "VirtualBox", "GraphicsView", "Dynamips"]
controller_settings = {}
for key, val in self._settings.items():
if key in section_to_save_on_controller:
controller_settings[key] = val
# We want only the VM settings on the server
elif key == "Server":
controller_settings["Server"]["vm"] = self._settings["Server"]["vm"]
Controller.instance().post("/settings", None, body=controller_settings)
def checkConfigChanged(self):
@@ -219,7 +347,7 @@ class LocalConfig(QtCore.QObject):
"""
self._config_file = config_file
self._readConfig(self._config_file)
self._resetLoadConfig()
def settings(self):
"""
@@ -239,7 +367,8 @@ class LocalConfig(QtCore.QObject):
if self._settings != settings:
self._settings.update(settings)
self._writeConfig()
self.writeConfig()
self.config_changed_signal.emit()
def loadSectionSettings(self, section, default_settings):
"""
@@ -274,7 +403,7 @@ class LocalConfig(QtCore.QObject):
if changed:
log.info("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
self._writeConfig()
self.writeConfig()
return copy.deepcopy(settings)
@@ -292,7 +421,7 @@ class LocalConfig(QtCore.QObject):
if self._settings[section] != settings:
self._settings[section].update(copy.deepcopy(settings))
log.info("Section %s has changed. Saving configuration", section)
self._writeConfig()
self.writeConfig()
else:
log.debug("Section %s has not changed. Skip saving configuration", section)
@@ -304,8 +433,30 @@ class LocalConfig(QtCore.QObject):
from gns3.settings import GENERAL_SETTINGS
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["experimental_features"]
def hdpi(self):
"""
:returns: Boolean. True if hdpi is allowed
"""
from gns3.settings import GENERAL_SETTINGS
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["hdpi"]
def multiProfiles(self):
"""
:returns: Boolean. True if multi_profiles is enabled
"""
from gns3.settings import GENERAL_SETTINGS
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["multi_profiles"]
def setMultiProfiles(self, value):
from gns3.settings import GENERAL_SETTINGS
settings = self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)
settings["multi_profiles"] = value
self.saveSectionSettings("MainWindow", settings)
@staticmethod
def instance(config_file=None):
def instance():
"""
Singleton to return only on instance of LocalConfig.
@@ -313,7 +464,7 @@ class LocalConfig(QtCore.QObject):
"""
if not hasattr(LocalConfig, "_instance") or LocalConfig._instance is None:
LocalConfig._instance = LocalConfig(config_file=config_file)
LocalConfig._instance = LocalConfig()
return LocalConfig._instance
@staticmethod
@@ -323,7 +474,7 @@ class LocalConfig(QtCore.QObject):
"""
my_pid = os.getpid()
pid_path = os.path.join(LocalConfig.configDirectory(), "gns3_gui.pid")
pid_path = os.path.join(LocalConfig.instance().configDirectory(), "gns3_gui.pid")
if os.path.exists(pid_path):
try:

599
gns3/local_server.py Normal file
View File

@@ -0,0 +1,599 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import copy
import stat
import shlex
import socket
import shutil
import random
import string
import struct
import psutil
import signal
import subprocess
from gns3.qt import QtWidgets, QtCore
from gns3.settings import LOCAL_SERVER_SETTINGS
from gns3.local_config import LocalConfig
from gns3.local_server_config import LocalServerConfig
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.http import getSynchronous
from gns3.utils.sudo import sudo
from gns3.http_client import HTTPClient
from gns3.controller import Controller
import logging
log = logging.getLogger(__name__)
class StopLocalServerWorker(QtCore.QObject):
"""
Worker for displaying a progress dialog when closing
the server
"""
# signals to update the progress dialog.
error = QtCore.pyqtSignal(str, bool)
finished = QtCore.pyqtSignal()
updated = QtCore.pyqtSignal(int)
def __init__(self, local_server_process):
super().__init__()
self._local_server_process = local_server_process
def run(self):
precision = 1
remaining_trial = 4 / precision # 4 Seconds
while remaining_trial > 0:
if self._local_server_process.returncode is None:
remaining_trial -= 1
self.thread().sleep(precision)
else:
break
self.finished.emit()
def cancel(self):
return
class LocalServer(QtCore.QObject):
"""
Manage the local server process
"""
def __init__(self, parent=None):
# Remember if the server was started by us or not
self._server_started_by_me = False
self._local_server_path = ""
self._local_server_process = None
super().__init__()
self._parent = parent
self._config_directory = LocalConfig.instance().configDirectory()
self._settings = {}
self.localServerSettings()
self._port = self._settings.get("port", 3080)
if not self._settings.get("auto_start", True):
if self._settings.get("host") is None:
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
else:
self._http_client = None
self._stopping = False
self._timer = QtCore.QTimer()
self._timer.setInterval(5000)
self._timer.timeout.connect(self._checkLocalServerRunningSlot)
self._timer.start()
def _pid_path(self):
"""
:returns: Path of the PID file
"""
return os.path.join(self._config_directory, "gns3_server.pid")
def parent(self):
"""
Parent window
"""
if self._parent is None:
from gns3.main_window import MainWindow
return MainWindow.instance()
return self._parent
def _checkWindowsService(self, service_name):
import pywintypes
import win32service
import win32serviceutil
try:
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
return False
except pywintypes.error as e:
if e.winerror == 1060:
return False
else:
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
return True
def _checkUbridgePermissions(self):
"""
Checks that uBridge can interact with network interfaces.
"""
path = os.path.abspath(self._settings["ubridge_path"])
if not path or len(path) == 0 or not os.path.exists(path):
return False
if sys.platform.startswith("win"):
# do not check anything on Windows
return True
if os.geteuid() == 0:
# we are root, so we should have privileged access.
return True
request_setuid = False
if sys.platform.startswith("linux"):
# test if the executable has the CAP_NET_RAW capability (Linux only)
try:
if "security.capability" in os.listxattr(path):
caps = os.getxattr(path, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
proceed = QtWidgets.QMessageBox.question(
self.parent(),
"uBridge",
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
sudo(["setcap", "cap_net_admin,cap_net_raw=ep"])
else:
# capabilities not supported
request_setuid = True
except AttributeError:
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
log.warning("Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)")
return True
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set CAP_NET_RAW capability to uBridge {}: {}".format(path, str(e)))
return False
if sys.platform.startswith("darwin") or request_setuid:
try:
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
proceed = QtWidgets.QMessageBox.question(
self.parent(),
"uBridge",
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set root permissions to uBridge {}: {}".format(path, str(e)))
return False
return True
def _passwordGenerate(self):
"""
Generate a random password
"""
return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(64))
def localServerSettings(self):
"""
Returns the local server settings.
:returns: local server settings (dict)
"""
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
self._settings = copy.copy(settings)
# user & password
if settings["auth"] is True and not settings["user"].strip():
settings["user"] = "admin"
settings["password"] = self._passwordGenerate()
# local GNS3 server path
local_server_path = shutil.which(settings["path"].strip())
if local_server_path is None:
default_server_path = shutil.which("gns3server")
if default_server_path is not None:
settings["path"] = os.path.abspath(default_server_path)
else:
settings["path"] = os.path.abspath(local_server_path)
# uBridge path
ubridge_path = shutil.which(settings["ubridge_path"].strip())
if ubridge_path is None:
default_ubridge_path = shutil.which("ubridge")
if default_ubridge_path is not None:
settings["ubridge_path"] = os.path.abspath(default_ubridge_path)
else:
settings["ubridge_path"] = os.path.abspath(ubridge_path)
if self._settings != settings:
self.updateLocalServerSettings(settings)
return settings
def updateLocalServerSettings(self, new_settings):
"""
Update the local server settings. Keep the key not in new_settings
"""
old_settings = copy.copy(self._settings)
if not self._settings:
self._settings = new_settings
else:
self._settings.update(new_settings)
self._port = self._settings["port"]
LocalServerConfig.instance().saveSettings("Server", self._settings)
# Settings have changed we need to restart the server
if old_settings != self._settings:
if self._settings["auto_start"]:
# We restart the local server only if we really need. Auth can be hot change
settings_require_restart = ('host', 'port', 'path')
need_restart = False
for s in settings_require_restart:
if old_settings.get(s) != self._settings.get(s):
need_restart = True
if need_restart:
self.stopLocalServer(wait=True)
self.localServerAutoStartIfRequire()
# If the controller is remote:
else:
self.stopLocalServer(wait=True)
if self._settings.get("host") is None:
self._http_client = None
else:
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
def shouldLocalServerAutoStart(self):
"""
Returns either the local server
is automatically started on startup.
:returns: boolean
"""
return self._settings["auto_start"] and self._settings["host"] is not None
def localServerPath(self):
"""
Returns the local server path.
:returns: path to local server program.
"""
return self._settings["path"]
def _killAlreadyRunningServer(self):
"""
Kill a running zombie server (started by a gui that no longer exists)
This will not kill server started by hand.
"""
try:
if os.path.exists(self._pid_path()):
with open(self._pid_path()) as f:
pid = int(f.read())
process = psutil.Process(pid=pid)
log.info("Kill already running server with PID %d", pid)
process.kill()
except (OSError, ValueError, psutil.NoSuchProcess, psutil.AccessDenied):
# Permission issue, or process no longer exists, or file is empty
return
def localServerAutoStartIfRequire(self):
"""
Try to start the embed gns3 server.
"""
if not self.shouldLocalServerAutoStart():
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
return
if self.isLocalServerRunning() and self._server_started_by_me:
return True
# We check if two gui are not launched at the same time
# to avoid killing the server of the other GUI
if not LocalConfig.isMainGui():
log.info("Not the main GUI, will not auto start the server")
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
return True
if self.isLocalServerRunning():
log.info("A local server already running on this host")
# Try to kill the server. The server can be still running after
# if the server was started by hand
self._killAlreadyRunningServer()
if not self.isLocalServerRunning():
if not self.initLocalServer():
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not start the local server process: {}".format(self._settings["path"]))
return False
if not self.startLocalServer():
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not start the local server process: {}".format(self._settings["path"]))
return False
if self.parent():
worker = WaitForConnectionWorker(self._settings["host"], self._port)
progress_dialog = ProgressDialog(worker,
"Local server",
"Connecting to server {} on port {}...".format(self._settings["host"], self._port),
"Cancel", busy=True, parent=self.parent())
progress_dialog.show()
if not progress_dialog.exec_():
return False
self._server_started_by_me = True
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
return True
def initLocalServer(self):
"""
Initialize the local server.
"""
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
self._port = self._settings["port"]
# check the local server path
local_server_path = self.localServerPath()
if not local_server_path:
log.warn("No local server is configured")
return False
if not os.path.isfile(local_server_path):
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not find local server {}".format(local_server_path))
return False
elif not os.access(local_server_path, os.X_OK):
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "{} is not an executable".format(local_server_path))
return False
try:
# check if the local address still exists
for res in socket.getaddrinfo(self._settings["host"], 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
af, socktype, proto, _, sa = res
with socket.socket(af, socktype, proto) as sock:
sock.bind(sa)
break
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not bind with {}: {} (please check your host binding setting in the preferences)".format(self._settings["host"], e))
return False
try:
# check if the port is already taken
find_unused_port = False
for res in socket.getaddrinfo(self._settings["host"], self._port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
af, socktype, proto, _, sa = res
with socket.socket(af, socktype, proto) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(sa)
break
except OSError as e:
log.warning("Could not use socket {}:{} {}".format(self._settings["host"], self._port, e))
find_unused_port = True
if find_unused_port:
# find an alternate port for the local server
old_port = self._port
try:
self._port = self._findUnusedLocalPort(self._settings["host"])
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not find an unused port for the local server: {}".format(e))
return False
log.warning("The server port {} is already in use, fallback to port {}".format(old_port, self._port))
return True
def _findUnusedLocalPort(self, host):
"""
Find an unused port.
:param host: server hosts
:returns: port number
"""
with socket.socket() as s:
s.bind((host, 0))
return s.getsockname()[1]
def startLocalServer(self):
"""
Starts the local server process.
"""
self._stopping = False
path = self.localServerPath()
command = '"{executable}" --local'.format(executable=path)
if LocalConfig.instance().profile():
command += " --profile {}".format(LocalConfig.instance().profile())
if self._settings["allow_console_from_anywhere"]:
# allow connections to console from remote addresses
command += " --allow"
if logging.getLogger().isEnabledFor(logging.DEBUG):
command += " --debug"
settings_dir = self._config_directory
if os.path.isdir(settings_dir):
# save server logging info to a file in the settings directory
logpath = os.path.join(settings_dir, "gns3_server.log")
if os.path.isfile(logpath):
# delete the previous log file
try:
os.remove(logpath)
except FileNotFoundError:
pass
except OSError as e:
log.warn("could not delete server log file {}: {}".format(logpath, e))
command += ' --log="{}" --pid="{}"'.format(logpath, self._pid_path())
log.info("Starting local server process with {}".format(command))
try:
if sys.platform.startswith("win"):
# use the string on Windows
self._local_server_process = subprocess.Popen(command, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, stderr=subprocess.PIPE)
else:
# use arguments on other platforms
args = shlex.split(command)
self._local_server_process = subprocess.Popen(args, stderr=subprocess.PIPE)
except (OSError, subprocess.SubprocessError) as e:
log.warning('Could not start local server "{}": {}'.format(command, e))
return False
log.info("Local server process has started (PID={})".format(self._local_server_process.pid))
return True
def _checkLocalServerRunningSlot(self):
if self._local_server_process and not self._stopping:
if not self.localServerProcessIsRunning():
log.error("Local server process has stopped")
try:
log.error(self._local_server_process.stderr.read().decode())
except (OSError, UnicodeDecodeError):
pass
self._local_server_process = None
def localServerProcessIsRunning(self):
"""
Returns either the local server is running.
:returns: boolean
"""
try:
if self._local_server_process and self._local_server_process.poll() is None:
return True
except OSError:
pass
return False
def isLocalServerRunning(self):
"""
Synchronous check if a server is already running on this host.
:returns: boolean
"""
status, json_data = getSynchronous(self._settings["protocol"], self._settings["host"], self._port, "version",
timeout=2, user=self._settings["user"], password=self._settings["password"])
if json_data is None or status != 200:
return False
else:
version = json_data.get("version", None)
if version is None:
log.debug("Server is not a GNS3 server")
return False
return True
def stopLocalServer(self, wait=False):
"""
Stops the local server.
:param wait: wait for the server to stop
"""
if self.localServerProcessIsRunning():
self._stopping = True
log.info("Stopping local server (PID={})".format(self._local_server_process.pid))
# local server is running, let's stop it
if self._http_client:
self._http_client.shutdown()
if wait:
worker = StopLocalServerWorker(self._local_server_process)
progress_dialog = ProgressDialog(worker, "Local server", "Waiting for the local server to stop...", None, busy=True, parent=self.parent())
progress_dialog.show()
progress_dialog.exec_()
if self._local_server_process.returncode is None:
self._killLocalServer()
self._server_started_by_me = False
def _killLocalServer(self):
# the local server couldn't be stopped with the normal procedure
try:
if sys.platform.startswith("win"):
self._local_server_process.send_signal(signal.CTRL_BREAK_EVENT)
else:
self._local_server_process.send_signal(signal.SIGINT)
# If the process is already dead we received a permission error
# it's a race condition between the timeout and send signal
except (PermissionError, SystemError):
pass
try:
# wait for the server to stop for maximum x seconds
self._local_server_process.wait(timeout=60)
except subprocess.TimeoutExpired:
proceed = QtWidgets.QMessageBox.question(self.parent(),
"Local server",
"The Local server cannot be stopped, would you like to kill it?",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
self._local_server_process.kill()
@staticmethod
def instance():
"""
Singleton to return only on instance of LocalServer.
:returns: instance of LocalServer
"""
if not hasattr(LocalServer, '_instance') or LocalServer._instance is None:
LocalServer._instance = LocalServer()
return LocalServer._instance
def main():
import pprint
pp = pprint.PrettyPrinter(indent=4)
print("Local server config")
local_server = LocalServer(False)
pp.pprint(local_server.localServerSettings())
local_server.localServerAutoStart()
local_server.stopLocalServer()
if __name__ == '__main__':
main()

View File

@@ -30,22 +30,25 @@ class LocalServerConfig:
Local server configuration.
"""
def __init__(self):
def __init__(self, config_file=None):
appname = "GNS3"
self._config = configparser.RawConfigParser()
if sys.platform.startswith("win"):
filename = "gns3_server.ini"
else:
filename = "gns3_server.conf"
if sys.platform.startswith("win"):
appdata = os.path.expandvars("%APPDATA%")
self._config_file = os.path.join(appdata, appname, filename)
if config_file:
self._config_file = config_file
else:
home = os.path.expanduser("~")
self._config_file = os.path.join(home, ".config", appname, filename)
if sys.platform.startswith("win"):
filename = "gns3_server.ini"
else:
filename = "gns3_server.conf"
from .local_config import LocalConfig
if sys.platform.startswith("win"):
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
else:
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
try:
# create the config file if it doesn't exist
@@ -54,6 +57,14 @@ class LocalServerConfig:
log.error("Could not create the local server configuration {}: {}".format(self._config_file, e))
self.readConfig()
def setConfigFile(self, path):
"""
Change the location of the server config (use for test)
"""
self._config = configparser.RawConfigParser()
self._config_file = path
self.readConfig()
def readConfig(self):
"""
Read the configuration file.
@@ -76,13 +87,12 @@ class LocalServerConfig:
except (OSError, configparser.Error) as e:
log.error("Could not write the local server configuration {}: {}".format(self._config_file, e))
def loadSettings(self, section, default_settings, types):
def loadSettings(self, section, default_settings):
"""
Get all the settings from a given section.
:param section: section name
:param default_settings: setting names and default values (dict)
:param types: setting types (dict)
:returns: settings (dict)
"""
@@ -92,14 +102,16 @@ class LocalServerConfig:
settings = {}
for name, default in default_settings.items():
if types[name] is int:
settings[name] = self._config[section].getint(name, default)
elif types[name] is bool:
if isinstance(default, bool):
settings[name] = self._config[section].getboolean(name, default)
elif types[name] is float:
elif isinstance(default, int):
settings[name] = self._config[section].getint(name, default)
elif isinstance(default, float):
settings[name] = self._config[section].getfloat(name, default)
else:
settings[name] = self._config[section].get(name, default)
if settings[name] == "None":
settings[name] = None
# sync with the config file
self.saveSettings(section, settings)

View File

@@ -85,14 +85,33 @@ class ColouredStreamHandler(logging.StreamHandler):
def init_logger(level, logfile, quiet=False):
if sys.platform.startswith("win"):
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {name}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
else:
stream_handler = ColouredStreamHandler(sys.stdout)
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{")
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {name}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{")
logging.basicConfig(level=level, handlers=[stream_handler])
log = logging.getLogger()
log.addHandler(stream_handler)
log_factory = logging.getLogRecordFactory()
def factory(name, level, fn, lno, msg, args, exc_info, func=None, sinfo=None, **kwargs):
"""
Reformat the log message to get something more clean
"""
# When qt message box is display the correct line number is a part of
# the name
if ":" in name:
name, lno = name.split(":")
lno = int(lno)
name = name.replace("gns3.", "")
try:
return log_factory(name, level, fn, lno, msg, args, exc_info, func=func, sinfo=sinfo, **kwargs)
except Exception as e: # To avoid recursion we just print the message if something is wrong when logging
print(msg)
return
logging.setLogRecordFactory(factory)
try:
try:
os.makedirs(os.path.dirname(logfile))

View File

@@ -48,7 +48,7 @@ import signal
import psutil
try:
from gns3.qt import QtCore, QtGui, QtWidgets
from gns3.qt import QtCore, QtWidgets
except ImportError:
raise SystemExit("Can't import Qt modules: Qt and/or PyQt is probably not installed correctly...")
from gns3.main_window import MainWindow
@@ -58,7 +58,7 @@ from gns3.crash_report import CrashReport
from gns3.local_config import LocalConfig
from gns3.application import Application
from gns3.utils import parse_version
from gns3.dialogs.profile_select import ProfileSelectDialog
import logging
log = logging.getLogger(__name__)
@@ -119,14 +119,14 @@ def main():
parser.add_argument("project", help="load a GNS3 project (.gns3)", metavar="path", nargs="?")
parser.add_argument("--version", help="show the version", action="version", version=__version__)
parser.add_argument("--debug", help="print out debug messages", action="store_true", default=False)
parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout")
parser.add_argument("--config", help="Configuration file")
parser.add_argument("--profile", help="Settings profile (blank will use default settings files)")
options = parser.parse_args()
exception_file_path = "exceptions.log"
if options.config:
LocalConfig.instance(config_file=options.config)
else:
LocalConfig.instance()
if options.project:
options.project = os.path.abspath(options.project)
if hasattr(sys, "frozen"):
# We add to the path where the OS search executable our binary location starting by GNS3
@@ -147,7 +147,6 @@ def main():
os.environ["PATH"] = os.pathsep.join(frozen_dirs) + os.pathsep + os.environ.get("PATH", "")
if options.project:
options.project = os.path.abspath(options.project)
os.chdir(frozen_dir)
def exceptionHook(exception, value, tb):
@@ -181,10 +180,6 @@ def main():
# catch exceptions to write them in a file
sys.excepthook = exceptionHook
current_year = datetime.date.today().year
print("GNS3 GUI version {}".format(__version__))
print("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
# we only support Python 3 version >= 3.4
if sys.version_info < (3, 4):
raise SystemExit("Python 3.4 or higher is required")
@@ -225,34 +220,64 @@ def main():
except win32console.error as e:
print("warning: could not allocate console: {}".format(e))
local_config = LocalConfig.instance()
global app
app = Application(sys.argv)
app = Application(sys.argv, hdpi=local_config.hdpi())
if local_config.multiProfiles() and not options.profile:
profile_select = ProfileSelectDialog()
profile_select.show()
profile_select.exec_()
options.profile = profile_select.profile()
# Init the config
if options.config:
local_config.setConfigFilePath(options.config)
elif options.profile:
local_config.setProfile(options.profile)
# save client logging info to a file
logfile = os.path.join(LocalConfig.configDirectory(), "gns3_gui.log")
logfile = os.path.join(LocalConfig.instance().configDirectory(), "gns3_gui.log")
# on debug enable logging to stdout
if options.debug:
root_logger = init_logger(logging.DEBUG, logfile)
init_logger(logging.DEBUG, logfile)
elif options.quiet:
init_logger(logging.ERROR, logfile)
else:
root_logger = init_logger(logging.INFO, logfile)
init_logger(logging.INFO, logfile)
current_year = datetime.date.today().year
log.info("GNS3 GUI version {}".format(__version__))
log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
log.info("Application started with {}".format("".join(sys.argv)))
# update the exception file path to have it in the same directory as the settings file.
exception_file_path = os.path.join(LocalConfig.configDirectory(), exception_file_path)
exception_file_path = os.path.join(LocalConfig.instance().configDirectory(), exception_file_path)
# We disallow to run GNS3 from outside the /Applications folder to avoid
# 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.")
sys.exit(1)
global mainwindow
mainwindow = MainWindow()
startup_file = app.open_file_at_startup
if not startup_file:
startup_file = options.project
mainwindow = MainWindow(open_file=startup_file)
# On OSX we can receive the file to open from a system event
# loadPath is smart and will load only if a path is present
mainwindow.ready_signal.connect(lambda: mainwindow.loadPath(app.open_file_at_startup))
mainwindow.ready_signal.connect(lambda: mainwindow.loadPath(options.project))
app.file_open_signal.connect(lambda path: mainwindow.loadPath(path))
# Manage Ctrl + C or kill command
def sigint_handler(*args):
log.info("Signal received exiting the application")
mainwindow.setSoftExit(False)
app.closeAllWindows()
orig_sigint = signal.signal(signal.SIGINT, sigint_handler)
orig_sigterm = signal.signal(signal.SIGTERM, sigint_handler)
@@ -264,7 +289,6 @@ def main():
signal.signal(signal.SIGINT, orig_sigint)
signal.signal(signal.SIGTERM, orig_sigterm)
delattr(MainWindow, "_instance")
# We force deleting the app object otherwise it's segfault on Fedora

File diff suppressed because it is too large Load Diff

View File

@@ -24,4 +24,4 @@ from gns3.modules.qemu import Qemu
from gns3.modules.vmware import VMware
from gns3.modules.docker import Docker
MODULES = [VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker, Builtin]
MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker]

View File

@@ -20,10 +20,23 @@ Built-in module implementation.
"""
from gns3.qt import QtWidgets
from gns3.local_config import LocalConfig
from ..module import Module
from .cloud import Cloud
from .host import Host
from .nat import Nat
from .ethernet_hub import EthernetHub
from .ethernet_switch import EthernetSwitch
from .frame_relay_switch import FrameRelaySwitch
from .atm_switch import ATMSwitch
from .settings import (
BUILTIN_SETTINGS,
CLOUD_SETTINGS,
NAT_SETTINGS,
ETHERNET_HUB_SETTINGS,
ETHERNET_SWITCH_SETTINGS
)
import logging
log = logging.getLogger(__name__)
@@ -38,11 +51,144 @@ class Builtin(Module):
def __init__(self):
super().__init__()
self._settings = {}
self._nodes = []
self._cloud_nodes = {}
self._nat_nodes = {}
self._ethernet_hubs = {}
self._ethernet_switches = {}
# load the settings
self._loadSettings()
def configChangedSlot(self):
pass
def settings(self):
"""
Returns the module settings
:returns: module settings (dictionary)
"""
return self._settings
def setSettings(self, settings):
"""Sets the module settings
:param settings: module settings (dictionary)
"""
self._settings.update(settings)
self._saveSettings()
def _saveSettings(self):
"""
Saves the settings to the persistent settings file.
"""
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
def _loadSettings(self):
"""
Loads the settings from the persistent settings file.
"""
local_config = LocalConfig.instance()
self._settings = local_config.loadSectionSettings(self.__class__.__name__, BUILTIN_SETTINGS)
self._loadNodes()
def _loadBuilinNodesPerType(self, node_dict, node_type, default_settings):
settings = LocalConfig.instance().settings()
if node_type in settings.get(self.__class__.__name__, {}):
for device in settings[self.__class__.__name__][node_type]:
name = device.get("name")
server = device.get("server")
key = "{server}:{name}".format(server=server, name=name)
if key in node_dict or not name or not server:
continue
node_settings = default_settings.copy()
node_settings.update(device)
node_dict[key] = node_settings
def _loadNodes(self):
"""
Load the built-in nodes from the persistent settings file.
"""
self._loadBuilinNodesPerType(self._cloud_nodes, "cloud_nodes", CLOUD_SETTINGS)
self._loadBuilinNodesPerType(self._ethernet_hubs, "ethernet_hubs", ETHERNET_HUB_SETTINGS)
self._loadBuilinNodesPerType(self._ethernet_switches, "ethernet_switches", ETHERNET_SWITCH_SETTINGS)
def _saveNodes(self):
"""
Saves the built-in nodes to the persistent settings file.
"""
self._settings["cloud_nodes"] = list(self._cloud_nodes.values())
self._settings["ethernet_hubs"] = list(self._ethernet_hubs.values())
self._settings["ethernet_switches"] = list(self._ethernet_switches.values())
self._saveSettings()
def cloudNodes(self):
"""
Returns cloud nodes settings.
:returns: Cloud nodes settings (dictionary)
"""
return self._cloud_nodes
def setCloudNodes(self, new_cloud_nodes):
"""
Sets cloud nodes settings.
:param new_cloud_nodes: cloud nodes settings (dictionary)
"""
self._cloud_nodes = new_cloud_nodes.copy()
self._saveNodes()
def ethernetHubs(self):
"""
Returns Ethernet hubs settings.
:returns: Ethernet hubs settings (dictionary)
"""
return self._ethernet_hubs
def setEthernetHubs(self, new_ethernet_hubs):
"""
Sets Ethernet hubs settings.
:param new_ethernet_hubs: Ethernet hubs settings (dictionary)
"""
self._ethernet_hubs = new_ethernet_hubs.copy()
self._saveNodes()
def ethernetSwitches(self):
"""
Returns Ethernet switches settings.
:returns: Ethernet switches settings (dictionary)
"""
return self._ethernet_switches
def setEthernetSwitches(self, new_ethernet_switches):
"""
Sets Ethernet switches settings.
:param new_ethernet_switches: Ethernet switches settings (dictionary)
"""
self._ethernet_switches = new_ethernet_switches.copy()
self._saveNodes()
def addNode(self, node):
"""
Adds a node to this module.
@@ -67,33 +213,55 @@ class Builtin(Module):
Resets the module.
"""
log.info("Built-in module reset")
self._nodes.clear()
def createNode(self, node_class, server, project):
def instantiateNode(self, node_class, server, project):
"""
Creates a new node.
Instantiate a new node.
:param node_class: Node object
:param server: HTTPClient instance
:param project: Project instance
"""
log.info("creating node {}".format(node_class))
log.info("instantiating node {}".format(node_class))
# create an instance of the node class
return node_class(self, server, project)
def setupNode(self, node, node_name):
def createNode(self, node, node_name):
"""
Setups a node.
Creates a node.
:param node: Node instance
:param node_name: Node name
"""
log.info("configuring node {}".format(node))
node.setup()
log.info("creating node {}".format(node))
if isinstance(node, Cloud):
for key, info in self._cloud_nodes.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
return
elif isinstance(node, Nat):
for key, info in self._nat_nodes.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(default_name_format=default_name_format)
return
elif isinstance(node, EthernetHub):
for key, info in self._ethernet_hubs.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
return
elif isinstance(node, EthernetSwitch):
for key, info in self._ethernet_switches.items():
if node_name == info["name"]:
default_name_format = info["default_name_format"].replace('{name}', node_name)
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
return
node.create()
@staticmethod
def findAlternativeInterface(node, missing_interface):
@@ -129,6 +297,22 @@ class Builtin(Module):
return globals()[name]
return None
@staticmethod
def getNodeType(name, platform=None):
if name == "cloud":
return Cloud
elif name == "nat":
return Nat
elif name == "ethernet_hub":
return EthernetHub
elif name == "ethernet_switch":
return EthernetSwitch
elif name == "frame_relay_switch":
return FrameRelaySwitch
elif name == "atm_switch":
return ATMSwitch
return None
@staticmethod
def classes():
"""
@@ -137,7 +321,7 @@ class Builtin(Module):
:returns: list of classes
"""
return [Cloud, Host]
return [Nat, Cloud, EthernetHub, EthernetSwitch, FrameRelaySwitch, ATMSwitch]
def nodes(self):
"""
@@ -152,8 +336,44 @@ class Builtin(Module):
"name": node_class.symbolName(),
"categories": node_class.categories(),
"symbol": node_class.defaultSymbol(),
"builtin": True}
"builtin": True,
"node_type": node_class.URL_PREFIX
}
)
# add custom cloud node templates
for cloud_node in self._cloud_nodes.values():
nodes.append(
{"class": Cloud.__name__,
"name": cloud_node["name"],
"server": cloud_node["server"],
"symbol": cloud_node["symbol"],
"categories": [cloud_node["category"]]
}
)
# add custom Ethernet hub templates
for hub in self._ethernet_hubs.values():
nodes.append(
{"class": EthernetHub.__name__,
"name": hub["name"],
"server": hub["server"],
"symbol": hub["symbol"],
"categories": [hub["category"]]
}
)
# add custom Ethernet switch templates
for switch in self._ethernet_switches.values():
nodes.append(
{"class": EthernetSwitch.__name__,
"name": switch["name"],
"server": switch["server"],
"symbol": switch["symbol"],
"categories": [switch["category"]]
}
)
return nodes
@staticmethod
@@ -162,7 +382,12 @@ class Builtin(Module):
:returns: QWidget object list
"""
return []
from .pages.builtin_preferences_page import BuiltinPreferencesPage
from .pages.cloud_preferences_page import CloudPreferencesPage
from .pages.ethernet_hub_preferences_page import EthernetHubPreferencesPage
from .pages.ethernet_switch_preferences_page import EthernetSwitchPreferencesPage
return [BuiltinPreferencesPage, EthernetHubPreferencesPage, EthernetSwitchPreferencesPage, CloudPreferencesPage]
@staticmethod
def instance():

View File

@@ -0,0 +1,229 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import uuid
from gns3.node import Node
import logging
log = logging.getLogger(__name__)
class ATMSwitch(Node):
"""
ATM switch.
:param module: parent module for this node
:param server: GNS3 server instance
:param project: Project instance
"""
URL_PREFIX = "atm_switch"
def __init__(self, module, server, project):
super().__init__(module, server, project)
# this is an always-on node
self.setStatus(Node.started)
self._always_on = True
self.settings().update({"mappings": {}})
def create(self, name=None, node_id=None, mappings=None, default_name_format="ATM{0}"):
"""
Creates this ATM switch.
:param name: optional name for this switch.
:param node_id: Node identifier on the server
:param mappings: mappings to be automatically added when creating this ATM switch
"""
params = {}
if mappings:
params["mappings"] = mappings
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
:param result: server response (dict)
"""
self.settings()["mappings"] = result["mappings"]
def update(self, new_settings):
"""
Updates the settings for this ATM switch.
:param new_settings: settings dictionary
"""
params = {}
for name, value in new_settings.items():
if name in self._settings and self._settings[name] != value:
params[name] = value
if params:
self._update(params)
def _updateCallback(self, result):
"""
Callback for update.
:param result: server response
"""
self.settings()["mappings"] = result["mappings"]
def info(self):
"""
Returns information about this ATM switch.
:returns: formatted string
"""
info = """ATM switch {name} is always-on
Local node ID is {id}
Server's Node ID is {node_id}
Hardware is Dynamips emulated simple ATM switch
Switch's server runs on {host}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self._compute.name())
port_info = ""
mapping = re.compile(r"""^([0-9]*):([0-9]*):([0-9]*)$""")
for port in self._ports:
if port.isFree():
port_info += " Port {} is empty\n".format(port.name())
else:
port_info += " Port {name} {description}\n".format(name=port.name(),
description=port.description())
for source, destination in self._settings["mappings"].items():
match_source_mapping = mapping.search(source)
match_destination_mapping = mapping.search(destination)
if match_source_mapping and match_destination_mapping:
source_port, source_vpi, source_vci = match_source_mapping.group(1, 2, 3)
destination_port, destination_vpi, destination_vci = match_destination_mapping.group(1, 2, 3)
else:
source_port, source_vpi = source.split(":")
destination_port, destination_vpi = destination.split(":")
source_vci = destination_vci = 0
if port.name() == source_port or port.name() == destination_port:
if port.name() == source_port:
vpi1 = source_vpi
vci1 = source_vci
port = destination_port
vci2 = destination_vci
vpi2 = destination_vpi
else:
vpi1 = destination_vpi
vci1 = destination_vci
port = source_port
vci2 = source_vci
vpi2 = source_vpi
if vci1 and vci2:
port_info += " incoming VPI {vpi1} and VCI {vci1} is switched to port {port} outgoing VPI {vpi2} and VCI {vci2}\n".format(vpi1=vpi1,
vci1=vci1,
port=port,
vpi2=vpi2,
vci2=vci2)
else:
port_info += " incoming VPI {vpi1} is switched to port {port} outgoing VPI {vpi2}\n".format(vpi1=vpi1,
port=port,
vpi2=vpi2)
break
return info + port_info
def dump(self):
"""
Returns a representation of this ATM switch
(to be saved in a topology file).
:returns: dictionary
"""
atmsw = super().dump()
if self._settings["mappings"]:
atmsw["properties"]["mappings"] = self._settings["mappings"]
return atmsw
def load(self, node_info):
"""
Loads an ATM switch representation
(from a topology file).
:param node_info: representation of the node (dictionary)
"""
super().load(node_info)
properties = node_info["properties"]
name = properties.pop("name")
# ATM switches do not have an UUID before version 2.0
node_id = properties.get("node_id", str(uuid.uuid4()))
mappings = {}
if "mappings" in properties:
mappings = properties["mappings"]
log.info("ATM switch {} is loading".format(name))
self.create(name, node_id, mappings)
def configPage(self):
"""
Returns the configuration page widget to be used by the node properties dialog.
:returns: QWidget object
"""
from .pages.atm_switch_configuration_page import ATMSwitchConfigurationPage
return ATMSwitchConfigurationPage
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this node.
:returns: symbol path (or resource).
"""
return ":/symbols/atm_switch.svg"
@staticmethod
def symbolName():
return "ATM switch"
@staticmethod
def categories():
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node categories
"""
return [Node.switches]
def __str__(self):
return "ATM switch"

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -15,21 +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/>.
"""
NIO implementation on the client side (in the form of a pseudo node represented as a cloud).
"""
import re
from gns3.node import Node
from gns3.ports.port import Port
from gns3.nios.nio_generic_ethernet import NIOGenericEthernet
from gns3.nios.nio_linux_ethernet import NIOLinuxEthernet
from gns3.nios.nio_nat import NIONAT
from gns3.nios.nio_udp import NIOUDP
from gns3.nios.nio_tap import NIOTAP
from gns3.nios.nio_unix import NIOUNIX
from gns3.nios.nio_vde import NIOVDE
from gns3.nios.nio_null import NIONull
import logging
log = logging.getLogger(__name__)
@@ -38,238 +24,53 @@ log = logging.getLogger(__name__)
class Cloud(Node):
"""
Dynamips cloud.
Cloud node
:param module: parent module for this node
:param server: GNS3 server instance
:param project: Project instance
"""
_name_instance_count = 1
URL_PREFIX = "cloud"
def __init__(self, module, server, project):
super().__init__(module, server, project)
self.setStatus(Node.started)
self._always_on = True
self._interfaces = {}
self._cloud_settings = {"ports_mapping": []}
self.settings().update(self._cloud_settings)
log.info("cloud is being created")
# create an unique id and name
self._name_id = Cloud._name_instance_count
Cloud._name_instance_count += 1
def interfaces(self):
name = "Cloud {}".format(self._name_id)
self.setStatus(Node.started) # this is an always-on node
self._initial_settings = None
self._settings = {"name": name,
"interfaces": {},
"nios": []}
return self._interfaces
def delete(self):
def create(self, name=None, node_id=None, ports=None, default_name_format="Cloud{0}"):
"""
Deletes this cloud.
"""
# first delete all the links attached to this node
self.delete_links_signal.emit()
self.deleted_signal.emit()
def setup(self, name=None, additional_settings={}):
"""
Setups this cloud.
Creates this cloud.
:param name: optional name for this cloud
:param node_id: Node identifier on the server
:param ports: ports to be automatically added when creating this cloud
"""
if name:
self._settings["name"] = name
params = {}
if ports:
params["ports_mapping"] = ports
self._create(name, node_id, params, default_name_format)
if additional_settings and "nios" in additional_settings:
self._settings["nios"] = additional_settings["nios"]
self._server.get("/interfaces", self._setupCallback)
def _setupCallback(self, result, error=False, **kwargs):
def _createCallback(self, result, error=False, **kwargs):
"""
Callback for setup.
Callback for create.
:param result: server response
:param error: indicates an error (boolean)
"""
if "ports_mapping" in result:
self._settings["ports_mapping"] = result["ports_mapping"].copy()
if error:
log.error("error while setting up {}: {}".format(self.name(), result["message"]))
# a warning message instead of a error is more appropriate here
self.warning_signal.emit(self.id(), result["message"])
else:
self._settings["interfaces"] = result.copy()
if self._settings["nios"]:
self._addPorts(self._settings["nios"])
if self._loading:
self.loaded_signal.emit()
else:
self.setInitialized(True)
log.info("cloud {} has been created".format(self.name()))
self.created_signal.emit(self.id())
self._module.addNode(self)
def _createNIOUDP(self, nio):
"""
Creates a NIO UDP.
:param nio: nio string
"""
match = re.search(r"""^nio_udp:(\d+):(.+):(\d+)$""", nio)
if match:
lport = int(match.group(1))
rhost = match.group(2)
rport = int(match.group(3))
return NIOUDP(lport, rhost, rport)
return None
def _createNIOGenericEthernet(self, nio):
"""
Creates a NIO Generic Ethernet.
:param nio: nio string
"""
match = re.search(r"""^nio_gen_eth:(.+)$""", nio)
if match:
ethernet_device = match.group(1)
return NIOGenericEthernet(ethernet_device)
return None
def _createNIOLinuxEthernet(self, nio):
"""
Creates a NIO Linux Ethernet.
:param nio: nio string
"""
match = re.search(r"""^nio_gen_linux:(.+)$""", nio)
if match:
linux_device = match.group(1)
return NIOLinuxEthernet(linux_device)
return None
def _createNIONAT(self, nio):
"""
Creates a NIO NAT.
:param nio: nio string
"""
match = re.search(r"""^nio_nat:(.+)$""", nio)
if match:
identifier = match.group(1)
return NIONAT(identifier)
return None
def _createNIOTAP(self, nio):
"""
Creates a NIO TAP.
:param nio: nio string
"""
match = re.search(r"""^nio_tap:(.+)$""", nio)
if match:
tap_device = match.group(1)
return NIOTAP(tap_device)
return None
def _createNIOUNIX(self, nio):
"""
Creates a NIO UNIX.
:param nio: nio string
"""
match = re.search(r"""^nio_unix:(.+):(.+)$""", nio)
if match:
local_file = match.group(1)
remote_file = match.group(2)
return NIOUNIX(local_file, remote_file)
return None
def _createNIOVDE(self, nio):
"""
Creates a NIO VDE.
:param nio: nio string
"""
match = re.search(r"""^nio_vde:(.+):(.+)$""", nio)
if match:
control_file = match.group(1)
local_file = match.group(2)
return NIOVDE(control_file, local_file)
return None
def _createNIONull(self, nio):
"""
Creates a NIO Null.
:param nio: nio string
"""
match = re.search(r"""^nio_null:(.+)$""", nio)
if match:
identifier = match.group(1)
return NIONull(identifier)
return None
def _allocateNIO(self, nio):
"""
Allocate a new NIO object.
:param nio: NIO description
:returns: NIO instance
"""
nio_object = None
if nio.lower().startswith("nio_udp"):
nio_object = self._createNIOUDP(nio)
if nio.lower().startswith("nio_gen_eth"):
nio_object = self._createNIOGenericEthernet(nio)
if nio.lower().startswith("nio_gen_linux"):
nio_object = self._createNIOLinuxEthernet(nio)
if nio.lower().startswith("nio_nat"):
nio_object = self._createNIONAT(nio)
if nio.lower().startswith("nio_tap"):
nio_object = self._createNIOTAP(nio)
if nio.lower().startswith("nio_unix"):
nio_object = self._createNIOUNIX(nio)
if nio.lower().startswith("nio_vde"):
nio_object = self._createNIOVDE(nio)
if nio.lower().startswith("nio_null"):
nio_object = self._createNIONull(nio)
if nio_object is None:
log.error("Could not create NIO object from {}".format(nio))
return nio_object
def _addPorts(self, nios, ignore_existing_nio=False):
"""
Adds adapters.
:param adapters: number of adapters
"""
# add ports
for nio in nios:
if ignore_existing_nio and nio in self._settings["nios"]:
# port already created for this NIO
continue
nio_object = self._allocateNIO(nio)
if nio_object is None:
continue
port = Port(nio, nio_object, stub=True)
port.setStatus(Port.started)
self._ports.append(port)
log.debug("port {} has been added".format(nio))
if "interfaces" in result:
self._interfaces = result["interfaces"].copy()
def update(self, new_settings):
"""
@@ -278,45 +79,35 @@ class Cloud(Node):
:param new_settings: settings dictionary
"""
updated = False
if "nios" in new_settings:
nios = new_settings["nios"]
self._addPorts(nios, ignore_existing_nio=True)
updated = True
params = {}
for name, value in new_settings.items():
if name in self._settings and self._settings[name] != value:
params[name] = value
if params:
self._update(params)
# delete ports
for nio in self._settings["nios"]:
if nio not in nios:
for port in self._ports.copy():
if port.name() == nio:
self._ports.remove(port)
updated = True
log.debug("port {} has been deleted".format(nio))
break
def _updateCallback(self, result):
"""
Callback for update.
self._settings["nios"] = new_settings["nios"].copy()
:param result: server response
"""
if "name" in new_settings and new_settings["name"] != self.name():
self._settings["name"] = new_settings["name"]
updated = True
if "ports_mapping" in result:
self._settings["ports_mapping"] = result["ports_mapping"].copy()
if updated:
log.info("cloud {} has been updated".format(self.name()))
self.updated_signal.emit()
def deleteNIO(self, port):
pass
if "interfaces" in result:
self._interfaces = result["interfaces"].copy()
def info(self):
"""
Returns information about this cloud.
:returns: formated string
:returns: formatted string
"""
info = """Cloud device {name} is always-on
This is a pseudo-device for external connections
This is a node for external connections
""".format(name=self.name())
port_info = ""
@@ -327,124 +118,8 @@ This is a pseudo-device for external connections
port_info += " Port {name} {description}\n".format(name=port.name(),
description=port.description())
# add the Windows interface name
match = re.search(r"""^nio_gen_eth:(\\device\\npf_.+)$""", port.name())
if match:
for interface in self._settings["interfaces"]:
if interface["name"].lower() == match.group(1):
port_info += " Windows name: {}\n".format(interface["description"])
break
return info + port_info
def dump(self):
"""
Returns a representation of this cloud
(to be saved in a topology file).
:returns: representation of the node (dictionary)
"""
cloud = {"id": self.id(),
"type": self.__class__.__name__,
"description": str(self),
"properties": {"name": self.name(),
"nios": self._settings["nios"]},
"server_id": self._server.id()}
# add the ports
if self._ports:
ports = cloud["ports"] = []
for port in self._ports:
ports.append(port.dump())
return cloud
def load(self, node_info):
"""
Loads a cloud representation
(from a topology file).
:param node_info: representation of the node (dictionary)
"""
settings = node_info["properties"]
name = settings.pop("name")
log.info("cloud {} is loading".format(name))
self.setName(name)
self._loading = True
self._node_info = node_info
self.loaded_signal.connect(self._updatePortSettings)
self.setup(name, additional_settings=settings)
def _updatePortSettings(self):
"""
Updates port settings when loading a topology.
"""
self.loaded_signal.disconnect(self._updatePortSettings)
# update the port with the correct IDs
if "ports" in self._node_info:
ports = self._node_info["ports"]
for topology_port in ports:
for port in self._ports:
if topology_port["name"] == port.name():
port.setId(topology_port["id"])
if topology_port["name"].startswith("nio_gen_eth") or topology_port["name"].startswith("nio_linux_eth"):
# lookup if the interface exists
available_interface = False
topology_port_name = topology_port["name"].split(':', 1)[1]
for interface in self._settings["interfaces"]:
if interface["name"] == topology_port_name:
available_interface = True
break
if not available_interface:
alternative_interface = self._module.findAlternativeInterface(self, topology_port_name)
if alternative_interface:
if topology_port["name"] in self._settings["nios"]:
self._settings["nios"].remove(topology_port["name"])
topology_port["name"] = topology_port["name"].replace(topology_port_name, alternative_interface)
nio = self._allocateNIO(topology_port["name"])
port.setDefaultNio(nio)
port.setName(topology_port["name"])
self._settings["nios"].append(topology_port["name"])
# now we can set the node as initialized and trigger the created signal
self.setInitialized(True)
log.info("cloud {} has been loaded".format(self.name()))
self.created_signal.emit(self.id())
self._module.addNode(self)
self._loading = False
self._node_info = None
def name(self):
"""
Returns the name of this cloud.
:returns: name (string)
"""
return self._settings["name"]
def settings(self):
"""
Returns all this cloud settings.
:returns: settings dictionary
"""
return self._settings
def ports(self):
"""
Returns all the ports for this cloud.
:returns: list of Port instances
"""
return self._ports
def configPage(self):
"""
Returns the configuration page widget to be used by the node properties dialog.
@@ -475,7 +150,7 @@ This is a pseudo-device for external connections
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node category (integer)
:returns: list of node categories
"""
return [Node.end_devices]

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Wizard for cloud nodes.
"""
from gns3.qt import QtGui, QtWidgets
from gns3.node import Node
from gns3.dialogs.vm_wizard import VMWizard
from ..ui.cloud_wizard_ui import Ui_CloudNodeWizard
from .. import Builtin
class CloudWizard(VMWizard, Ui_CloudNodeWizard):
"""
Wizard to create a cloud node template.
:param parent: parent widget
"""
def __init__(self, cloud_nodes, parent):
super().__init__(cloud_nodes, parent)
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/cloud.svg"))
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
def getSettings(self):
"""
Returns the settings set in this Wizard.
:return: settings dict
"""
settings = {"name": self.uiNameLineEdit.text(),
"symbol": ":/symbols/cloud.svg",
"category": Node.end_devices,
"server": self._compute_id}
return settings

View File

@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Wizard for Ethernet hubs.
"""
from gns3.qt import QtGui, QtWidgets
from gns3.node import Node
from gns3.dialogs.vm_wizard import VMWizard
from ..ui.ethernet_hub_wizard_ui import Ui_EthernetHubWizard
from .. import Builtin
class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
"""
Wizard to create an Ethernet hub template.
:param parent: parent widget
"""
def __init__(self, ethernet_hubs, parent):
super().__init__(ethernet_hubs, parent)
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/hub.svg"))
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
def getSettings(self):
"""
Returns the settings set in this Wizard.
:return: settings dict
"""
ports = []
for port_number in range(0, self.uiPortsSpinBox.value()):
ports.append({"port_number": int(port_number),
"name": "Ethernet{}".format(port_number)})
settings = {"name": self.uiNameLineEdit.text(),
"symbol": ":/symbols/hub.svg",
"category": Node.switches,
"server": self._compute_id,
"ports_mapping": ports}
return settings

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Wizard for Ethernet switches.
"""
from gns3.qt import QtGui, QtWidgets
from gns3.node import Node
from gns3.dialogs.vm_wizard import VMWizard
from ..ui.ethernet_switch_wizard_ui import Ui_EthernetSwitchWizard
from .. import Builtin
class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
"""
Wizard to create an Ethernet switch template.
:param parent: parent widget
"""
def __init__(self, ethernet_switches, parent):
super().__init__(ethernet_switches, parent)
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/ethernet_switch.svg"))
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
def getSettings(self):
"""
Returns the settings set in this Wizard.
:return: settings dict
"""
ports = []
for port_number in range(0, self.uiPortsSpinBox.value()):
ports.append({"port_number": int(port_number),
"name": "Ethernet{}".format(port_number),
"type": "access",
"vlan": 1,
"ethertype": ""})
settings = {"name": self.uiNameLineEdit.text(),
"symbol": ":/symbols/ethernet_switch.svg",
"category": Node.switches,
"server": self._compute_id,
"ports_mapping": ports}
return settings

View File

@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3.node import Node
import logging
log = logging.getLogger(__name__)
class EthernetHub(Node):
"""
Ethernet hub.
:param module: parent module for this node
:param server: GNS3 server instance
:param project: Project instance
"""
URL_PREFIX = "ethernet_hub"
def __init__(self, module, server, project):
super().__init__(module, server, project)
# this is an always-on node
self.setStatus(Node.started)
self._always_on = True
self.settings().update({"ports_mapping": []})
def create(self, name=None, node_id=None, ports=None, default_name_format="Hub{0}"):
"""
Creates this hub.
:param name: optional name for this hub
:param node_id: node identifier on the server
:param ports: ports to automatically be added when creating this hub
"""
params = {}
if ports:
params["ports_mapping"] = ports
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
:param result: server response (dict)
"""
self.settings()["ports_mapping"] = result["ports_mapping"]
def update(self, new_settings):
"""
Updates the settings for this Ethernet hub.
:param new_settings: settings dictionary
"""
params = {}
if "name" in new_settings:
params["name"] = new_settings["name"]
if "ports_mapping" in new_settings:
params["ports_mapping"] = new_settings["ports_mapping"]
if params:
self._update(params)
def _updateCallback(self, result):
"""
Callback for update.
:param result: server response
"""
self.settings()["ports_mapping"] = result["ports_mapping"]
def info(self):
"""
Returns information about this Ethernet hub.
:returns: formatted string
"""
info = """Ethernet hub {name} is always-on
Local node ID is {id}
Server's node ID is {node_id}
Hub's server runs on {host}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self.compute().name())
port_info = ""
for port in self._ports:
if port.isFree():
port_info += " Port {} is empty\n".format(port.name())
else:
port_info += " Port {name} {description}\n".format(name=port.name(),
description=port.description())
return info + port_info
def configPage(self):
"""
Returns the configuration page widget to be used by the node properties dialog.
:returns: QWidget object
"""
from .pages.ethernet_hub_configuration_page import EthernetHubConfigurationPage
return EthernetHubConfigurationPage
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this node.
:returns: symbol path (or resource).
"""
return ":/symbols/hub.svg"
@staticmethod
def symbolName():
return "Ethernet hub"
@staticmethod
def categories():
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node categories
"""
return [Node.switches]
def __str__(self):
return "Ethernet hub"

View File

@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3.node import Node
import logging
log = logging.getLogger(__name__)
class EthernetSwitch(Node):
"""
Ethernet switch.
:param module: parent module for this node
:param server: GNS3 server instance
:param project: Project instance
"""
URL_PREFIX = "ethernet_switch"
def __init__(self, module, server, project):
super().__init__(module, server, project)
# this is an always-on node
self.setStatus(Node.started)
self._always_on = True
self.settings().update({"ports_mapping": []})
def create(self, name=None, node_id=None, ports=None, default_name_format="SW{0}"):
"""
Creates this Ethernet switch.
:param name: optional name for this switch
:param node_id: node identifier on the server
:param ports: ports to be automatically added when creating this switch
"""
params = {}
if ports:
params["ports_mapping"] = ports
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
:param result: server response (dict)
"""
self.settings()["ports_mapping"] = result["ports_mapping"]
def update(self, new_settings):
"""
Updates the settings for this Ethernet switch.
:param new_settings: settings dictionary
"""
params = {}
for name, value in new_settings.items():
if name in self._settings and self._settings[name] != value:
params[name] = value
if params:
self._update(params)
def _updateCallback(self, result):
"""
Callback for update.
:param result: server response
"""
self.settings()["ports_mapping"] = result["ports_mapping"]
def info(self):
"""
Returns information about this Ethernet switch.
:returns: formatted string
"""
info = """Ethernet switch {name} is always-on
Local node ID is {id}
Server's Node ID is {node_id}
Switch's server runs on {host}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self.compute().name())
port_info = ""
for port in self._ports:
if port.isFree():
port_info += " Port {} is empty\n".format(port.name())
else:
for port_settings in self._settings["ports_mapping"]:
if port_settings["port_number"] == port.portNumber():
port_type = port_settings["type"]
port_ethertype = port_settings.get("ethertype", "")
port_vlan = port_settings["vlan"]
port_ethertype_info = ""
if port_type == "access":
port_vlan_info = "VLAN ID {}".format(port_vlan)
elif port_type == "dot1q":
port_vlan_info = "native VLAN {}".format(port_vlan)
elif port_type == "qinq":
port_vlan_info = "outer VLAN {}".format(port_vlan)
port_ethertype_info = "({})".format(port_ethertype)
port_info += " Port {name} is in {port_type} {port_ethertype_info} mode, with {port_vlan_info},\n".format(name=port.name(),
port_type=port_type,
port_ethertype_info=port_ethertype_info,
port_vlan_info=port_vlan_info)
port_info += " {port_description}\n".format(port_description=port.description())
break
return info + port_info
def configPage(self):
"""
Returns the configuration page widget to be used by the node properties dialog.
:returns: QWidget object
"""
from .pages.ethernet_switch_configuration_page import EthernetSwitchConfigurationPage
return EthernetSwitchConfigurationPage
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this node.
:returns: symbol path (or resource).
"""
return ":/symbols/ethernet_switch.svg"
@staticmethod
def symbolName():
return "Ethernet switch"
@staticmethod
def categories():
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node categories
"""
return [Node.switches]
def __str__(self):
return "Ethernet switch"

View File

@@ -0,0 +1,171 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3.node import Node
import logging
log = logging.getLogger(__name__)
class FrameRelaySwitch(Node):
"""
Frame-Relay switch.
:param module: parent module for this node
:param server: GNS3 server instance
:param project: Project instance
"""
URL_PREFIX = "frame_relay_switch"
def __init__(self, module, server, project):
super().__init__(module, server, project)
# this is an always-on node
self.setStatus(Node.started)
self._always_on = True
self.settings().update({"mappings": {}})
def create(self, name=None, node_id=None, mappings={}, default_name_format="FR{0}"):
"""
Creates this Frame Relay switch.
:param name: name for this switch.
:param node_id: node identifier on the server
:param mappings: mappings to be automatically added when creating this Frame relay switch
"""
params = {}
if mappings:
params["mappings"] = mappings
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result):
"""
Callback for create.
:param result: server response (dict)
"""
self.settings()["mappings"] = result["mappings"]
def update(self, new_settings):
"""
Updates the settings for this Frame Relay switch.
:param new_settings: settings dictionary
"""
params = {}
for name, value in new_settings.items():
if name in self._settings and self._settings[name] != value:
params[name] = value
if params:
self._update(params)
def _updateCallback(self, result):
"""
Callback for update.
:param result: server response
"""
self.settings()["mappings"] = result["mappings"]
def info(self):
"""
Returns information about this Frame Relay switch.
:returns: formatted string
"""
info = """Frame relay switch {name} is always-on
Local node ID is {id}
Server's Node ID is {node_id}
Hardware is Dynamips emulated simple Frame relay switch
Switch's server runs on {host}:{port}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
host=self._compute.host(),
port=self._compute.port())
port_info = ""
for port in self._ports:
if port.isFree():
port_info += " Port {} is empty\n".format(port.name())
else:
port_info += " Port {name} {description}\n".format(name=port.name(),
description=port.description())
for source, destination in self._settings["mappings"].items():
source_port, source_dlci = source.split(":")
destination_port, destination_dlci = destination.split(":")
if port.name() == source_port or port.name() == destination_port:
if port.name() == source_port:
dlci1 = source_dlci
port = destination_port
dlci2 = destination_dlci
else:
dlci1 = destination_dlci
port = source_port
dlci2 = source_dlci
port_info += " incoming DLCI {dlci1} is switched to port {port} outgoing DLCI {dlci2}\n".format(dlci1=dlci1,
port=port,
dlci2=dlci2)
break
return info + port_info
def configPage(self):
"""
Returns the configuration page widget to be used by the node properties dialog.
:returns: QWidget object
"""
from .pages.frame_relay_switch_configuration_page import FrameRelaySwitchConfigurationPage
return FrameRelaySwitchConfigurationPage
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this node.
:returns: symbol path (or resource).
"""
return ":/symbols/frame_relay_switch.svg"
@staticmethod
def symbolName():
return "Frame Relay switch"
@staticmethod
def categories():
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node categories
"""
return [Node.switches]
def __str__(self):
return "Frame Relay switch"

View File

@@ -1,108 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3.node import Node
from .cloud import Cloud
import logging
log = logging.getLogger(__name__)
class Host(Cloud):
"""
Pseudo host based on a Dynamips Cloud.
:param module: parent module for this node
:param server: GNS3 server instance
:param project: Project instance
"""
_name_instance_count = 1
def __init__(self, module, server, project):
super().__init__(module, server, project)
log.info("host is being created")
# create an unique id and name
self._name_id = Host._name_instance_count
Host._name_instance_count += 1
name = "Host{}".format(self._name_id)
self._settings["name"] = name
def setup(self, name=None, additional_settings={}):
"""
Setups this host.
:param name: optional name for this host
"""
if name:
self._settings["name"] = name
if additional_settings and "nios" in additional_settings:
self._settings["nios"] = additional_settings["nios"]
else:
self.created_signal.connect(self._autoConfigure)
self._server.get("/interfaces", self._setupCallback)
def _autoConfigure(self, node_id):
"""
Auto adds all Ethernet and TAP interfaces.
:param node_id: ignored
"""
new_settings = {"nios": []}
for interface in self._settings["interfaces"]:
if interface["name"].startswith("tap"):
new_settings["nios"].append("nio_tap:{}".format(interface["name"]))
else:
new_settings["nios"].append("nio_gen_eth:{}".format(interface["name"]))
self.update(new_settings)
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this host.
:returns: symbol path (or resource).
"""
return ":/symbols/computer.svg"
@staticmethod
def symbolName():
return "Host"
@staticmethod
def categories():
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node category (integer)
"""
return [Node.end_devices]
def __str__(self):
return "Host"

143
gns3/modules/builtin/nat.py Normal file
View File

@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3.node import Node
import logging
log = logging.getLogger(__name__)
class Nat(Node):
"""
Nat node
:param module: parent module for this node
:param server: GNS3 server instance
:param project: Project instance
"""
URL_PREFIX = "nat"
def __init__(self, module, server, project):
super().__init__(module, server, project)
self.setStatus(Node.started)
self._always_on = True
self._nat_settings = {}
self.settings().update(self._nat_settings)
def interfaces(self):
return self._interfaces
def create(self, name=None, node_id=None, default_name_format="Nat{0}"):
"""
Creates this nat.
:param name: optional name for this nat
:param node_id: Node identifier on the server
"""
params = {}
self._create(name, node_id, params, default_name_format)
def _createCallback(self, result, error=False, **kwargs):
"""
Callback for create.
:param result: server response
"""
if error:
log.error("Error while creating nat: {}".format(result["message"]))
return
def update(self, new_settings):
"""
Updates the settings for this nat.
:param new_settings: settings dictionary
"""
params = {}
for name, value in new_settings.items():
if name in self._settings and self._settings[name] != value:
params[name] = value
if params:
self._update(params)
def _updateCallback(self, result, error=False, **kwargs):
"""
Callback for update.
:param result: server response
"""
if error:
log.error("Error while creating nat: {}".format(result["message"]))
return
def info(self):
"""
Returns information about this nat.
:returns: formatted string
"""
info = """Nat device {name} is always-on
This is a node for external connections
""".format(name=self.name())
port_info = ""
for port in self._ports:
if port.isFree():
port_info += " Port {} is empty\n".format(port.name())
else:
port_info += " Port {name} {description}\n".format(name=port.name(),
description=port.description())
return info + port_info
@staticmethod
def defaultSymbol():
"""
Returns the default symbol path for this nat.
:returns: symbol path (or resource).
"""
return ":/symbols/cloud.svg"
@staticmethod
def symbolName():
return "Nat"
@staticmethod
def categories():
"""
Returns the node categories the node is part of (used by the device panel).
:returns: list of node categories
"""
return [Node.end_devices]
def __str__(self):
return "Nat"

View File

@@ -195,7 +195,5 @@ class ATMSwitchConfigurationPage(QtWidgets.QWidget, Ui_atmSwitchConfigPageWidget
QtWidgets.QMessageBox.critical(self, "Name", "ATM switch name cannot be empty!")
else:
settings["name"] = name
else:
del settings["name"]
settings["mappings"] = self._mapping.copy()
return settings

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Configuration page for Built-in preferences.
"""
from gns3.qt import QtWidgets
from .. import Builtin
from ..ui.builtin_preferences_page_ui import Ui_BuiltinPreferencesPageWidget
from ..settings import BUILTIN_SETTINGS
class BuiltinPreferencesPage(QtWidgets.QWidget, Ui_BuiltinPreferencesPageWidget):
"""QWidget preference page for Built-in."""
def __init__(self):
super().__init__()
self.setupUi(self)
# connect signals
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
def _restoreDefaultsSlot(self):
"""Slot to populate the page widgets with the default settings."""
self._populateWidgets(BUILTIN_SETTINGS)
def _populateWidgets(self, settings):
"""Populates the widgets with the settings.
:param settings: Built-in settings
"""
def loadPreferences(self):
"""Loads Built-in preferences."""
builtin_settings = Builtin.instance().settings()
self._populateWidgets(builtin_settings)
def savePreferences(self):
"""Saves Built-in preferences."""
new_settings = {}
Builtin.instance().setSettings(new_settings)

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,8 +19,11 @@
Configuration page for clouds.
"""
import re
from gns3.qt import QtCore, QtWidgets
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.controller import Controller
from gns3.node import Node
from ..ui.cloud_configuration_page_ui import Ui_cloudConfigPageWidget
@@ -34,499 +37,339 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
super().__init__()
self.setupUi(self)
self._nios = set()
self._node = None
self._ports = []
self._interfaces = []
# connect NIO generic Ethernet slots
self.uiGenericEthernetComboBox.currentIndexChanged.connect(self._genericEthernetSelectedSlot)
self.uiGenericEthernetListWidget.itemSelectionChanged.connect(self._genericEthernetChangedSlot)
self.uiAddGenericEthernetPushButton.clicked.connect(self._genericEthernetAddSlot)
self.uiDeleteGenericEthernetPushButton.clicked.connect(self._genericEthernetDeleteSlot)
# add the categories
for name, category in Node.defaultCategories().items():
self.uiCategoryComboBox.addItem(name, category)
# connect NIO Linux Ethernet slots
self.uiLinuxEthernetComboBox.currentIndexChanged.connect(self._linuxEthernetSelectedSlot)
self.uiLinuxEthernetListWidget.itemSelectionChanged.connect(self._linuxEthernetChangedSlot)
self.uiAddLinuxEthernetPushButton.clicked.connect(self._linuxEthernetAddSlot)
self.uiDeleteLinuxEthernetPushButton.clicked.connect(self._linuxEthernetDeleteSlot)
# connect Ethernet slots
self.uiEthernetListWidget.itemSelectionChanged.connect(self._EthernetChangedSlot)
self.uiEthernetWarningPushButton.clicked.connect(self._EthernetWarningSlot)
self.uiAddEthernetPushButton.clicked.connect(self._EthernetAddSlot)
self.uiAddAllEthernetPushButton.clicked.connect(self._EthernetAddAllSlot)
self.uiDeleteEthernetPushButton.clicked.connect(self._EthernetDeleteSlot)
# connect NIO NAT slots
self.uiNIONATListWidget.currentRowChanged.connect(self._NIONATSelectedSlot)
self.uiNIONATListWidget.itemSelectionChanged.connect(self._NIONATChangedSlot)
self.uiAddNIONATPushButton.clicked.connect(self._NIONATAddSlot)
self.uiDeleteNIONATPushButton.clicked.connect(self._NIONATDeleteSlot)
# connect TAP slots
self.uiTAPComboBox.currentIndexChanged.connect(self._TAPSelectedSlot)
self.uiTAPListWidget.itemSelectionChanged.connect(self._TAPChangedSlot)
self.uiAddTAPPushButton.clicked.connect(self._TAPAddSlot)
self.uiAddAllTAPPushButton.clicked.connect(self._TAPAddAllSlot)
self.uiDeleteTAPPushButton.clicked.connect(self._TAPDeleteSlot)
# connect NIO UDP slots
self.uiNIOUDPListWidget.currentRowChanged.connect(self._NIOUDPSelectedSlot)
self.uiNIOUDPListWidget.itemSelectionChanged.connect(self._NIOUDPChangedSlot)
self.uiAddNIOUDPPushButton.clicked.connect(self._NIOUDPAddSlot)
self.uiDeleteNIOUDPPushButton.clicked.connect(self._NIOUDPDeleteSlot)
# connect UDP slots
self.uiUDPTreeWidget.itemActivated.connect(self._UDPSelectedSlot)
self.uiUDPTreeWidget.itemSelectionChanged.connect(self._UDPChangedSlot)
self.uiAddUDPPushButton.clicked.connect(self._UDPAddSlot)
self.uiDeleteUDPPushButton.clicked.connect(self._UDPDeleteSlot)
# connect NIO TAP slots
self.uiNIOTAPListWidget.currentRowChanged.connect(self._NIOTAPSelectedSlot)
self.uiNIOTAPListWidget.itemSelectionChanged.connect(self._NIOTAPChangedSlot)
self.uiAddNIOTAPPushButton.clicked.connect(self._NIOTAPAddSlot)
self.uiDeleteNIOTAPPushButton.clicked.connect(self._NIOTAPDeleteSlot)
self.uiShowSpecialInterfacesCheckBox.stateChanged.connect(self._showSpecialInterfacesSlot)
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
# connect NIO UNIX slots
self.uiNIOUNIXListWidget.currentRowChanged.connect(self._NIOUNIXSelectedSlot)
self.uiNIOUNIXListWidget.itemSelectionChanged.connect(self._NIOUNIXChangedSlot)
self.uiAddNIOUNIXPushButton.clicked.connect(self._NIOUNIXAddSlot)
self.uiDeleteNIOUNIXPushButton.clicked.connect(self._NIOUNIXDeleteSlot)
# connect NIO VDE slots
self.uiNIOVDEListWidget.currentRowChanged.connect(self._NIOVDESelectedSlot)
self.uiNIOVDEListWidget.itemSelectionChanged.connect(self._NIOVDEChangedSlot)
self.uiAddNIOVDEPushButton.clicked.connect(self._NIOVDEAddSlot)
self.uiDeleteNIOVDEPushButton.clicked.connect(self._NIOVDEDeleteSlot)
# connect NIO NULL slots
self.uiNIONullListWidget.currentRowChanged.connect(self._NIONullSelectedSlot)
self.uiNIONullListWidget.itemSelectionChanged.connect(self._NIONullChangedSlot)
self.uiAddNIONullPushButton.clicked.connect(self._NIONullAddSlot)
self.uiDeleteNIONullPushButton.clicked.connect(self._NIONullDeleteSlot)
def _genericEthernetSelectedSlot(self, index):
"""
Loads the selected generic Ethernet interface in lineEdit.
:param index: ignored
"""
self.uiGenericEthernetLineEdit.setText(self.uiGenericEthernetComboBox.currentText())
def _genericEthernetChangedSlot(self):
def _EthernetChangedSlot(self):
"""
Enables the use of the delete button.
"""
item = self.uiGenericEthernetListWidget.currentItem()
item = self.uiEthernetListWidget.currentItem()
if item:
self.uiDeleteGenericEthernetPushButton.setEnabled(True)
self.uiDeleteEthernetPushButton.setEnabled(True)
else:
self.uiDeleteGenericEthernetPushButton.setEnabled(False)
self.uiDeleteEthernetPushButton.setEnabled(False)
def _genericEthernetAddSlot(self):
def _EthernetWarningSlot(self):
"""
Adds a new generic Ethernet NIO.
Shows a warning about Wifi Ethernet interfaces.
"""
interface = self.uiGenericEthernetLineEdit.text()
QtWidgets.QMessageBox.warning(self, "Ethernet interfaces", "Wifi interfaces may not work properly. It is recommended to use wired Ethernet or Loopback interfaces only.")
def _EthernetAddSlot(self, interface=None):
"""
Adds a new Ethernet interface.
"""
if not interface:
interface = self.uiEthernetComboBox.currentText()
if interface:
nio = "nio_gen_eth:{interface}".format(interface=interface)
if nio not in self._nios:
self.uiGenericEthernetListWidget.addItem(nio)
self._nios.add(nio)
def _genericEthernetDeleteSlot(self):
"""
Deletes the selected generic Ethernet NIO.
"""
item = self.uiGenericEthernetListWidget.currentItem()
if item:
nio = item.text()
# check we can delete that NIO
node_ports = self._node.ports()
for node_port in node_ports:
if node_port.name() == nio and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
for port in self._ports:
if port["name"] == interface and port["type"] == "ethernet":
return
try:
self._nios.remove(nio)
except KeyError: # If you ask for delete multiple time with race condition you could have a missing key https://github.com/GNS3/gns3-gui/issues/1352
return
self.uiGenericEthernetListWidget.takeItem(self.uiGenericEthernetListWidget.currentRow())
self.uiEthernetListWidget.addItem(interface)
self._ports.append({"name": interface,
"port_number": len(self._ports),
"type": "ethernet",
"interface": interface})
index = self.uiEthernetComboBox.findText(interface)
if index != -1:
self.uiEthernetComboBox.removeItem(index)
def _linuxEthernetSelectedSlot(self, index):
def _EthernetAddAllSlot(self):
"""
Loads the selected Linux interface in lineEdit.
Adds all Ethernet interfaces.
"""
for index in range(0, self.uiEthernetComboBox.count()):
interface = self.uiEthernetComboBox.itemText(index)
self._EthernetAddSlot(interface)
def _EthernetDeleteSlot(self):
"""
Deletes the selected Ethernet interface.
"""
if self._node:
for item in self.uiEthernetListWidget.selectedItems():
interface = item.text()
# check we can delete that interface
for node_port in self._node.ports():
if node_port.name() == interface and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to {}, please remove it first".format(interface))
return
for item in self.uiEthernetListWidget.selectedItems():
interface = item.text()
for port in self._ports.copy():
if port["name"] == interface:
self._ports.remove(port)
self.uiEthernetListWidget.takeItem(self.uiEthernetListWidget.row(item))
for interface in self._interfaces:
if not self.uiShowSpecialInterfacesCheckBox.isChecked() and interface["special"]:
continue
if interface["name"] == port["name"] and interface["type"] == "ethernet":
self.uiEthernetComboBox.addItem(interface["name"])
break
break
def _TAPSelectedSlot(self, index):
"""
Loads the selected TAP interface.
:param index: ignored
"""
self.uiLinuxEthernetLineEdit.setText(self.uiLinuxEthernetComboBox.currentText())
self.uiTAPLineEdit.setText(self.uiTAPComboBox.currentText())
def _linuxEthernetChangedSlot(self):
def _TAPChangedSlot(self):
"""
Enables the use of the delete button.
"""
item = self.uiLinuxEthernetListWidget.currentItem()
item = self.uiTAPListWidget.currentItem()
if item:
self.uiDeleteLinuxEthernetPushButton.setEnabled(True)
self.uiDeleteTAPPushButton.setEnabled(True)
self.uiTAPLineEdit.setText(item.text())
else:
self.uiDeleteLinuxEthernetPushButton.setEnabled(False)
self.uiDeleteTAPPushButton.setEnabled(False)
def _linuxEthernetAddSlot(self):
def _TAPAddSlot(self, interface=None):
"""
Adds a new Linux Ethernet NIO.
Adds a new TAP interface.
"""
interface = self.uiLinuxEthernetLineEdit.text()
if not interface:
interface = self.uiTAPLineEdit.text()
if interface:
nio = "nio_gen_linux:{interface}".format(interface=interface)
if nio not in self._nios:
self.uiLinuxEthernetListWidget.addItem(nio)
self._nios.add(nio)
def _linuxEthernetDeleteSlot(self):
"""
Deletes the selected Linux Ethernet NIO.
"""
item = self.uiLinuxEthernetListWidget.currentItem()
if item:
nio = item.text()
# check we can delete that NIO
node_ports = self._node.ports()
for node_port in node_ports:
if node_port.name() == nio and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
for port in self._ports:
if port["name"] == interface and port["type"] == "tap":
return
try:
self._nios.remove(nio)
# An interface could have been removed and no longer exists in nios
except KeyError:
pass
self.uiLinuxEthernetListWidget.takeItem(self.uiLinuxEthernetListWidget.currentRow())
self.uiTAPListWidget.addItem(interface)
self._ports.append({"name": interface,
"port_number": len(self._ports),
"type": "tap",
"interface": interface})
index = self.uiTAPComboBox.findText(interface)
if index != -1:
self.uiTAPComboBox.removeItem(index)
def _NIONATSelectedSlot(self, index):
def _TAPAddAllSlot(self):
"""
Loads a selected NAT NIO.
:param index: ignored
Adds all TAP interfaces
"""
item = self.uiNIONATListWidget.currentItem()
if item:
nio = item.text()
match = re.search(r"""^nio_nat:(.+)$""", nio)
if match:
self.uiNIONATIdentiferLineEdit.setText(match.group(1))
for index in range(0, self.uiTAPComboBox.count()):
interface = self.uiTAPComboBox.itemText(index)
self._TAPAddSlot(interface)
def _NIONATChangedSlot(self):
def _TAPDeleteSlot(self):
"""
Deletes a TAP interface.
"""
if self._node:
for item in self.uiTAPListWidget.selectedItems():
interface = item.text()
# check we can delete that interface
for node_port in self._node.ports():
if node_port.name() == interface and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to {}, please remove it first".format(interface))
return
for item in self.uiTAPListWidget.selectedItems():
interface = item.text()
for port in self._ports.copy():
if port["name"] == interface:
self._ports.remove(port)
self.uiTAPListWidget.takeItem(self.uiTAPListWidget.row(item))
for interface in self._interfaces:
if interface["name"] == port["name"] and interface["type"] == "tap":
self.uiTAPComboBox.addItem(interface["name"])
break
def _UDPSelectedSlot(self, item, column):
"""
Loads a selected UDP tunnel.
:param item: selected TreeWidgetItem instance
:param column: ignored
"""
name = item.text(0)
local_port = int(item.text(1))
remote_host = item.text(2)
remote_port = int(item.text(3))
self.uiUDPNameLineEdit.setText(name)
self.uiLocalPortSpinBox.setValue(local_port)
self.uiRemoteHostLineEdit.setText(remote_host)
self.uiRemotePortSpinBox.setValue(remote_port)
def _UDPChangedSlot(self):
"""
Enables the use of the delete button.
"""
item = self.uiNIONATListWidget.currentItem()
item = self.uiUDPTreeWidget.currentItem()
if item:
self.uiDeleteNIONATPushButton.setEnabled(True)
self.uiDeleteUDPPushButton.setEnabled(True)
else:
self.uiDeleteNIONATPushButton.setEnabled(False)
self.uiDeleteUDPPushButton.setEnabled(False)
def _NIONATAddSlot(self):
def _UDPAddSlot(self):
"""
Adds a new NAT NIO.
"""
identifier = self.uiNIONATIdentiferLineEdit.text()
if identifier:
nio = "nio_nat:{}".format(identifier)
if nio not in self._nios:
self.uiNIONATListWidget.addItem(nio)
self._nios.add(nio)
def _NIONATDeleteSlot(self):
"""
Deletes a NAT NIO.
"""
item = self.uiNIONATListWidget.currentItem()
if item:
nio = item.text()
# check we can delete that NIO
node_ports = self._node.ports()
for node_port in node_ports:
if node_port.name() == nio and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
return
self._nios.remove(nio)
self.uiNIONATListWidget.takeItem(self.uiNIONATListWidget.currentRow())
def _NIOUDPSelectedSlot(self, index):
"""
Loads a selected UDP.
:param index: ignored
"""
item = self.uiNIOUDPListWidget.currentItem()
if item:
nio = item.text()
match = re.search(r"""^nio_udp:(\d+):(.+):(\d+)$""", nio)
if match:
self.uiLocalPortSpinBox.setValue(int(match.group(1)))
self.uiRemoteHostLineEdit.setText(match.group(2))
self.uiRemotePortSpinBox.setValue(int(match.group(3)))
def _NIOUDPChangedSlot(self):
"""
Enables the use of the delete button.
"""
item = self.uiNIOUDPListWidget.currentItem()
if item:
self.uiDeleteNIOUDPPushButton.setEnabled(True)
else:
self.uiDeleteNIOUDPPushButton.setEnabled(False)
def _NIOUDPAddSlot(self):
"""
Adds a new UDP NIO.
Adds a new UDP tunnel
"""
name = self.uiUDPNameLineEdit.text()
local_port = self.uiLocalPortSpinBox.value()
remote_host = self.uiRemoteHostLineEdit.text()
remote_port = self.uiRemotePortSpinBox.value()
if remote_host:
nio = "nio_udp:{lport}:{rhost}:{rport}".format(lport=local_port,
rhost=remote_host,
rport=remote_port)
if nio not in self._nios:
self.uiNIOUDPListWidget.addItem(nio)
self._nios.add(nio)
self.uiLocalPortSpinBox.setValue(local_port + 1)
self.uiRemotePortSpinBox.setValue(remote_port + 1)
def _NIOUDPDeleteSlot(self):
"""
Deletes an UDP NIO.
"""
item = self.uiNIOUDPListWidget.currentItem()
if item:
nio = item.text()
# check we can delete that NIO
node_ports = self._node.ports()
for node_port in node_ports:
if node_port.name() == nio and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
if name and remote_host:
for port in self._ports:
if port["name"] == name:
return
self._nios.remove(nio)
self.uiNIOUDPListWidget.takeItem(self.uiNIOUDPListWidget.currentRow())
def _NIOTAPSelectedSlot(self, index):
# add a new entry in the tree widget
item = QtWidgets.QTreeWidgetItem(self.uiUDPTreeWidget)
item.setText(0, name)
item.setText(1, str(local_port))
item.setText(2, remote_host)
item.setText(3, str(remote_port))
self.uiUDPTreeWidget.addTopLevelItem(item)
self._ports.append({"name": name,
"port_number": len(self._ports),
"type": "udp",
"lport": local_port,
"rhost": remote_host,
"rport": remote_port})
self.uiLocalPortSpinBox.setValue(local_port + 1)
self.uiRemotePortSpinBox.setValue(remote_port + 1)
self.uiUDPTreeWidget.resizeColumnToContents(0)
self.uiUDPTreeWidget.resizeColumnToContents(1)
self.uiUDPTreeWidget.resizeColumnToContents(2)
self.uiUDPTreeWidget.resizeColumnToContents(3)
nb_tunnels = 0
for port in self._ports:
if port["type"] == "udp":
nb_tunnels += 1
self.uiUDPNameLineEdit.setText("UDP tunnel {}".format(nb_tunnels + 1))
def _UDPDeleteSlot(self):
"""
Loads the selected NIO TAP in lineEdit.
:param index: ignored
Deletes an UDP tunnel.
"""
item = self.uiNIOTAPListWidget.currentItem()
if item:
nio = item.text()
match = re.search(r"""^nio_tap:(.+)$""", nio)
if match:
self.uiNIOTAPLineEdit.setText(match.group(1))
if self._node:
for item in self.uiUDPTreeWidget.selectedItems():
name = item.text(0)
# check we can delete that UDP tunnel
for node_port in self._node.ports():
if node_port.name() == name and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to {}, please remove it first".format(name))
return
def _NIOTAPChangedSlot(self):
for item in self.uiUDPTreeWidget.selectedItems():
name = item.text(0)
for port in self._ports.copy():
if port["name"] == name:
self._ports.remove(port)
self.uiUDPTreeWidget.takeTopLevelItem(self.uiUDPTreeWidget.indexOfTopLevelItem(item))
nb_tunnels = 0
for port in self._ports:
if port["type"] == "udp":
nb_tunnels += 1
self.uiUDPNameLineEdit.setText("UDP tunnel {}".format(nb_tunnels + 1))
def _showSpecialInterfacesSlot(self, state):
self.uiEthernetComboBox.clear()
index = 0
for interface in self._interfaces:
if interface["type"] == "ethernet":
if not state and interface["special"]:
continue
if self.uiEthernetListWidget.findItems(interface["name"], QtCore.Qt.MatchFixedString):
continue
self.uiEthernetComboBox.addItem(interface["name"])
index += 1
def _symbolBrowserSlot(self):
"""
Enables the use of the delete button.
Slot to open the symbol browser and select a new symbol.
"""
item = self.uiNIOTAPListWidget.currentItem()
if item:
self.uiDeleteNIOTAPPushButton.setEnabled(True)
symbol_path = self.uiSymbolLineEdit.text()
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
dialog.show()
if dialog.exec_():
new_symbol_path = dialog.getSymbol()
self.uiSymbolLineEdit.setText(new_symbol_path)
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
def _loadNetworkInterfaces(self, interfaces):
self.uiEthernetComboBox.clear()
index = 0
for interface in interfaces:
if interface["type"] == "ethernet" and not interface["special"]:
self.uiEthernetComboBox.addItem(interface["name"])
index += 1
# load all TAP interfaces
self.uiTAPComboBox.clear()
index = 0
for interface in interfaces:
if interface["type"] == "tap":
self.uiTAPComboBox.addItem(interface["name"])
index += 1
def _getInterfacesFromServerCallback(self, result, error=False, **kwargs):
"""
Callback for retrieving the network interfaces
:param progress_dialog: QProgressDialog instance
:param result: server response
:param error: indicates an error (boolean)
"""
if error:
QtWidgets.QMessageBox.critical(self, "Network interfaces", "{}".format(result["message"]))
else:
self.uiDeleteNIOTAPPushButton.setEnabled(False)
self._interfaces = result
self._loadNetworkInterfaces(result)
def _NIOTAPAddSlot(self):
"""
Adds a new UDP NIO.
"""
tap_interface = self.uiNIOTAPLineEdit.text()
if tap_interface:
nio = "nio_tap:{}".format(tap_interface.lower())
if nio not in self._nios:
self.uiNIOTAPListWidget.addItem(nio)
self._nios.add(nio)
def _NIOTAPDeleteSlot(self):
"""
Deletes a TAP NIO.
"""
item = self.uiNIOTAPListWidget.currentItem()
if item:
nio = item.text()
# check we can delete that NIO
node_ports = self._node.ports()
for node_port in node_ports:
if node_port.name() == nio and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
return
self._nios.remove(nio)
self.uiNIOTAPListWidget.takeItem(self.uiNIOTAPListWidget.currentRow())
def _NIOUNIXSelectedSlot(self, index):
"""
Loads a selected UNIX NIO.
:param index: ignored
"""
item = self.uiNIOUNIXListWidget.currentItem()
if item:
nio = item.text()
match = re.search(r"""^nio_unix:(.+):(.+)$""", nio)
if match:
self.uiLocalFileLineEdit.setText(match.group(1))
self.uiRemoteFileLineEdit.setText(match.group(2))
def _NIOUNIXChangedSlot(self):
"""
Enables the use of the delete button.
"""
item = self.uiNIOUNIXListWidget.currentItem()
if item:
self.uiDeleteNIOUNIXPushButton.setEnabled(True)
else:
self.uiDeleteNIOUNIXPushButton.setEnabled(False)
def _NIOUNIXAddSlot(self):
"""
Adds a new UNIX NIO.
"""
local_file = self.uiLocalFileLineEdit.text()
remote_file = self.uiRemoteFileLineEdit.text()
if local_file and remote_file:
nio = "nio_unix:{local}:{remote}".format(local=local_file,
remote=remote_file)
if nio not in self._nios:
self.uiNIOUNIXListWidget.addItem(nio)
self._nios.add(nio)
def _NIOUNIXDeleteSlot(self):
"""
Deletes an UNIX NIO.
"""
item = self.uiNIOUNIXListWidget.currentItem()
if item:
nio = item.text()
# check we can delete that NIO
node_ports = self._node.ports()
for node_port in node_ports:
if node_port.name() == nio and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
return
self._nios.remove(nio)
self.uiNIOUNIXListWidget.takeItem(self.uiNIOUNIXListWidget.currentRow())
def _NIOVDESelectedSlot(self, index):
"""
Loads a selected VDE NIO.
:param index: ignored
"""
item = self.uiNIOVDEListWidget.currentItem()
if item:
nio = item.text()
match = re.search(r"""^nio_vde:(.+):(.+)$""", nio)
if match:
self.uiVDEControlFileLineEdit.setText(match.group(1))
self.uiVDELocalFileLineEdit.setText(match.group(2))
def _NIOVDEChangedSlot(self):
"""
Enables the use of the delete button.
"""
item = self.uiNIOVDEListWidget.currentItem()
if item:
self.uiDeleteNIOVDEPushButton.setEnabled(True)
else:
self.uiDeleteNIOVDEPushButton.setEnabled(False)
def _NIOVDEAddSlot(self):
"""
Adds a new VDE NIO.
"""
control_file = self.uiVDEControlFileLineEdit.text()
local_file = self.uiVDELocalFileLineEdit.text()
if local_file and control_file:
nio = "nio_vde:{control}:{local}".format(control=control_file, local=local_file)
if nio not in self._nios:
self.uiNIOVDEListWidget.addItem(nio)
self._nios.add(nio)
def _NIOVDEDeleteSlot(self):
"""
Deletes a VDE NIO.
"""
item = self.uiNIOVDEListWidget.currentItem()
if item:
nio = item.text()
# check we can delete that NIO
node_ports = self._node.ports()
for node_port in node_ports:
if node_port.name() == nio and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
return
self._nios.remove(nio)
self.uiNIOVDEListWidget.takeItem(self.uiNIOVDEListWidget.currentRow())
def _NIONullSelectedSlot(self, index):
"""
Loads a selected NULL NIO.
:param index: ignored
"""
item = self.uiNIONullListWidget.currentItem()
if item:
nio = item.text()
match = re.search(r"""^nio_null:(.+)$""", nio)
if match:
self.uiNIONullIdentiferLineEdit.setText(match.group(1))
def _NIONullChangedSlot(self):
"""
Enables the use of the delete button.
"""
item = self.uiNIONullListWidget.currentItem()
if item:
self.uiDeleteNIONullPushButton.setEnabled(True)
else:
self.uiDeleteNIONullPushButton.setEnabled(False)
def _NIONullAddSlot(self):
"""
Adds a new NULL NIO.
"""
identifier = self.uiNIONullIdentiferLineEdit.text()
if identifier:
nio = "nio_null:{}".format(identifier)
if nio not in self._nios:
self.uiNIONullListWidget.addItem(nio)
self._nios.add(nio)
def _NIONullDeleteSlot(self):
"""
Deletes a NULL NIO.
"""
item = self.uiNIONullListWidget.currentItem()
if item:
nio = item.text()
# check we can delete that NIO
node_ports = self._node.ports()
for node_port in node_ports:
if node_port.name() == nio and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
return
self._nios.remove(nio)
self.uiNIONullListWidget.takeItem(self.uiNIONullListWidget.currentRow())
def loadSettings(self, settings, node, group=False):
def loadSettings(self, settings, node=None, group=False):
"""
Loads the cloud settings.
@@ -540,58 +383,72 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
else:
self.uiNameLineEdit.setEnabled(False)
self._node = node
if not node:
# these are template settings
# load all network interfaces
self.uiGenericEthernetComboBox.clear()
index = 0
for interface in settings["interfaces"]:
if interface["name"].startswith("tap"):
# do not add TAP interfaces
continue
self.uiGenericEthernetComboBox.addItem(interface["name"])
self.uiGenericEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
index += 1
self.uiGenericEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
# rename the label from "Name" to "Template name"
self.uiNameLabel.setText("Template name:")
# load all network interfaces
self.uiLinuxEthernetComboBox.clear()
index = 0
for interface in settings["interfaces"]:
if not interface["name"].startswith(r"\Device\NPF_") and not interface["name"].startswith("tap"):
self.uiLinuxEthernetComboBox.addItem(interface["name"])
self.uiLinuxEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
index += 1
self.uiLinuxEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
# load the default name format
self.uiDefaultNameFormatLineEdit.setText(settings["default_name_format"])
# populate the NIO lists
self.nios = set()
self.uiGenericEthernetListWidget.clear()
self.uiLinuxEthernetListWidget.clear()
self.uiNIOUDPListWidget.clear()
self.uiNIOTAPListWidget.clear()
self.uiNIOUNIXListWidget.clear()
self.uiNIOVDEListWidget.clear()
self.uiNIONullListWidget.clear()
# load the symbol
self.uiSymbolLineEdit.setText(settings["symbol"])
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(settings["symbol"]))
for nio in settings["nios"]:
self._nios.add(nio)
if nio.lower().startswith("nio_gen_eth"):
self.uiGenericEthernetListWidget.addItem(nio)
elif nio.lower().startswith("nio_gen_linux"):
self.uiLinuxEthernetListWidget.addItem(nio)
elif nio.lower().startswith("nio_udp"):
self.uiNIOUDPListWidget.addItem(nio)
elif nio.lower().startswith("nio_tap"):
self.uiNIOTAPListWidget.addItem(nio)
elif nio.lower().startswith("nio_unix"):
self.uiNIOUNIXListWidget.addItem(nio)
elif nio.lower().startswith("nio_vde"):
self.uiNIOVDEListWidget.addItem(nio)
elif nio.lower().startswith("nio_null"):
self.uiNIONullListWidget.addItem(nio)
# load the category
index = self.uiCategoryComboBox.findData(settings["category"])
if index != -1:
self.uiCategoryComboBox.setCurrentIndex(index)
def saveSettings(self, settings, node, group=False):
Controller.instance().getCompute("/network/interfaces", settings["server"],
self._getInterfacesFromServerCallback,
progressText="Retrieving network interfaces...")
else:
self.uiDefaultNameFormatLabel.hide()
self.uiDefaultNameFormatLineEdit.hide()
self.uiSymbolLabel.hide()
self.uiSymbolLineEdit.hide()
self.uiSymbolToolButton.hide()
self.uiCategoryComboBox.hide()
self.uiCategoryLabel.hide()
self.uiCategoryComboBox.hide()
self._node = node
self._interfaces = self._node.interfaces()
self._loadNetworkInterfaces(self._interfaces)
# load the current ports
self._ports = []
self.uiEthernetListWidget.clear()
self.uiTAPListWidget.clear()
self.uiUDPTreeWidget.clear()
for port in settings["ports_mapping"]:
self._ports.append(port)
if port["type"] == "ethernet":
self.uiEthernetListWidget.addItem(port["name"])
index = self.uiEthernetComboBox.findText(port["name"])
if index != -1:
self.uiEthernetComboBox.removeItem(index)
elif port["type"] == "tap":
self.uiTAPListWidget.addItem(port["name"])
index = self.uiTAPComboBox.findText(port["name"])
if index != -1:
self.uiTAPComboBox.removeItem(index)
elif port["type"] == "udp":
item = QtWidgets.QTreeWidgetItem(self.uiUDPTreeWidget)
item.setText(0, port["name"])
item.setText(1, str(port["lport"]))
item.setText(2, port["rhost"])
item.setText(3, str(port["rport"]))
self.uiUDPTreeWidget.addTopLevelItem(item)
self.uiUDPTreeWidget.resizeColumnToContents(0)
self.uiUDPTreeWidget.resizeColumnToContents(1)
self.uiUDPTreeWidget.resizeColumnToContents(2)
self.uiUDPTreeWidget.resizeColumnToContents(3)
def saveSettings(self, settings, node=None, group=False):
"""
Saves the cloud settings.
@@ -602,7 +459,22 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
if not group:
settings["name"] = self.uiNameLineEdit.text()
else:
del settings["name"]
settings["nios"] = list(self._nios)
if not node:
# these are template settings
# save the default name format
default_name_format = self.uiDefaultNameFormatLineEdit.text().strip()
if '{0}' not in default_name_format and '{id}' not in default_name_format:
QtWidgets.QMessageBox.critical(self, "Default name format", "The default name format must contain at least {0} or {id}")
else:
settings["default_name_format"] = default_name_format
symbol_path = self.uiSymbolLineEdit.text()
settings["symbol"] = symbol_path
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
settings["ports_mapping"] = self._ports
else:
settings["ports_mapping"] = self._ports
return settings

View File

@@ -0,0 +1,188 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Configuration page for cloud node preferences.
"""
import copy
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
from gns3.main_window import MainWindow
from gns3.dialogs.configuration_dialog import ConfigurationDialog
from gns3.compute_manager import ComputeManager
from gns3.controller import Controller
from .. import Builtin
from ..settings import CLOUD_SETTINGS
from ..ui.cloud_preferences_page_ui import Ui_CloudPreferencesPageWidget
from ..pages.cloud_configuration_page import CloudConfigurationPage
from ..dialogs.cloud_wizard import CloudWizard
class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
"""
QWidget preference page for cloud node preferences.
"""
def __init__(self):
super().__init__()
self.setupUi(self)
self._main_window = MainWindow.instance()
self._cloud_nodes = {}
self._items = []
self.uiNewCloudNodePushButton.clicked.connect(self._newCloudNodeSlot)
self.uiEditCloudNodePushButton.clicked.connect(self._editCloudNodeSlot)
self.uiDeleteCloudNodePushButton.clicked.connect(self._deleteCloudNodeSlot)
self.uiCloudNodesTreeWidget.itemSelectionChanged.connect(self._cloudNodeChangedSlot)
def _createSectionItem(self, name):
section_item = QtWidgets.QTreeWidgetItem(self.uiCloudNodeInfoTreeWidget)
section_item.setText(0, name)
font = section_item.font(0)
font.setBold(True)
section_item.setFont(0, font)
return section_item
def _refreshInfo(self, cloud_node):
self.uiCloudNodeInfoTreeWidget.clear()
# fill out the General section
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", cloud_node["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", cloud_node["default_name_format"]])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["server"]).name()])
except KeyError:
pass
self.uiCloudNodeInfoTreeWidget.expandAll()
self.uiCloudNodeInfoTreeWidget.resizeColumnToContents(0)
self.uiCloudNodeInfoTreeWidget.resizeColumnToContents(1)
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
def _cloudNodeChangedSlot(self):
"""
Loads a selected cloud node template from the tree widget.
"""
selection = self.uiCloudNodesTreeWidget.selectedItems()
self.uiDeleteCloudNodePushButton.setEnabled(len(selection) != 0)
single_selected = len(selection) == 1
self.uiEditCloudNodePushButton.setEnabled(single_selected)
if single_selected:
key = selection[0].data(0, QtCore.Qt.UserRole)
cloud_node = self._cloud_nodes[key]
self._refreshInfo(cloud_node)
else:
self.uiCloudNodeInfoTreeWidget.clear()
def _newCloudNodeSlot(self):
"""
Creates a new cloud node template.
"""
wizard = CloudWizard(self._cloud_nodes, parent=self)
wizard.show()
if wizard.exec_():
new_cloud_settings = wizard.getSettings()
key = "{server}:{name}".format(server=new_cloud_settings["server"], name=new_cloud_settings["name"])
self._cloud_nodes[key] = CLOUD_SETTINGS.copy()
self._cloud_nodes[key].update(new_cloud_settings)
item = QtWidgets.QTreeWidgetItem(self.uiCloudNodesTreeWidget)
item.setText(0, self._cloud_nodes[key]["name"])
Controller.instance().getSymbolIcon(self._cloud_nodes[key]["symbol"], qpartial(self._setItemIcon, item))
item.setData(0, QtCore.Qt.UserRole, key)
self._items.append(item)
self.uiCloudNodesTreeWidget.setCurrentItem(item)
def _editCloudNodeSlot(self):
"""
Edits a cloud node template.
"""
item = self.uiCloudNodesTreeWidget.currentItem()
if item:
key = item.data(0, QtCore.Qt.UserRole)
cloud_node = self._cloud_nodes[key]
dialog = ConfigurationDialog(cloud_node["name"], cloud_node, CloudConfigurationPage(), parent=self)
dialog.show()
if dialog.exec_():
# update the icon
Controller.instance().getSymbolIcon(cloud_node["symbol"], qpartial(self._setItemIcon, item))
if cloud_node["name"] != item.text(0):
new_key = "{server}:{name}".format(server=cloud_node["server"], name=cloud_node["name"])
if new_key in self._cloud_nodes:
QtWidgets.QMessageBox.critical(self, "Cloud node", "Cloud node name {} already exists for server {}".format(cloud_node["name"],
cloud_node["server"]))
cloud_node["name"] = item.text(0)
return
self._cloud_nodes[new_key] = self._cloud_nodes[key]
del self._cloud_nodes[key]
item.setText(0, cloud_node["name"])
item.setData(0, QtCore.Qt.UserRole, new_key)
self._refreshInfo(cloud_node)
def _deleteCloudNodeSlot(self):
"""
Deletes a cloud node template.
"""
for item in self.uiCloudNodesTreeWidget.selectedItems():
if item:
key = item.data(0, QtCore.Qt.UserRole)
del self._cloud_nodes[key]
self.uiCloudNodesTreeWidget.takeTopLevelItem(self.uiCloudNodesTreeWidget.indexOfTopLevelItem(item))
def loadPreferences(self):
"""
Loads the cloud node preferences.
"""
builtin_module = Builtin.instance()
self._cloud_nodes = copy.deepcopy(builtin_module.cloudNodes())
self._items.clear()
for key, cloud_node in self._cloud_nodes.items():
item = QtWidgets.QTreeWidgetItem(self.uiCloudNodesTreeWidget)
item.setText(0, cloud_node["name"])
Controller.instance().getSymbolIcon(cloud_node["symbol"], qpartial(self._setItemIcon, item))
item.setData(0, QtCore.Qt.UserRole, key)
self._items.append(item)
if self._items:
self.uiCloudNodesTreeWidget.setCurrentItem(self._items[0])
self.uiCloudNodesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
def _setItemIcon(self, item, icon):
item.setIcon(0, icon)
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
def savePreferences(self):
"""
Saves the cloud node preferences.
"""
Builtin.instance().setCloudNodes(self._cloud_nodes)

View File

@@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Configuration page for Ethernet hubs.
"""
from gns3.qt import QtWidgets
from gns3.dialogs.node_properties_dialog import ConfigurationError
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.node import Node
from ..ui.ethernet_hub_configuration_page_ui import Ui_ethernetHubConfigPageWidget
class EthernetHubConfigurationPage(QtWidgets.QWidget, Ui_ethernetHubConfigPageWidget):
"""
QWidget configuration page for Ethernet hubs.
"""
def __init__(self):
super().__init__()
self.setupUi(self)
# add the categories
for name, category in Node.defaultCategories().items():
self.uiCategoryComboBox.addItem(name, category)
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
def _symbolBrowserSlot(self):
"""
Slot to open the symbol browser and select a new symbol.
"""
symbol_path = self.uiSymbolLineEdit.text()
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
dialog.show()
if dialog.exec_():
new_symbol_path = dialog.getSymbol()
self.uiSymbolLineEdit.setText(new_symbol_path)
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
def loadSettings(self, settings, node=None, group=False):
"""
Loads the Ethernet hub settings.
:param settings: the settings (dictionary)
:param node: Node instance
:param group: indicates the settings apply to a group
"""
if not group:
self.uiNameLineEdit.setText(settings["name"])
else:
self.uiNameLineEdit.hide()
self.uiNameLabel.hide()
if not node:
# these are template settings
# rename the label from "Name" to "Template name"
self.uiNameLabel.setText("Template name:")
# load the default name format
self.uiDefaultNameFormatLineEdit.setText(settings["default_name_format"])
# load the symbol
self.uiSymbolLineEdit.setText(settings["symbol"])
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(settings["symbol"]))
# load the category
index = self.uiCategoryComboBox.findData(settings["category"])
if index != -1:
self.uiCategoryComboBox.setCurrentIndex(index)
else:
self.uiDefaultNameFormatLabel.hide()
self.uiDefaultNameFormatLineEdit.hide()
self.uiSymbolLabel.hide()
self.uiSymbolLineEdit.hide()
self.uiSymbolToolButton.hide()
self.uiCategoryComboBox.hide()
self.uiCategoryLabel.hide()
self.uiCategoryComboBox.hide()
nb_ports = len(settings["ports_mapping"])
self.uiPortsSpinBox.setValue(nb_ports)
def saveSettings(self, settings, node=None, group=False):
"""
Saves the Ethernet hub settings.
:param settings: the settings (dictionary)
:param node: Node instance
:param group: indicates the settings apply to a group
"""
if not group:
# set the device name
name = self.uiNameLineEdit.text()
if not name:
QtWidgets.QMessageBox.critical(self, "Name", "Ethernet hub name cannot be empty!")
else:
settings["name"] = name
nb_ports = self.uiPortsSpinBox.value()
if node:
# check that a link isn't connected to a port before we delete it
ports = node.ports()
for port in ports:
if not port.isFree() and port.portNumber() > nb_ports:
self.loadSettings(settings, node)
QtWidgets.QMessageBox.critical(self, node.name(), "A link is connected to port {}, please remove it first".format(port.name()))
raise ConfigurationError()
else:
# these are template settings
# save the default name format
default_name_format = self.uiDefaultNameFormatLineEdit.text().strip()
if '{0}' not in default_name_format and '{id}' not in default_name_format:
QtWidgets.QMessageBox.critical(self, "Default name format", "The default name format must contain at least {0} or {id}")
else:
settings["default_name_format"] = default_name_format
symbol_path = self.uiSymbolLineEdit.text()
settings["symbol"] = symbol_path
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
settings["ports_mapping"] = []
for port_number in range(0, nb_ports):
settings["ports_mapping"].append({"port_number": int(port_number),
"name": "Ethernet{}".format(port_number)})
return settings

View File

@@ -0,0 +1,189 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Configuration page for Ethernet hub preferences.
"""
import copy
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
from gns3.main_window import MainWindow
from gns3.dialogs.configuration_dialog import ConfigurationDialog
from gns3.compute_manager import ComputeManager
from gns3.controller import Controller
from .. import Builtin
from ..settings import ETHERNET_HUB_SETTINGS
from ..ui.ethernet_hub_preferences_page_ui import Ui_EthernetHubPreferencesPageWidget
from ..pages.ethernet_hub_configuration_page import EthernetHubConfigurationPage
from ..dialogs.ethernet_hub_wizard import EthernetHubWizard
class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPageWidget):
"""
QWidget preference page for Ethernet hub preferences.
"""
def __init__(self):
super().__init__()
self.setupUi(self)
self._main_window = MainWindow.instance()
self._ethernet_hubs = {}
self._items = []
self.uiNewEthernetHubPushButton.clicked.connect(self._newEthernetHubSlot)
self.uiEditEthernetHubPushButton.clicked.connect(self._editEthernetHubSlot)
self.uiDeleteEthernetHubPushButton.clicked.connect(self._deleteEthernetHubSlot)
self.uiEthernetHubsTreeWidget.itemSelectionChanged.connect(self._ethernetHubChangedSlot)
def _createSectionItem(self, name):
section_item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubInfoTreeWidget)
section_item.setText(0, name)
font = section_item.font(0)
font.setBold(True)
section_item.setFont(0, font)
return section_item
def _refreshInfo(self, ethernet_hub):
self.uiEthernetHubInfoTreeWidget.clear()
# fill out the General section
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_hub["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_hub["default_name_format"]])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["server"]).name()])
except KeyError:
pass
QtWidgets.QTreeWidgetItem(section_item, ["Number of ports:", str(len(ethernet_hub["ports_mapping"]))])
self.uiEthernetHubInfoTreeWidget.expandAll()
self.uiEthernetHubInfoTreeWidget.resizeColumnToContents(0)
self.uiEthernetHubInfoTreeWidget.resizeColumnToContents(1)
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
def _ethernetHubChangedSlot(self):
"""
Loads a selected Ethernet hub template from the tree widget.
"""
selection = self.uiEthernetHubsTreeWidget.selectedItems()
self.uiDeleteEthernetHubPushButton.setEnabled(len(selection) != 0)
single_selected = len(selection) == 1
self.uiEditEthernetHubPushButton.setEnabled(single_selected)
if single_selected:
key = selection[0].data(0, QtCore.Qt.UserRole)
ethernet_hub = self._ethernet_hubs[key]
self._refreshInfo(ethernet_hub)
else:
self.uiEthernetHubInfoTreeWidget.clear()
def _newEthernetHubSlot(self):
"""
Creates a new Ethernet hub template.
"""
wizard = EthernetHubWizard(self._ethernet_hubs, parent=self)
wizard.show()
if wizard.exec_():
new_ethernet_hub_settings = wizard.getSettings()
key = "{server}:{name}".format(server=new_ethernet_hub_settings["server"], name=new_ethernet_hub_settings["name"])
self._ethernet_hubs[key] = ETHERNET_HUB_SETTINGS.copy()
self._ethernet_hubs[key].update(new_ethernet_hub_settings)
item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubsTreeWidget)
item.setText(0, self._ethernet_hubs[key]["name"])
Controller.instance().getSymbolIcon(self._ethernet_hubs[key]["symbol"], qpartial(self._setItemIcon, item))
item.setData(0, QtCore.Qt.UserRole, key)
self._items.append(item)
self.uiEthernetHubsTreeWidget.setCurrentItem(item)
def _editEthernetHubSlot(self):
"""
Edits an Ethernet hub template.
"""
item = self.uiEthernetHubsTreeWidget.currentItem()
if item:
key = item.data(0, QtCore.Qt.UserRole)
ethernet_hub = self._ethernet_hubs[key]
dialog = ConfigurationDialog(ethernet_hub["name"], ethernet_hub, EthernetHubConfigurationPage(), parent=self)
dialog.show()
if dialog.exec_():
# update the icon
Controller.instance().getSymbolIcon(ethernet_hub["symbol"], qpartial(self._setItemIcon, item))
if ethernet_hub["name"] != item.text(0):
new_key = "{server}:{name}".format(server=ethernet_hub["server"], name=ethernet_hub["name"])
if new_key in self._ethernet_hubs:
QtWidgets.QMessageBox.critical(self, "Ethernet hub", "Ethernet hub name {} already exists for server {}".format(ethernet_hub["name"],
ethernet_hub["server"]))
ethernet_hub["name"] = item.text(0)
return
self._ethernet_hubs[new_key] = self._ethernet_hubs[key]
del self._ethernet_hubs[key]
item.setText(0, ethernet_hub["name"])
item.setData(0, QtCore.Qt.UserRole, new_key)
self._refreshInfo(ethernet_hub)
def _deleteEthernetHubSlot(self):
"""
Deletes an Ethernet hub template.
"""
for item in self.uiEthernetHubsTreeWidget.selectedItems():
if item:
key = item.data(0, QtCore.Qt.UserRole)
del self._ethernet_hubs[key]
self.uiEthernetHubsTreeWidget.takeTopLevelItem(self.uiEthernetHubsTreeWidget.indexOfTopLevelItem(item))
def loadPreferences(self):
"""
Loads the ethernet hub preferences.
"""
builtin_module = Builtin.instance()
self._ethernet_hubs = copy.deepcopy(builtin_module.ethernetHubs())
self._items.clear()
for key, ethernet_hub in self._ethernet_hubs.items():
item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubsTreeWidget)
item.setText(0, ethernet_hub["name"])
Controller.instance().getSymbolIcon(ethernet_hub["symbol"], qpartial(self._setItemIcon, item))
item.setData(0, QtCore.Qt.UserRole, key)
self._items.append(item)
if self._items:
self.uiEthernetHubsTreeWidget.setCurrentItem(self._items[0])
self.uiEthernetHubsTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
def _setItemIcon(self, item, icon):
item.setIcon(0, icon)
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
def savePreferences(self):
"""
Saves the Ethernet hub preferences.
"""
Builtin.instance().setEthernetHubs(self._ethernet_hubs)

View File

@@ -16,10 +16,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Configuration page for Dynamips Ethernet switches.
Configuration page for Ethernet switches.
"""
from gns3.qt import QtCore, QtWidgets
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.node import Node
from ..utils.tree_widget_item import TreeWidgetItem
from ..ui.ethernet_switch_configuration_page_ui import Ui_ethernetSwitchConfigPageWidget
@@ -36,6 +39,10 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
self.setupUi(self)
self._ports = {}
# add the categories
for name, category in Node.defaultCategories().items():
self.uiCategoryComboBox.addItem(name, category)
# connect slots
self.uiAddPushButton.clicked.connect(self._addPortSlot)
self.uiDeletePushButton.clicked.connect(self._deletePortSlot)
@@ -47,6 +54,21 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
self.uiPortsTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiPortsTreeWidget.setSortingEnabled(True)
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
def _symbolBrowserSlot(self):
"""
Slot to open the symbol browser and select a new symbol.
"""
symbol_path = self.uiSymbolLineEdit.text()
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
dialog.show()
if dialog.exec_():
new_symbol_path = dialog.getSymbol()
self.uiSymbolLineEdit.setText(new_symbol_path)
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
def _portSelectedSlot(self, item, column):
"""
Loads a selected port from the tree widget.
@@ -123,7 +145,9 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
item.setText(3, port_ethertype)
self.uiPortsTreeWidget.addTopLevelItem(item)
self._ports[port] = {"type": port_type,
self._ports[port] = {"name": "Ethernet{}".format(port),
"port_number": port,
"type": port_type,
"vlan": vlan,
"ethertype": port_ethertype}
@@ -138,11 +162,11 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
item = self.uiPortsTreeWidget.currentItem()
if item:
port = int(item.text(0))
node_ports = self._node.ports()
for node_port in node_ports:
if node_port.portNumber() == port and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to port {}, please remove it first".format(node_port.name()))
return
if self._node:
for node_port in self._node.ports():
if node_port.portNumber() == port and not node_port.isFree():
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to port {}, please remove it first".format(node_port.name()))
return
del self._ports[port]
self.uiPortsTreeWidget.takeTopLevelItem(self.uiPortsTreeWidget.indexOfTopLevelItem(item))
@@ -151,7 +175,7 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
else:
self.uiPortSpinBox.setValue(1)
def loadSettings(self, settings, node, group=False):
def loadSettings(self, settings, node=None, group=False):
"""
Loads the Ethernet switch settings.
@@ -169,21 +193,48 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
self._ports = {}
self._node = node
for port, info in settings["ports"].items():
if not node:
# these are template settings
# rename the label from "Name" to "Template name"
self.uiNameLabel.setText("Template name:")
# load the default name format
self.uiDefaultNameFormatLineEdit.setText(settings["default_name_format"])
# load the symbol
self.uiSymbolLineEdit.setText(settings["symbol"])
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(settings["symbol"]))
# load the category
index = self.uiCategoryComboBox.findData(settings["category"])
if index != -1:
self.uiCategoryComboBox.setCurrentIndex(index)
else:
self.uiDefaultNameFormatLabel.hide()
self.uiDefaultNameFormatLineEdit.hide()
self.uiSymbolLabel.hide()
self.uiSymbolLineEdit.hide()
self.uiSymbolToolButton.hide()
self.uiCategoryComboBox.hide()
self.uiCategoryLabel.hide()
self.uiCategoryComboBox.hide()
for port_info in settings["ports_mapping"]:
item = TreeWidgetItem(self.uiPortsTreeWidget)
item.setText(0, str(port))
item.setText(1, str(info["vlan"]))
item.setText(2, info["type"])
item.setText(3, info["ethertype"])
item.setText(0, str(port_info["port_number"]))
item.setText(1, str(port_info["vlan"]))
item.setText(2, port_info["type"])
item.setText(3, port_info.get("ethertype", ""))
self.uiPortsTreeWidget.addTopLevelItem(item)
self._ports[port] = info
self._ports[port_info["port_number"]] = port_info
self.uiPortsTreeWidget.resizeColumnToContents(0)
self.uiPortsTreeWidget.resizeColumnToContents(1)
if len(self._ports) > 0:
self.uiPortSpinBox.setValue(max(self._ports) + 1)
def saveSettings(self, settings, node, group=False):
def saveSettings(self, settings, node=None, group=False):
"""
Saves the Ethernet switch settings.
@@ -199,7 +250,21 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
QtWidgets.QMessageBox.critical(self, "Name", "Ethernet switch name cannot be empty!")
else:
settings["name"] = name
else:
del settings["name"]
settings["ports"] = self._ports.copy()
if not node:
# these are template settings
# save the default name format
default_name_format = self.uiDefaultNameFormatLineEdit.text().strip()
if '{0}' not in default_name_format and '{id}' not in default_name_format:
QtWidgets.QMessageBox.critical(self, "Default name format", "The default name format must contain at least {0} or {id}")
else:
settings["default_name_format"] = default_name_format
symbol_path = self.uiSymbolLineEdit.text()
settings["symbol"] = symbol_path
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
settings["ports_mapping"] = list(self._ports.values())
return settings

View File

@@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Configuration page for Ethernet switch preferences.
"""
import copy
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
from gns3.controller import Controller
from gns3.main_window import MainWindow
from gns3.dialogs.configuration_dialog import ConfigurationDialog
from gns3.compute_manager import ComputeManager
from .. import Builtin
from ..settings import ETHERNET_SWITCH_SETTINGS
from ..ui.ethernet_switch_preferences_page_ui import Ui_EthernetSwitchPreferencesPageWidget
from ..pages.ethernet_switch_configuration_page import EthernetSwitchConfigurationPage
from ..dialogs.ethernet_switch_wizard import EthernetSwitchWizard
class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferencesPageWidget):
"""
QWidget preference page for Ethernet switch preferences.
"""
def __init__(self):
super().__init__()
self.setupUi(self)
self._main_window = MainWindow.instance()
self._ethernet_switches = {}
self._items = []
self.uiNewEthernetSwitchPushButton.clicked.connect(self._newEthernetSwitchSlot)
self.uiEditEthernetSwitchPushButton.clicked.connect(self._editEthernetSwitchSlot)
self.uiDeleteEthernetSwitchPushButton.clicked.connect(self._deleteEthernetSwitchSlot)
self.uiEthernetSwitchesTreeWidget.itemSelectionChanged.connect(self._ethernetSwitchChangedSlot)
def _createSectionItem(self, name):
section_item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchInfoTreeWidget)
section_item.setText(0, name)
font = section_item.font(0)
font.setBold(True)
section_item.setFont(0, font)
return section_item
def _refreshInfo(self, ethernet_switch):
self.uiEthernetSwitchInfoTreeWidget.clear()
# fill out the General section
section_item = self._createSectionItem("General")
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_switch["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_switch["default_name_format"]])
try:
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["server"]).name()])
except KeyError:
pass
for port in ethernet_switch["ports_mapping"]:
section_item = self._createSectionItem("Port{}".format(port["port_number"]))
QtWidgets.QTreeWidgetItem(section_item, ["Name:", port["name"]])
QtWidgets.QTreeWidgetItem(section_item, ["Type:", port["type"]])
QtWidgets.QTreeWidgetItem(section_item, ["VLAN:", str(port["vlan"])])
self.uiEthernetSwitchInfoTreeWidget.expandAll()
self.uiEthernetSwitchInfoTreeWidget.resizeColumnToContents(0)
self.uiEthernetSwitchInfoTreeWidget.resizeColumnToContents(1)
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
def _ethernetSwitchChangedSlot(self):
"""
Loads a selected Ethernet switch template from the tree widget.
"""
selection = self.uiEthernetSwitchesTreeWidget.selectedItems()
self.uiDeleteEthernetSwitchPushButton.setEnabled(len(selection) != 0)
single_selected = len(selection) == 1
self.uiEditEthernetSwitchPushButton.setEnabled(single_selected)
if single_selected:
key = selection[0].data(0, QtCore.Qt.UserRole)
ethernet_switch = self._ethernet_switches[key]
self._refreshInfo(ethernet_switch)
else:
self.uiEthernetSwitchInfoTreeWidget.clear()
def _newEthernetSwitchSlot(self):
"""
Creates a new Ethernet switch template.
"""
wizard = EthernetSwitchWizard(self._ethernet_switches, parent=self)
wizard.show()
if wizard.exec_():
new_ethernet_switch_settings = wizard.getSettings()
key = "{server}:{name}".format(server=new_ethernet_switch_settings["server"], name=new_ethernet_switch_settings["name"])
self._ethernet_switches[key] = ETHERNET_SWITCH_SETTINGS.copy()
self._ethernet_switches[key].update(new_ethernet_switch_settings)
item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchesTreeWidget)
item.setText(0, self._ethernet_switches[key]["name"])
Controller.instance().getSymbolIcon(self._ethernet_switches[key]["symbol"], qpartial(self._setItemIcon, item))
item.setData(0, QtCore.Qt.UserRole, key)
self._items.append(item)
self.uiEthernetSwitchesTreeWidget.setCurrentItem(item)
def _editEthernetSwitchSlot(self):
"""
Edits an Ethernet switch template.
"""
item = self.uiEthernetSwitchesTreeWidget.currentItem()
if item:
key = item.data(0, QtCore.Qt.UserRole)
ethernet_switch = self._ethernet_switches[key]
dialog = ConfigurationDialog(ethernet_switch["name"], ethernet_switch, EthernetSwitchConfigurationPage(), parent=self)
dialog.show()
if dialog.exec_():
# update the icon
Controller.instance().getSymbolIcon(ethernet_switch["symbol"], qpartial(self._setItemIcon, item))
if ethernet_switch["name"] != item.text(0):
new_key = "{server}:{name}".format(server=ethernet_switch["server"], name=ethernet_switch["name"])
if new_key in self._ethernet_switches:
QtWidgets.QMessageBox.critical(self, "Ethernet switch", "Ethernet switch name {} already exists for server {}".format(ethernet_switch["name"],
ethernet_switch["server"]))
ethernet_switch["name"] = item.text(0)
return
self._ethernet_switches[new_key] = self._ethernet_switches[key]
del self._ethernet_switches[key]
item.setText(0, ethernet_switch["name"])
item.setData(0, QtCore.Qt.UserRole, new_key)
self._refreshInfo(ethernet_switch)
def _deleteEthernetSwitchSlot(self):
"""
Deletes an Ethernet switch template.
"""
for item in self.uiEthernetSwitchesTreeWidget.selectedItems():
if item:
key = item.data(0, QtCore.Qt.UserRole)
del self._ethernet_switches[key]
self.uiEthernetSwitchesTreeWidget.takeTopLevelItem(self.uiEthernetSwitchesTreeWidget.indexOfTopLevelItem(item))
def loadPreferences(self):
"""
Loads the ethernet switch preferences.
"""
builtin_module = Builtin.instance()
self._ethernet_switches = copy.deepcopy(builtin_module.ethernetSwitches())
self._items.clear()
for key, ethernet_switch in self._ethernet_switches.items():
item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchesTreeWidget)
item.setText(0, ethernet_switch["name"])
Controller.instance().getSymbolIcon(ethernet_switch["symbol"], qpartial(self._setItemIcon, item))
item.setData(0, QtCore.Qt.UserRole, key)
self._items.append(item)
if self._items:
self.uiEthernetSwitchesTreeWidget.setCurrentItem(self._items[0])
self.uiEthernetSwitchesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
def savePreferences(self):
"""
Saves the Ethernet switch preferences.
"""
Builtin.instance().setEthernetSwitches(self._ethernet_switches)
def _setItemIcon(self, item, icon):
item.setIcon(0, icon)
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)

View File

@@ -170,7 +170,6 @@ class FrameRelaySwitchConfigurationPage(QtWidgets.QWidget, Ui_frameRelaySwitchCo
QtWidgets.QMessageBox.critical(self, "Name", "Frame relay switch name cannot be empty!")
else:
settings["name"] = name
else:
del settings["name"]
settings["mappings"] = self._mapping.copy()
return settings

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Default Built-in settings.
"""
from gns3.node import Node
BUILTIN_SETTINGS = {
}
NAT_SETTINGS = {
"name": "",
"default_name_format": "Nat{0}",
"symbol": ":/symbols/cloud.svg",
"category": Node.end_devices,
"ports_mapping": [],
}
CLOUD_SETTINGS = {
"name": "",
"default_name_format": "Cloud{0}",
"symbol": ":/symbols/cloud.svg",
"category": Node.end_devices,
"ports_mapping": [],
}
ETHERNET_HUB_SETTINGS = {
"name": "",
"default_name_format": "Hub{0}",
"symbol": ":/symbols/hub.svg",
"category": Node.switches,
"ports_mapping": [],
}
ETHERNET_SWITCH_SETTINGS = {
"name": "",
"default_name_format": "Switch{0}",
"symbol": ":/symbols/ethernet_switch.svg",
"category": Node.switches,
"ports_mapping": [],
}

View File

@@ -6,13 +6,16 @@
<rect>
<x>0</x>
<y>0</y>
<width>459</width>
<height>419</height>
<width>540</width>
<height>553</height>
</rect>
</property>
<property name="windowTitle">
<string>ATM Switch</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This is a simple ATM switch. Only IOS c7200 routers with at least a configured PA-A1 adapter can connect to it.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="3">
<widget class="QGroupBox" name="uiGeneralGroupBox">

View File

@@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/dynamips/ui/atm_switch_configuration_page.ui'
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/atm_switch_configuration_page.ui'
#
# Created by: PyQt5 UI code generator 5.4.2
# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_atmSwitchConfigPageWidget(object):
def setupUi(self, atmSwitchConfigPageWidget):
atmSwitchConfigPageWidget.setObjectName("atmSwitchConfigPageWidget")
atmSwitchConfigPageWidget.resize(459, 419)
atmSwitchConfigPageWidget.resize(459, 430)
self.gridLayout_2 = QtWidgets.QGridLayout(atmSwitchConfigPageWidget)
self.gridLayout_2.setObjectName("gridLayout_2")
self.uiGeneralGroupBox = QtWidgets.QGroupBox(atmSwitchConfigPageWidget)
@@ -170,6 +168,7 @@ class Ui_atmSwitchConfigPageWidget(object):
def retranslateUi(self, atmSwitchConfigPageWidget):
_translate = QtCore.QCoreApplication.translate
atmSwitchConfigPageWidget.setWindowTitle(_translate("atmSwitchConfigPageWidget", "ATM Switch"))
atmSwitchConfigPageWidget.setWhatsThis(_translate("atmSwitchConfigPageWidget", "<html><head/><body><p>This is a simple ATM switch. Only IOS c7200 routers with at least a configured PA-A1 adapter can connect to it.</p></body></html>"))
self.uiGeneralGroupBox.setTitle(_translate("atmSwitchConfigPageWidget", "General"))
self.uiNameLabel.setText(_translate("atmSwitchConfigPageWidget", "Name:"))
self.uiVPICheckBox.setText(_translate("atmSwitchConfigPageWidget", "Use VPI only (VP tunnel)"))
@@ -186,3 +185,4 @@ class Ui_atmSwitchConfigPageWidget(object):
self.uiDestinationPortLabel.setText(_translate("atmSwitchConfigPageWidget", "Port:"))
self.uiDestinationVPILabel.setText(_translate("atmSwitchConfigPageWidget", "VPI:"))
self.uiDestinationVCILabel.setText(_translate("atmSwitchConfigPageWidget", "VCI:"))

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BuiltinPreferencesPageWidget</class>
<widget class="QWidget" name="BuiltinPreferencesPageWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>330</width>
<height>200</height>
</rect>
</property>
<property name="windowTitle">
<string>Built-in</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="uiTabWidget">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="uiServerSettingsTabWidget">
<attribute name="title">
<string>Local settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>254</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="uiRestoreDefaultsPushButton">
<property name="text">
<string>Restore defaults</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
<designerdata>
<property name="gridDeltaX">
<number>10</number>
</property>
<property name="gridDeltaY">
<number>10</number>
</property>
<property name="gridSnapX">
<bool>true</bool>
</property>
<property name="gridSnapY">
<bool>true</bool>
</property>
<property name="gridVisible">
<bool>true</bool>
</property>
</designerdata>
</ui>

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/builtin_preferences_page.ui'
#
# Created: Wed Dec 7 21:40:18 2016
# by: PyQt5 UI code generator 5.2.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_BuiltinPreferencesPageWidget(object):
def setupUi(self, BuiltinPreferencesPageWidget):
BuiltinPreferencesPageWidget.setObjectName("BuiltinPreferencesPageWidget")
BuiltinPreferencesPageWidget.resize(330, 200)
self.verticalLayout = QtWidgets.QVBoxLayout(BuiltinPreferencesPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(BuiltinPreferencesPageWidget)
self.uiTabWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.uiTabWidget.setObjectName("uiTabWidget")
self.uiServerSettingsTabWidget = QtWidgets.QWidget()
self.uiServerSettingsTabWidget.setObjectName("uiServerSettingsTabWidget")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.uiServerSettingsTabWidget)
self.verticalLayout_2.setObjectName("verticalLayout_2")
spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_2.addItem(spacerItem)
self.uiTabWidget.addTab(self.uiServerSettingsTabWidget, "")
self.verticalLayout.addWidget(self.uiTabWidget)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
spacerItem1 = QtWidgets.QSpacerItem(254, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem1)
self.uiRestoreDefaultsPushButton = QtWidgets.QPushButton(BuiltinPreferencesPageWidget)
self.uiRestoreDefaultsPushButton.setObjectName("uiRestoreDefaultsPushButton")
self.horizontalLayout_2.addWidget(self.uiRestoreDefaultsPushButton)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.retranslateUi(BuiltinPreferencesPageWidget)
self.uiTabWidget.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(BuiltinPreferencesPageWidget)
def retranslateUi(self, BuiltinPreferencesPageWidget):
_translate = QtCore.QCoreApplication.translate
BuiltinPreferencesPageWidget.setWindowTitle(_translate("BuiltinPreferencesPageWidget", "Built-in"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("BuiltinPreferencesPageWidget", "Local settings"))
self.uiRestoreDefaultsPushButton.setText(_translate("BuiltinPreferencesPageWidget", "Restore defaults"))

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
#
# Created: Fri May 27 22:47:08 2016
# by: PyQt5 UI code generator 5.2.1
# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
@@ -12,159 +11,132 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_cloudConfigPageWidget(object):
def setupUi(self, cloudConfigPageWidget):
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
cloudConfigPageWidget.resize(653, 478)
self.vboxlayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
self.vboxlayout.setObjectName("vboxlayout")
self.uiNIOsTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
self.uiNIOsTabWidget.setObjectName("uiNIOsTabWidget")
self.NIOEthernetTab = QtWidgets.QWidget()
self.NIOEthernetTab.setObjectName("NIOEthernetTab")
self.vboxlayout1 = QtWidgets.QVBoxLayout(self.NIOEthernetTab)
self.vboxlayout1.setObjectName("vboxlayout1")
self.uiGenericEthernetGroupBox = QtWidgets.QGroupBox(self.NIOEthernetTab)
self.uiGenericEthernetGroupBox.setObjectName("uiGenericEthernetGroupBox")
self.gridlayout = QtWidgets.QGridLayout(self.uiGenericEthernetGroupBox)
self.gridlayout.setObjectName("gridlayout")
self.uiGenericEthernetComboBox = QtWidgets.QComboBox(self.uiGenericEthernetGroupBox)
cloudConfigPageWidget.resize(758, 299)
self.verticalLayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
self.uiTabWidget.setObjectName("uiTabWidget")
self.EthernetTab = QtWidgets.QWidget()
self.EthernetTab.setObjectName("EthernetTab")
self.gridLayout_3 = QtWidgets.QGridLayout(self.EthernetTab)
self.gridLayout_3.setObjectName("gridLayout_3")
self.uiEthernetComboBox = QtWidgets.QComboBox(self.EthernetTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiGenericEthernetComboBox.sizePolicy().hasHeightForWidth())
self.uiGenericEthernetComboBox.setSizePolicy(sizePolicy)
self.uiGenericEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
self.uiGenericEthernetComboBox.setObjectName("uiGenericEthernetComboBox")
self.gridlayout.addWidget(self.uiGenericEthernetComboBox, 0, 0, 1, 3)
self.uiGenericEthernetLineEdit = QtWidgets.QLineEdit(self.uiGenericEthernetGroupBox)
self.uiGenericEthernetLineEdit.setObjectName("uiGenericEthernetLineEdit")
self.gridlayout.addWidget(self.uiGenericEthernetLineEdit, 1, 0, 1, 1)
self.uiAddGenericEthernetPushButton = QtWidgets.QPushButton(self.uiGenericEthernetGroupBox)
self.uiAddGenericEthernetPushButton.setObjectName("uiAddGenericEthernetPushButton")
self.gridlayout.addWidget(self.uiAddGenericEthernetPushButton, 1, 1, 1, 1)
self.uiDeleteGenericEthernetPushButton = QtWidgets.QPushButton(self.uiGenericEthernetGroupBox)
self.uiDeleteGenericEthernetPushButton.setEnabled(False)
self.uiDeleteGenericEthernetPushButton.setObjectName("uiDeleteGenericEthernetPushButton")
self.gridlayout.addWidget(self.uiDeleteGenericEthernetPushButton, 1, 2, 1, 1)
self.uiGenericEthernetListWidget = QtWidgets.QListWidget(self.uiGenericEthernetGroupBox)
self.uiGenericEthernetListWidget.setObjectName("uiGenericEthernetListWidget")
self.gridlayout.addWidget(self.uiGenericEthernetListWidget, 2, 0, 1, 3)
self.vboxlayout1.addWidget(self.uiGenericEthernetGroupBox)
self.uiLinuxEthernetGroupBox = QtWidgets.QGroupBox(self.NIOEthernetTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiLinuxEthernetGroupBox.sizePolicy().hasHeightForWidth())
self.uiLinuxEthernetGroupBox.setSizePolicy(sizePolicy)
self.uiLinuxEthernetGroupBox.setObjectName("uiLinuxEthernetGroupBox")
self.gridlayout1 = QtWidgets.QGridLayout(self.uiLinuxEthernetGroupBox)
self.gridlayout1.setObjectName("gridlayout1")
self.uiLinuxEthernetComboBox = QtWidgets.QComboBox(self.uiLinuxEthernetGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiLinuxEthernetComboBox.sizePolicy().hasHeightForWidth())
self.uiLinuxEthernetComboBox.setSizePolicy(sizePolicy)
self.uiLinuxEthernetComboBox.setObjectName("uiLinuxEthernetComboBox")
self.gridlayout1.addWidget(self.uiLinuxEthernetComboBox, 0, 0, 1, 3)
self.uiLinuxEthernetLineEdit = QtWidgets.QLineEdit(self.uiLinuxEthernetGroupBox)
self.uiLinuxEthernetLineEdit.setObjectName("uiLinuxEthernetLineEdit")
self.gridlayout1.addWidget(self.uiLinuxEthernetLineEdit, 1, 0, 1, 1)
self.uiAddLinuxEthernetPushButton = QtWidgets.QPushButton(self.uiLinuxEthernetGroupBox)
self.uiAddLinuxEthernetPushButton.setObjectName("uiAddLinuxEthernetPushButton")
self.gridlayout1.addWidget(self.uiAddLinuxEthernetPushButton, 1, 1, 1, 1)
self.uiDeleteLinuxEthernetPushButton = QtWidgets.QPushButton(self.uiLinuxEthernetGroupBox)
self.uiDeleteLinuxEthernetPushButton.setEnabled(False)
self.uiDeleteLinuxEthernetPushButton.setObjectName("uiDeleteLinuxEthernetPushButton")
self.gridlayout1.addWidget(self.uiDeleteLinuxEthernetPushButton, 1, 2, 1, 1)
self.uiLinuxEthernetListWidget = QtWidgets.QListWidget(self.uiLinuxEthernetGroupBox)
self.uiLinuxEthernetListWidget.setObjectName("uiLinuxEthernetListWidget")
self.gridlayout1.addWidget(self.uiLinuxEthernetListWidget, 2, 0, 1, 3)
self.vboxlayout1.addWidget(self.uiLinuxEthernetGroupBox)
spacerItem = QtWidgets.QSpacerItem(21, 16, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
self.vboxlayout1.addItem(spacerItem)
self.uiNIOsTabWidget.addTab(self.NIOEthernetTab, "")
self.NIONATTab = QtWidgets.QWidget()
self.NIONATTab.setObjectName("NIONATTab")
self.gridLayout_2 = QtWidgets.QGridLayout(self.NIONATTab)
sizePolicy.setHeightForWidth(self.uiEthernetComboBox.sizePolicy().hasHeightForWidth())
self.uiEthernetComboBox.setSizePolicy(sizePolicy)
self.uiEthernetComboBox.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
self.uiEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
self.uiEthernetComboBox.setObjectName("uiEthernetComboBox")
self.gridLayout_3.addWidget(self.uiEthernetComboBox, 0, 0, 1, 1)
self.uiAddEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiAddEthernetPushButton.setObjectName("uiAddEthernetPushButton")
self.gridLayout_3.addWidget(self.uiAddEthernetPushButton, 0, 2, 1, 1)
self.uiAddAllEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiAddAllEthernetPushButton.setObjectName("uiAddAllEthernetPushButton")
self.gridLayout_3.addWidget(self.uiAddAllEthernetPushButton, 0, 3, 1, 1)
self.uiDeleteEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiDeleteEthernetPushButton.setEnabled(False)
self.uiDeleteEthernetPushButton.setObjectName("uiDeleteEthernetPushButton")
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 4, 1, 1)
self.uiEthernetListWidget = QtWidgets.QListWidget(self.EthernetTab)
self.uiEthernetListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.uiEthernetListWidget.setObjectName("uiEthernetListWidget")
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 5)
self.uiEthernetWarningPushButton = QtWidgets.QPushButton(self.EthernetTab)
self.uiEthernetWarningPushButton.setText("")
icon = QtGui.QIcon.fromTheme("dialog-warning")
self.uiEthernetWarningPushButton.setIcon(icon)
self.uiEthernetWarningPushButton.setObjectName("uiEthernetWarningPushButton")
self.gridLayout_3.addWidget(self.uiEthernetWarningPushButton, 0, 1, 1, 1)
self.uiShowSpecialInterfacesCheckBox = QtWidgets.QCheckBox(self.EthernetTab)
self.uiShowSpecialInterfacesCheckBox.setObjectName("uiShowSpecialInterfacesCheckBox")
self.gridLayout_3.addWidget(self.uiShowSpecialInterfacesCheckBox, 2, 0, 1, 2)
self.uiEthernetListWidget.raise_()
self.uiEthernetComboBox.raise_()
self.uiAddEthernetPushButton.raise_()
self.uiDeleteEthernetPushButton.raise_()
self.uiAddAllEthernetPushButton.raise_()
self.uiShowSpecialInterfacesCheckBox.raise_()
self.uiEthernetWarningPushButton.raise_()
self.uiTabWidget.addTab(self.EthernetTab, "")
self.TAPTab = QtWidgets.QWidget()
self.TAPTab.setObjectName("TAPTab")
self.gridLayout_2 = QtWidgets.QGridLayout(self.TAPTab)
self.gridLayout_2.setObjectName("gridLayout_2")
self.uiNIONATSettingsGroupBox = QtWidgets.QGroupBox(self.NIONATTab)
self.uiNIONATSettingsGroupBox.setObjectName("uiNIONATSettingsGroupBox")
self._2 = QtWidgets.QGridLayout(self.uiNIONATSettingsGroupBox)
self._2.setObjectName("_2")
self.uiNIONATIdentifierLabel = QtWidgets.QLabel(self.uiNIONATSettingsGroupBox)
self.uiNIONATIdentifierLabel.setObjectName("uiNIONATIdentifierLabel")
self._2.addWidget(self.uiNIONATIdentifierLabel, 0, 0, 1, 1)
self.uiNIONATIdentiferLineEdit = QtWidgets.QLineEdit(self.uiNIONATSettingsGroupBox)
self.uiDeleteTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
self.uiDeleteTAPPushButton.setEnabled(False)
self.uiDeleteTAPPushButton.setObjectName("uiDeleteTAPPushButton")
self.gridLayout_2.addWidget(self.uiDeleteTAPPushButton, 1, 4, 1, 1)
self.uiTAPListWidget = QtWidgets.QListWidget(self.TAPTab)
self.uiTAPListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.uiTAPListWidget.setObjectName("uiTAPListWidget")
self.gridLayout_2.addWidget(self.uiTAPListWidget, 2, 0, 1, 5)
self.uiTAPLineEdit = QtWidgets.QLineEdit(self.TAPTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiTAPLineEdit.sizePolicy().hasHeightForWidth())
self.uiTAPLineEdit.setSizePolicy(sizePolicy)
self.uiTAPLineEdit.setObjectName("uiTAPLineEdit")
self.gridLayout_2.addWidget(self.uiTAPLineEdit, 1, 1, 1, 1)
self.uiAddTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
self.uiAddTAPPushButton.setObjectName("uiAddTAPPushButton")
self.gridLayout_2.addWidget(self.uiAddTAPPushButton, 1, 2, 1, 1)
self.uiTAPComboBox = QtWidgets.QComboBox(self.TAPTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiNIONATIdentiferLineEdit.sizePolicy().hasHeightForWidth())
self.uiNIONATIdentiferLineEdit.setSizePolicy(sizePolicy)
self.uiNIONATIdentiferLineEdit.setObjectName("uiNIONATIdentiferLineEdit")
self._2.addWidget(self.uiNIONATIdentiferLineEdit, 1, 0, 1, 1)
self.gridLayout_2.addWidget(self.uiNIONATSettingsGroupBox, 0, 0, 1, 2)
self.uiNIONATListGroupBox = QtWidgets.QGroupBox(self.NIONATTab)
self.uiNIONATListGroupBox.setObjectName("uiNIONATListGroupBox")
self._3 = QtWidgets.QVBoxLayout(self.uiNIONATListGroupBox)
self._3.setObjectName("_3")
self.uiNIONATListWidget = QtWidgets.QListWidget(self.uiNIONATListGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHeightForWidth(self.uiTAPComboBox.sizePolicy().hasHeightForWidth())
self.uiTAPComboBox.setSizePolicy(sizePolicy)
self.uiTAPComboBox.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
self.uiTAPComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
self.uiTAPComboBox.setObjectName("uiTAPComboBox")
self.gridLayout_2.addWidget(self.uiTAPComboBox, 0, 1, 1, 4)
self.uiAddAllTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
self.uiAddAllTAPPushButton.setObjectName("uiAddAllTAPPushButton")
self.gridLayout_2.addWidget(self.uiAddAllTAPPushButton, 1, 3, 1, 1)
self.uiTabWidget.addTab(self.TAPTab, "")
self.UDPTab = QtWidgets.QWidget()
self.UDPTab.setObjectName("UDPTab")
self.gridLayout_5 = QtWidgets.QGridLayout(self.UDPTab)
self.gridLayout_5.setObjectName("gridLayout_5")
self.uiUDPTunnelSettingsGroupBox = QtWidgets.QGroupBox(self.UDPTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiNIONATListWidget.sizePolicy().hasHeightForWidth())
self.uiNIONATListWidget.setSizePolicy(sizePolicy)
self.uiNIONATListWidget.setObjectName("uiNIONATListWidget")
self._3.addWidget(self.uiNIONATListWidget)
self.gridLayout_2.addWidget(self.uiNIONATListGroupBox, 0, 2, 3, 1)
self.uiAddNIONATPushButton = QtWidgets.QPushButton(self.NIONATTab)
self.uiAddNIONATPushButton.setObjectName("uiAddNIONATPushButton")
self.gridLayout_2.addWidget(self.uiAddNIONATPushButton, 1, 0, 1, 1)
self.uiDeleteNIONATPushButton = QtWidgets.QPushButton(self.NIONATTab)
self.uiDeleteNIONATPushButton.setEnabled(False)
self.uiDeleteNIONATPushButton.setObjectName("uiDeleteNIONATPushButton")
self.gridLayout_2.addWidget(self.uiDeleteNIONATPushButton, 1, 1, 1, 1)
spacerItem1 = QtWidgets.QSpacerItem(20, 294, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout_2.addItem(spacerItem1, 2, 0, 2, 1)
spacerItem2 = QtWidgets.QSpacerItem(20, 194, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout_2.addItem(spacerItem2, 3, 2, 1, 1)
self.uiNIOsTabWidget.addTab(self.NIONATTab, "")
self.NIOUDPTab = QtWidgets.QWidget()
self.NIOUDPTab.setObjectName("NIOUDPTab")
self.gridlayout2 = QtWidgets.QGridLayout(self.NIOUDPTab)
self.gridlayout2.setObjectName("gridlayout2")
self.uiNIOUDPSettingsGroupBox = QtWidgets.QGroupBox(self.NIOUDPTab)
self.uiNIOUDPSettingsGroupBox.setObjectName("uiNIOUDPSettingsGroupBox")
self.gridlayout3 = QtWidgets.QGridLayout(self.uiNIOUDPSettingsGroupBox)
self.gridlayout3.setObjectName("gridlayout3")
self.uiLocalPortLabel = QtWidgets.QLabel(self.uiNIOUDPSettingsGroupBox)
self.uiLocalPortLabel.setObjectName("uiLocalPortLabel")
self.gridlayout3.addWidget(self.uiLocalPortLabel, 0, 0, 1, 1)
self.uiLocalPortSpinBox = QtWidgets.QSpinBox(self.uiNIOUDPSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiLocalPortSpinBox.sizePolicy().hasHeightForWidth())
self.uiLocalPortSpinBox.setSizePolicy(sizePolicy)
self.uiLocalPortSpinBox.setMaximum(65535)
self.uiLocalPortSpinBox.setProperty("value", 30000)
self.uiLocalPortSpinBox.setObjectName("uiLocalPortSpinBox")
self.gridlayout3.addWidget(self.uiLocalPortSpinBox, 0, 1, 1, 1)
self.uiRemoteHostLabel = QtWidgets.QLabel(self.uiNIOUDPSettingsGroupBox)
self.uiRemoteHostLabel.setObjectName("uiRemoteHostLabel")
self.gridlayout3.addWidget(self.uiRemoteHostLabel, 1, 0, 1, 1)
self.uiRemoteHostLineEdit = QtWidgets.QLineEdit(self.uiNIOUDPSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHeightForWidth(self.uiUDPTunnelSettingsGroupBox.sizePolicy().hasHeightForWidth())
self.uiUDPTunnelSettingsGroupBox.setSizePolicy(sizePolicy)
self.uiUDPTunnelSettingsGroupBox.setObjectName("uiUDPTunnelSettingsGroupBox")
self.gridLayout_4 = QtWidgets.QGridLayout(self.uiUDPTunnelSettingsGroupBox)
self.gridLayout_4.setObjectName("gridLayout_4")
self.uiRemoteHostLineEdit = QtWidgets.QLineEdit(self.uiUDPTunnelSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiRemoteHostLineEdit.sizePolicy().hasHeightForWidth())
self.uiRemoteHostLineEdit.setSizePolicy(sizePolicy)
self.uiRemoteHostLineEdit.setMinimumSize(QtCore.QSize(80, 0))
self.uiRemoteHostLineEdit.setObjectName("uiRemoteHostLineEdit")
self.gridlayout3.addWidget(self.uiRemoteHostLineEdit, 1, 1, 1, 1)
self.uiRemotePortLabel = QtWidgets.QLabel(self.uiNIOUDPSettingsGroupBox)
self.gridLayout_4.addWidget(self.uiRemoteHostLineEdit, 2, 1, 1, 1)
self.uiRemotePortLabel = QtWidgets.QLabel(self.uiUDPTunnelSettingsGroupBox)
self.uiRemotePortLabel.setObjectName("uiRemotePortLabel")
self.gridlayout3.addWidget(self.uiRemotePortLabel, 2, 0, 1, 1)
self.uiRemotePortSpinBox = QtWidgets.QSpinBox(self.uiNIOUDPSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.gridLayout_4.addWidget(self.uiRemotePortLabel, 3, 0, 1, 1)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.uiAddUDPPushButton = QtWidgets.QPushButton(self.uiUDPTunnelSettingsGroupBox)
self.uiAddUDPPushButton.setObjectName("uiAddUDPPushButton")
self.horizontalLayout.addWidget(self.uiAddUDPPushButton)
self.uiDeleteUDPPushButton = QtWidgets.QPushButton(self.uiUDPTunnelSettingsGroupBox)
self.uiDeleteUDPPushButton.setEnabled(False)
self.uiDeleteUDPPushButton.setObjectName("uiDeleteUDPPushButton")
self.horizontalLayout.addWidget(self.uiDeleteUDPPushButton)
spacerItem = QtWidgets.QSpacerItem(50, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.gridLayout_4.addLayout(self.horizontalLayout, 4, 0, 1, 2)
self.uiRemotePortSpinBox = QtWidgets.QSpinBox(self.uiUDPTunnelSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiRemotePortSpinBox.sizePolicy().hasHeightForWidth())
@@ -172,220 +144,52 @@ class Ui_cloudConfigPageWidget(object):
self.uiRemotePortSpinBox.setMaximum(65535)
self.uiRemotePortSpinBox.setProperty("value", 20000)
self.uiRemotePortSpinBox.setObjectName("uiRemotePortSpinBox")
self.gridlayout3.addWidget(self.uiRemotePortSpinBox, 2, 1, 1, 1)
self.gridlayout2.addWidget(self.uiNIOUDPSettingsGroupBox, 0, 0, 1, 2)
self.uiNIOUDPListGroupBox = QtWidgets.QGroupBox(self.NIOUDPTab)
self.uiNIOUDPListGroupBox.setObjectName("uiNIOUDPListGroupBox")
self.vboxlayout2 = QtWidgets.QVBoxLayout(self.uiNIOUDPListGroupBox)
self.vboxlayout2.setObjectName("vboxlayout2")
self.uiNIOUDPListWidget = QtWidgets.QListWidget(self.uiNIOUDPListGroupBox)
self.uiNIOUDPListWidget.setObjectName("uiNIOUDPListWidget")
self.vboxlayout2.addWidget(self.uiNIOUDPListWidget)
self.gridlayout2.addWidget(self.uiNIOUDPListGroupBox, 0, 2, 2, 1)
self.uiAddNIOUDPPushButton = QtWidgets.QPushButton(self.NIOUDPTab)
self.uiAddNIOUDPPushButton.setObjectName("uiAddNIOUDPPushButton")
self.gridlayout2.addWidget(self.uiAddNIOUDPPushButton, 1, 0, 1, 1)
self.uiDeleteNIOUDPPushButton = QtWidgets.QPushButton(self.NIOUDPTab)
self.uiDeleteNIOUDPPushButton.setEnabled(False)
self.uiDeleteNIOUDPPushButton.setObjectName("uiDeleteNIOUDPPushButton")
self.gridlayout2.addWidget(self.uiDeleteNIOUDPPushButton, 1, 1, 1, 1)
spacerItem3 = QtWidgets.QSpacerItem(20, 211, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridlayout2.addItem(spacerItem3, 2, 1, 1, 1)
self.uiNIOsTabWidget.addTab(self.NIOUDPTab, "")
self.NIOTAPTab = QtWidgets.QWidget()
self.NIOTAPTab.setObjectName("NIOTAPTab")
self.vboxlayout3 = QtWidgets.QVBoxLayout(self.NIOTAPTab)
self.vboxlayout3.setObjectName("vboxlayout3")
self.uiNIOTAPGroupBox = QtWidgets.QGroupBox(self.NIOTAPTab)
self.uiNIOTAPGroupBox.setObjectName("uiNIOTAPGroupBox")
self.gridlayout4 = QtWidgets.QGridLayout(self.uiNIOTAPGroupBox)
self.gridlayout4.setObjectName("gridlayout4")
self.uiNIOTAPLineEdit = QtWidgets.QLineEdit(self.uiNIOTAPGroupBox)
self.uiNIOTAPLineEdit.setObjectName("uiNIOTAPLineEdit")
self.gridlayout4.addWidget(self.uiNIOTAPLineEdit, 0, 0, 1, 1)
self.uiAddNIOTAPPushButton = QtWidgets.QPushButton(self.uiNIOTAPGroupBox)
self.uiAddNIOTAPPushButton.setObjectName("uiAddNIOTAPPushButton")
self.gridlayout4.addWidget(self.uiAddNIOTAPPushButton, 0, 1, 1, 1)
self.uiDeleteNIOTAPPushButton = QtWidgets.QPushButton(self.uiNIOTAPGroupBox)
self.uiDeleteNIOTAPPushButton.setEnabled(False)
self.uiDeleteNIOTAPPushButton.setObjectName("uiDeleteNIOTAPPushButton")
self.gridlayout4.addWidget(self.uiDeleteNIOTAPPushButton, 0, 2, 1, 1)
self.uiNIOTAPListWidget = QtWidgets.QListWidget(self.uiNIOTAPGroupBox)
self.uiNIOTAPListWidget.setObjectName("uiNIOTAPListWidget")
self.gridlayout4.addWidget(self.uiNIOTAPListWidget, 1, 0, 1, 3)
self.vboxlayout3.addWidget(self.uiNIOTAPGroupBox)
spacerItem4 = QtWidgets.QSpacerItem(20, 191, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.vboxlayout3.addItem(spacerItem4)
self.uiNIOsTabWidget.addTab(self.NIOTAPTab, "")
self.NIOUnixTab = QtWidgets.QWidget()
self.NIOUnixTab.setObjectName("NIOUnixTab")
self.gridlayout5 = QtWidgets.QGridLayout(self.NIOUnixTab)
self.gridlayout5.setObjectName("gridlayout5")
self.uiNIOUNIXSettingsGroupBox = QtWidgets.QGroupBox(self.NIOUnixTab)
self.uiNIOUNIXSettingsGroupBox.setObjectName("uiNIOUNIXSettingsGroupBox")
self.gridlayout6 = QtWidgets.QGridLayout(self.uiNIOUNIXSettingsGroupBox)
self.gridlayout6.setObjectName("gridlayout6")
self.gridlayout7 = QtWidgets.QGridLayout()
self.gridlayout7.setObjectName("gridlayout7")
self.uiLocalFileLabel = QtWidgets.QLabel(self.uiNIOUNIXSettingsGroupBox)
self.uiLocalFileLabel.setObjectName("uiLocalFileLabel")
self.gridlayout7.addWidget(self.uiLocalFileLabel, 0, 0, 1, 1)
self.uiLocalFileLineEdit = QtWidgets.QLineEdit(self.uiNIOUNIXSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.gridLayout_4.addWidget(self.uiRemotePortSpinBox, 3, 1, 1, 1)
self.uiUDPNameLineEdit = QtWidgets.QLineEdit(self.uiUDPTunnelSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiLocalFileLineEdit.sizePolicy().hasHeightForWidth())
self.uiLocalFileLineEdit.setSizePolicy(sizePolicy)
self.uiLocalFileLineEdit.setObjectName("uiLocalFileLineEdit")
self.gridlayout7.addWidget(self.uiLocalFileLineEdit, 1, 0, 1, 1)
self.gridlayout6.addLayout(self.gridlayout7, 0, 0, 1, 1)
self.gridlayout8 = QtWidgets.QGridLayout()
self.gridlayout8.setObjectName("gridlayout8")
self.uiRemoteFileLabel = QtWidgets.QLabel(self.uiNIOUNIXSettingsGroupBox)
self.uiRemoteFileLabel.setObjectName("uiRemoteFileLabel")
self.gridlayout8.addWidget(self.uiRemoteFileLabel, 0, 0, 1, 1)
self.uiRemoteFileLineEdit = QtWidgets.QLineEdit(self.uiNIOUNIXSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHeightForWidth(self.uiUDPNameLineEdit.sizePolicy().hasHeightForWidth())
self.uiUDPNameLineEdit.setSizePolicy(sizePolicy)
self.uiUDPNameLineEdit.setObjectName("uiUDPNameLineEdit")
self.gridLayout_4.addWidget(self.uiUDPNameLineEdit, 0, 1, 1, 1)
self.uiRemoteHostLabel = QtWidgets.QLabel(self.uiUDPTunnelSettingsGroupBox)
self.uiRemoteHostLabel.setObjectName("uiRemoteHostLabel")
self.gridLayout_4.addWidget(self.uiRemoteHostLabel, 2, 0, 1, 1)
self.uiLocalPortSpinBox = QtWidgets.QSpinBox(self.uiUDPTunnelSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiRemoteFileLineEdit.sizePolicy().hasHeightForWidth())
self.uiRemoteFileLineEdit.setSizePolicy(sizePolicy)
self.uiRemoteFileLineEdit.setObjectName("uiRemoteFileLineEdit")
self.gridlayout8.addWidget(self.uiRemoteFileLineEdit, 1, 0, 1, 1)
self.gridlayout6.addLayout(self.gridlayout8, 1, 0, 1, 1)
self.gridlayout5.addWidget(self.uiNIOUNIXSettingsGroupBox, 0, 0, 1, 2)
self.uiNIOUNIXListGroupBox = QtWidgets.QGroupBox(self.NIOUnixTab)
self.uiNIOUNIXListGroupBox.setObjectName("uiNIOUNIXListGroupBox")
self.vboxlayout4 = QtWidgets.QVBoxLayout(self.uiNIOUNIXListGroupBox)
self.vboxlayout4.setObjectName("vboxlayout4")
self.uiNIOUNIXListWidget = QtWidgets.QListWidget(self.uiNIOUNIXListGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHeightForWidth(self.uiLocalPortSpinBox.sizePolicy().hasHeightForWidth())
self.uiLocalPortSpinBox.setSizePolicy(sizePolicy)
self.uiLocalPortSpinBox.setMaximum(65535)
self.uiLocalPortSpinBox.setProperty("value", 30000)
self.uiLocalPortSpinBox.setObjectName("uiLocalPortSpinBox")
self.gridLayout_4.addWidget(self.uiLocalPortSpinBox, 1, 1, 1, 1)
self.uiLocalPortLabel = QtWidgets.QLabel(self.uiUDPTunnelSettingsGroupBox)
self.uiLocalPortLabel.setObjectName("uiLocalPortLabel")
self.gridLayout_4.addWidget(self.uiLocalPortLabel, 1, 0, 1, 1)
self.uiUDPNameLabel = QtWidgets.QLabel(self.uiUDPTunnelSettingsGroupBox)
self.uiUDPNameLabel.setObjectName("uiUDPNameLabel")
self.gridLayout_4.addWidget(self.uiUDPNameLabel, 0, 0, 1, 1)
spacerItem1 = QtWidgets.QSpacerItem(20, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout_4.addItem(spacerItem1, 5, 0, 1, 1)
self.gridLayout_5.addWidget(self.uiUDPTunnelSettingsGroupBox, 0, 0, 1, 1)
self.uiUDPTunnelsGroupBox = QtWidgets.QGroupBox(self.UDPTab)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiNIOUNIXListWidget.sizePolicy().hasHeightForWidth())
self.uiNIOUNIXListWidget.setSizePolicy(sizePolicy)
self.uiNIOUNIXListWidget.setObjectName("uiNIOUNIXListWidget")
self.vboxlayout4.addWidget(self.uiNIOUNIXListWidget)
self.gridlayout5.addWidget(self.uiNIOUNIXListGroupBox, 0, 2, 3, 1)
self.uiAddNIOUNIXPushButton = QtWidgets.QPushButton(self.NIOUnixTab)
self.uiAddNIOUNIXPushButton.setObjectName("uiAddNIOUNIXPushButton")
self.gridlayout5.addWidget(self.uiAddNIOUNIXPushButton, 1, 0, 1, 1)
self.uiDeleteNIOUNIXPushButton = QtWidgets.QPushButton(self.NIOUnixTab)
self.uiDeleteNIOUNIXPushButton.setEnabled(False)
self.uiDeleteNIOUNIXPushButton.setObjectName("uiDeleteNIOUNIXPushButton")
self.gridlayout5.addWidget(self.uiDeleteNIOUNIXPushButton, 1, 1, 1, 1)
spacerItem5 = QtWidgets.QSpacerItem(160, 190, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
self.gridlayout5.addItem(spacerItem5, 2, 0, 2, 2)
spacerItem6 = QtWidgets.QSpacerItem(196, 132, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridlayout5.addItem(spacerItem6, 3, 2, 1, 1)
self.uiNIOsTabWidget.addTab(self.NIOUnixTab, "")
self.NIOVDETab = QtWidgets.QWidget()
self.NIOVDETab.setObjectName("NIOVDETab")
self.gridlayout9 = QtWidgets.QGridLayout(self.NIOVDETab)
self.gridlayout9.setObjectName("gridlayout9")
self.uiNIOVDESettingsGroupBox = QtWidgets.QGroupBox(self.NIOVDETab)
self.uiNIOVDESettingsGroupBox.setObjectName("uiNIOVDESettingsGroupBox")
self.gridlayout10 = QtWidgets.QGridLayout(self.uiNIOVDESettingsGroupBox)
self.gridlayout10.setObjectName("gridlayout10")
self.gridlayout11 = QtWidgets.QGridLayout()
self.gridlayout11.setObjectName("gridlayout11")
self.uiVDEControlFileLabel = QtWidgets.QLabel(self.uiNIOVDESettingsGroupBox)
self.uiVDEControlFileLabel.setObjectName("uiVDEControlFileLabel")
self.gridlayout11.addWidget(self.uiVDEControlFileLabel, 0, 0, 1, 1)
self.uiVDEControlFileLineEdit = QtWidgets.QLineEdit(self.uiNIOVDESettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiVDEControlFileLineEdit.sizePolicy().hasHeightForWidth())
self.uiVDEControlFileLineEdit.setSizePolicy(sizePolicy)
self.uiVDEControlFileLineEdit.setObjectName("uiVDEControlFileLineEdit")
self.gridlayout11.addWidget(self.uiVDEControlFileLineEdit, 1, 0, 1, 1)
self.gridlayout10.addLayout(self.gridlayout11, 0, 0, 1, 1)
self.gridlayout12 = QtWidgets.QGridLayout()
self.gridlayout12.setObjectName("gridlayout12")
self.uiVDELocalFileLabel = QtWidgets.QLabel(self.uiNIOVDESettingsGroupBox)
self.uiVDELocalFileLabel.setObjectName("uiVDELocalFileLabel")
self.gridlayout12.addWidget(self.uiVDELocalFileLabel, 0, 0, 1, 1)
self.uiVDELocalFileLineEdit = QtWidgets.QLineEdit(self.uiNIOVDESettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiVDELocalFileLineEdit.sizePolicy().hasHeightForWidth())
self.uiVDELocalFileLineEdit.setSizePolicy(sizePolicy)
self.uiVDELocalFileLineEdit.setObjectName("uiVDELocalFileLineEdit")
self.gridlayout12.addWidget(self.uiVDELocalFileLineEdit, 1, 0, 1, 1)
self.gridlayout10.addLayout(self.gridlayout12, 1, 0, 1, 1)
self.gridlayout9.addWidget(self.uiNIOVDESettingsGroupBox, 0, 0, 1, 2)
self.uiNIOVDEListGroupBox = QtWidgets.QGroupBox(self.NIOVDETab)
self.uiNIOVDEListGroupBox.setObjectName("uiNIOVDEListGroupBox")
self.vboxlayout5 = QtWidgets.QVBoxLayout(self.uiNIOVDEListGroupBox)
self.vboxlayout5.setObjectName("vboxlayout5")
self.uiNIOVDEListWidget = QtWidgets.QListWidget(self.uiNIOVDEListGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiNIOVDEListWidget.sizePolicy().hasHeightForWidth())
self.uiNIOVDEListWidget.setSizePolicy(sizePolicy)
self.uiNIOVDEListWidget.setObjectName("uiNIOVDEListWidget")
self.vboxlayout5.addWidget(self.uiNIOVDEListWidget)
self.gridlayout9.addWidget(self.uiNIOVDEListGroupBox, 0, 2, 3, 1)
self.uiAddNIOVDEPushButton = QtWidgets.QPushButton(self.NIOVDETab)
self.uiAddNIOVDEPushButton.setObjectName("uiAddNIOVDEPushButton")
self.gridlayout9.addWidget(self.uiAddNIOVDEPushButton, 1, 0, 1, 1)
self.uiDeleteNIOVDEPushButton = QtWidgets.QPushButton(self.NIOVDETab)
self.uiDeleteNIOVDEPushButton.setEnabled(False)
self.uiDeleteNIOVDEPushButton.setObjectName("uiDeleteNIOVDEPushButton")
self.gridlayout9.addWidget(self.uiDeleteNIOVDEPushButton, 1, 1, 1, 1)
spacerItem7 = QtWidgets.QSpacerItem(161, 201, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
self.gridlayout9.addItem(spacerItem7, 2, 0, 2, 2)
spacerItem8 = QtWidgets.QSpacerItem(196, 132, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridlayout9.addItem(spacerItem8, 3, 2, 1, 1)
self.uiNIOsTabWidget.addTab(self.NIOVDETab, "")
self.NIONullTab = QtWidgets.QWidget()
self.NIONullTab.setObjectName("NIONullTab")
self.gridlayout13 = QtWidgets.QGridLayout(self.NIONullTab)
self.gridlayout13.setObjectName("gridlayout13")
self.uiNIONullSettingsGroupBox = QtWidgets.QGroupBox(self.NIONullTab)
self.uiNIONullSettingsGroupBox.setObjectName("uiNIONullSettingsGroupBox")
self.gridlayout14 = QtWidgets.QGridLayout(self.uiNIONullSettingsGroupBox)
self.gridlayout14.setObjectName("gridlayout14")
self.uiNIONullIdentifierLabel = QtWidgets.QLabel(self.uiNIONullSettingsGroupBox)
self.uiNIONullIdentifierLabel.setObjectName("uiNIONullIdentifierLabel")
self.gridlayout14.addWidget(self.uiNIONullIdentifierLabel, 0, 0, 1, 1)
self.uiNIONullIdentiferLineEdit = QtWidgets.QLineEdit(self.uiNIONullSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiNIONullIdentiferLineEdit.sizePolicy().hasHeightForWidth())
self.uiNIONullIdentiferLineEdit.setSizePolicy(sizePolicy)
self.uiNIONullIdentiferLineEdit.setObjectName("uiNIONullIdentiferLineEdit")
self.gridlayout14.addWidget(self.uiNIONullIdentiferLineEdit, 1, 0, 1, 1)
self.gridlayout13.addWidget(self.uiNIONullSettingsGroupBox, 0, 0, 1, 2)
self.uiNIONullListGroupBox = QtWidgets.QGroupBox(self.NIONullTab)
self.uiNIONullListGroupBox.setObjectName("uiNIONullListGroupBox")
self.vboxlayout6 = QtWidgets.QVBoxLayout(self.uiNIONullListGroupBox)
self.vboxlayout6.setObjectName("vboxlayout6")
self.uiNIONullListWidget = QtWidgets.QListWidget(self.uiNIONullListGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiNIONullListWidget.sizePolicy().hasHeightForWidth())
self.uiNIONullListWidget.setSizePolicy(sizePolicy)
self.uiNIONullListWidget.setObjectName("uiNIONullListWidget")
self.vboxlayout6.addWidget(self.uiNIONullListWidget)
self.gridlayout13.addWidget(self.uiNIONullListGroupBox, 0, 2, 3, 1)
self.uiAddNIONullPushButton = QtWidgets.QPushButton(self.NIONullTab)
self.uiAddNIONullPushButton.setObjectName("uiAddNIONullPushButton")
self.gridlayout13.addWidget(self.uiAddNIONullPushButton, 1, 0, 1, 1)
self.uiDeleteNIONullPushButton = QtWidgets.QPushButton(self.NIONullTab)
self.uiDeleteNIONullPushButton.setEnabled(False)
self.uiDeleteNIONullPushButton.setObjectName("uiDeleteNIONullPushButton")
self.gridlayout13.addWidget(self.uiDeleteNIONullPushButton, 1, 1, 1, 1)
spacerItem9 = QtWidgets.QSpacerItem(20, 261, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridlayout13.addItem(spacerItem9, 2, 0, 2, 2)
spacerItem10 = QtWidgets.QSpacerItem(20, 181, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridlayout13.addItem(spacerItem10, 3, 2, 1, 1)
self.uiNIOsTabWidget.addTab(self.NIONullTab, "")
sizePolicy.setHeightForWidth(self.uiUDPTunnelsGroupBox.sizePolicy().hasHeightForWidth())
self.uiUDPTunnelsGroupBox.setSizePolicy(sizePolicy)
self.uiUDPTunnelsGroupBox.setObjectName("uiUDPTunnelsGroupBox")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.uiUDPTunnelsGroupBox)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.uiUDPTreeWidget = QtWidgets.QTreeWidget(self.uiUDPTunnelsGroupBox)
self.uiUDPTreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.uiUDPTreeWidget.setObjectName("uiUDPTreeWidget")
self.verticalLayout_2.addWidget(self.uiUDPTreeWidget)
self.gridLayout_5.addWidget(self.uiUDPTunnelsGroupBox, 0, 1, 1, 1)
self.uiTabWidget.addTab(self.UDPTab, "")
self.MiscTab = QtWidgets.QWidget()
self.MiscTab.setObjectName("MiscTab")
self.gridLayout = QtWidgets.QGridLayout(self.MiscTab)
@@ -395,65 +199,75 @@ class Ui_cloudConfigPageWidget(object):
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
self.uiNameLineEdit = QtWidgets.QLineEdit(self.MiscTab)
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
spacerItem11 = QtWidgets.QSpacerItem(20, 399, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem11, 1, 1, 1, 1)
self.uiNIOsTabWidget.addTab(self.MiscTab, "")
self.vboxlayout.addWidget(self.uiNIOsTabWidget)
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 2, 1, 1)
self.uiDefaultNameFormatLabel = QtWidgets.QLabel(self.MiscTab)
self.uiDefaultNameFormatLabel.setObjectName("uiDefaultNameFormatLabel")
self.gridLayout.addWidget(self.uiDefaultNameFormatLabel, 1, 0, 1, 2)
self.uiDefaultNameFormatLineEdit = QtWidgets.QLineEdit(self.MiscTab)
self.uiDefaultNameFormatLineEdit.setObjectName("uiDefaultNameFormatLineEdit")
self.gridLayout.addWidget(self.uiDefaultNameFormatLineEdit, 1, 2, 1, 1)
self.uiSymbolLabel = QtWidgets.QLabel(self.MiscTab)
self.uiSymbolLabel.setObjectName("uiSymbolLabel")
self.gridLayout.addWidget(self.uiSymbolLabel, 2, 0, 1, 2)
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
self.uiSymbolLineEdit = QtWidgets.QLineEdit(self.MiscTab)
self.uiSymbolLineEdit.setObjectName("uiSymbolLineEdit")
self.horizontalLayout_7.addWidget(self.uiSymbolLineEdit)
self.uiSymbolToolButton = QtWidgets.QToolButton(self.MiscTab)
self.uiSymbolToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
self.uiSymbolToolButton.setObjectName("uiSymbolToolButton")
self.horizontalLayout_7.addWidget(self.uiSymbolToolButton)
self.gridLayout.addLayout(self.horizontalLayout_7, 2, 2, 1, 1)
self.uiCategoryLabel = QtWidgets.QLabel(self.MiscTab)
self.uiCategoryLabel.setObjectName("uiCategoryLabel")
self.gridLayout.addWidget(self.uiCategoryLabel, 3, 0, 1, 2)
self.uiCategoryComboBox = QtWidgets.QComboBox(self.MiscTab)
self.uiCategoryComboBox.setObjectName("uiCategoryComboBox")
self.gridLayout.addWidget(self.uiCategoryComboBox, 3, 2, 1, 1)
spacerItem2 = QtWidgets.QSpacerItem(20, 399, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem2, 4, 1, 1, 2)
self.uiTabWidget.addTab(self.MiscTab, "")
self.verticalLayout.addWidget(self.uiTabWidget)
self.retranslateUi(cloudConfigPageWidget)
self.uiNIOsTabWidget.setCurrentIndex(0)
self.uiTabWidget.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(cloudConfigPageWidget)
def retranslateUi(self, cloudConfigPageWidget):
_translate = QtCore.QCoreApplication.translate
cloudConfigPageWidget.setWindowTitle(_translate("cloudConfigPageWidget", "Cloud configuration"))
self.uiGenericEthernetGroupBox.setTitle(_translate("cloudConfigPageWidget", "Generic Ethernet NIO"))
self.uiAddGenericEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiDeleteGenericEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiLinuxEthernetGroupBox.setTitle(_translate("cloudConfigPageWidget", "Linux Ethernet NIO (Linux only)"))
self.uiAddLinuxEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiDeleteLinuxEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOEthernetTab), _translate("cloudConfigPageWidget", "Ethernet"))
self.uiNIONATSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
self.uiNIONATIdentifierLabel.setText(_translate("cloudConfigPageWidget", "Local identifier:"))
self.uiNIONATListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
self.uiAddNIONATPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiDeleteNIONATPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIONATTab), _translate("cloudConfigPageWidget", "NAT"))
self.uiNIOUDPSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
self.uiLocalPortLabel.setText(_translate("cloudConfigPageWidget", "Local port:"))
self.uiRemoteHostLabel.setText(_translate("cloudConfigPageWidget", "Remote host:"))
cloudConfigPageWidget.setWhatsThis(_translate("cloudConfigPageWidget", "<html><head/><body><p>A cloud node allows you to connect your project to the &quot;real world&quot; (a network or host) using either an Ethernet interface, a TAP interface (Linux only) or even an UDP tunnel. <span style=\" font-weight:600;\">Please be aware that Wifi interfaces may not work properly.</span></p></body></html>"))
self.uiAddEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiAddAllEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
self.uiDeleteEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiEthernetListWidget.setSortingEnabled(True)
self.uiShowSpecialInterfacesCheckBox.setText(_translate("cloudConfigPageWidget", "&Show special Ethernet interfaces"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.EthernetTab), _translate("cloudConfigPageWidget", "Ethernet interfaces"))
self.uiDeleteTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiTAPListWidget.setSortingEnabled(True)
self.uiAddTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiAddAllTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.TAPTab), _translate("cloudConfigPageWidget", "TAP interfaces"))
self.uiUDPTunnelSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "UDP tunnel settings"))
self.uiRemoteHostLineEdit.setText(_translate("cloudConfigPageWidget", "127.0.0.1"))
self.uiRemotePortLabel.setText(_translate("cloudConfigPageWidget", "Remote port:"))
self.uiNIOUDPListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
self.uiAddNIOUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiDeleteNIOUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOUDPTab), _translate("cloudConfigPageWidget", "UDP"))
self.uiNIOTAPGroupBox.setTitle(_translate("cloudConfigPageWidget", "TAP interface"))
self.uiAddNIOTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiDeleteNIOTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOTAPTab), _translate("cloudConfigPageWidget", "TAP"))
self.uiNIOUNIXSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
self.uiLocalFileLabel.setText(_translate("cloudConfigPageWidget", "Local file:"))
self.uiRemoteFileLabel.setText(_translate("cloudConfigPageWidget", "Remote file:"))
self.uiNIOUNIXListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
self.uiAddNIOUNIXPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiDeleteNIOUNIXPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOUnixTab), _translate("cloudConfigPageWidget", "UNIX"))
self.uiNIOVDESettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
self.uiVDEControlFileLabel.setText(_translate("cloudConfigPageWidget", "Control file:"))
self.uiVDELocalFileLabel.setText(_translate("cloudConfigPageWidget", "Local file:"))
self.uiNIOVDEListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
self.uiAddNIOVDEPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiDeleteNIOVDEPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOVDETab), _translate("cloudConfigPageWidget", "VDE"))
self.uiNIONullSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
self.uiNIONullIdentifierLabel.setText(_translate("cloudConfigPageWidget", "Local identifier:"))
self.uiNIONullListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
self.uiAddNIONullPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiDeleteNIONullPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIONullTab), _translate("cloudConfigPageWidget", "NULL"))
self.uiAddUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
self.uiDeleteUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
self.uiUDPNameLineEdit.setText(_translate("cloudConfigPageWidget", "UDP tunnel 1"))
self.uiRemoteHostLabel.setText(_translate("cloudConfigPageWidget", "Remote host:"))
self.uiLocalPortLabel.setText(_translate("cloudConfigPageWidget", "Local port:"))
self.uiUDPNameLabel.setText(_translate("cloudConfigPageWidget", "Name:"))
self.uiUDPTunnelsGroupBox.setTitle(_translate("cloudConfigPageWidget", "UDP tunnels"))
self.uiUDPTreeWidget.headerItem().setText(0, _translate("cloudConfigPageWidget", "Name"))
self.uiUDPTreeWidget.headerItem().setText(1, _translate("cloudConfigPageWidget", "Local port"))
self.uiUDPTreeWidget.headerItem().setText(2, _translate("cloudConfigPageWidget", "Remote host"))
self.uiUDPTreeWidget.headerItem().setText(3, _translate("cloudConfigPageWidget", "Remote port"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.UDPTab), _translate("cloudConfigPageWidget", "UDP tunnels"))
self.uiNameLabel.setText(_translate("cloudConfigPageWidget", "Name:"))
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.MiscTab), _translate("cloudConfigPageWidget", "Misc."))
self.uiDefaultNameFormatLabel.setText(_translate("cloudConfigPageWidget", "Default name format:"))
self.uiSymbolLabel.setText(_translate("cloudConfigPageWidget", "Symbol:"))
self.uiSymbolToolButton.setText(_translate("cloudConfigPageWidget", "&Browse..."))
self.uiCategoryLabel.setText(_translate("cloudConfigPageWidget", "Category:"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.MiscTab), _translate("cloudConfigPageWidget", "Misc."))

View File

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

View File

@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_preferences_page.ui'
#
# Created: Fri Jun 10 14:39:19 2016
# by: PyQt5 UI code generator 5.2.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_CloudPreferencesPageWidget(object):
def setupUi(self, CloudPreferencesPageWidget):
CloudPreferencesPageWidget.setObjectName("CloudPreferencesPageWidget")
CloudPreferencesPageWidget.resize(546, 455)
CloudPreferencesPageWidget.setAccessibleDescription("")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(CloudPreferencesPageWidget)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.splitter = QtWidgets.QSplitter(CloudPreferencesPageWidget)
self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName("splitter")
self.uiCloudNodesTreeWidget = QtWidgets.QTreeWidget(self.splitter)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiCloudNodesTreeWidget.sizePolicy().hasHeightForWidth())
self.uiCloudNodesTreeWidget.setSizePolicy(sizePolicy)
self.uiCloudNodesTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
font = QtGui.QFont()
font.setPointSize(11)
font.setBold(True)
font.setWeight(75)
self.uiCloudNodesTreeWidget.setFont(font)
self.uiCloudNodesTreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.uiCloudNodesTreeWidget.setIconSize(QtCore.QSize(32, 32))
self.uiCloudNodesTreeWidget.setRootIsDecorated(False)
self.uiCloudNodesTreeWidget.setObjectName("uiCloudNodesTreeWidget")
self.uiCloudNodesTreeWidget.headerItem().setText(0, "1")
self.uiCloudNodesTreeWidget.header().setVisible(False)
self.layoutWidget = QtWidgets.QWidget(self.splitter)
self.layoutWidget.setObjectName("layoutWidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.uiCloudNodeInfoTreeWidget = QtWidgets.QTreeWidget(self.layoutWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiCloudNodeInfoTreeWidget.sizePolicy().hasHeightForWidth())
self.uiCloudNodeInfoTreeWidget.setSizePolicy(sizePolicy)
self.uiCloudNodeInfoTreeWidget.setIndentation(10)
self.uiCloudNodeInfoTreeWidget.setAllColumnsShowFocus(True)
self.uiCloudNodeInfoTreeWidget.setObjectName("uiCloudNodeInfoTreeWidget")
self.uiCloudNodeInfoTreeWidget.header().setVisible(False)
self.verticalLayout.addWidget(self.uiCloudNodeInfoTreeWidget)
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.uiNewCloudNodePushButton = QtWidgets.QPushButton(self.layoutWidget)
self.uiNewCloudNodePushButton.setObjectName("uiNewCloudNodePushButton")
self.horizontalLayout_5.addWidget(self.uiNewCloudNodePushButton)
self.uiEditCloudNodePushButton = QtWidgets.QPushButton(self.layoutWidget)
self.uiEditCloudNodePushButton.setEnabled(False)
self.uiEditCloudNodePushButton.setObjectName("uiEditCloudNodePushButton")
self.horizontalLayout_5.addWidget(self.uiEditCloudNodePushButton)
self.uiDeleteCloudNodePushButton = QtWidgets.QPushButton(self.layoutWidget)
self.uiDeleteCloudNodePushButton.setEnabled(False)
self.uiDeleteCloudNodePushButton.setObjectName("uiDeleteCloudNodePushButton")
self.horizontalLayout_5.addWidget(self.uiDeleteCloudNodePushButton)
self.verticalLayout.addLayout(self.horizontalLayout_5)
self.verticalLayout_2.addWidget(self.splitter)
self.retranslateUi(CloudPreferencesPageWidget)
QtCore.QMetaObject.connectSlotsByName(CloudPreferencesPageWidget)
CloudPreferencesPageWidget.setTabOrder(self.uiNewCloudNodePushButton, self.uiDeleteCloudNodePushButton)
def retranslateUi(self, CloudPreferencesPageWidget):
_translate = QtCore.QCoreApplication.translate
CloudPreferencesPageWidget.setWindowTitle(_translate("CloudPreferencesPageWidget", "Cloud nodes"))
CloudPreferencesPageWidget.setAccessibleName(_translate("CloudPreferencesPageWidget", "Cloud node templates"))
self.uiCloudNodeInfoTreeWidget.headerItem().setText(0, _translate("CloudPreferencesPageWidget", "1"))
self.uiCloudNodeInfoTreeWidget.headerItem().setText(1, _translate("CloudPreferencesPageWidget", "2"))
self.uiNewCloudNodePushButton.setText(_translate("CloudPreferencesPageWidget", "&New"))
self.uiEditCloudNodePushButton.setText(_translate("CloudPreferencesPageWidget", "&Edit"))
self.uiDeleteCloudNodePushButton.setText(_translate("CloudPreferencesPageWidget", "&Delete"))

View File

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

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_wizard.ui'
#
# Created: Fri Jun 10 15:32:51 2016
# by: PyQt5 UI code generator 5.2.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_CloudNodeWizard(object):
def setupUi(self, CloudNodeWizard):
CloudNodeWizard.setObjectName("CloudNodeWizard")
CloudNodeWizard.resize(585, 423)
CloudNodeWizard.setModal(True)
self.uiServerWizardPage = QtWidgets.QWizardPage()
self.uiServerWizardPage.setObjectName("uiServerWizardPage")
self.gridLayout_2 = QtWidgets.QGridLayout(self.uiServerWizardPage)
self.gridLayout_2.setObjectName("gridLayout_2")
self.uiServerTypeGroupBox = QtWidgets.QGroupBox(self.uiServerWizardPage)
self.uiServerTypeGroupBox.setObjectName("uiServerTypeGroupBox")
self.verticalLayout = QtWidgets.QVBoxLayout(self.uiServerTypeGroupBox)
self.verticalLayout.setObjectName("verticalLayout")
self.uiRemoteRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
self.uiRemoteRadioButton.setChecked(True)
self.uiRemoteRadioButton.setObjectName("uiRemoteRadioButton")
self.verticalLayout.addWidget(self.uiRemoteRadioButton)
self.uiVMRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
self.uiVMRadioButton.setObjectName("uiVMRadioButton")
self.verticalLayout.addWidget(self.uiVMRadioButton)
self.uiLocalRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
self.uiLocalRadioButton.setObjectName("uiLocalRadioButton")
self.verticalLayout.addWidget(self.uiLocalRadioButton)
self.gridLayout_2.addWidget(self.uiServerTypeGroupBox, 0, 0, 1, 1)
self.uiRemoteServersGroupBox = QtWidgets.QGroupBox(self.uiServerWizardPage)
self.uiRemoteServersGroupBox.setObjectName("uiRemoteServersGroupBox")
self.gridLayout_7 = QtWidgets.QGridLayout(self.uiRemoteServersGroupBox)
self.gridLayout_7.setObjectName("gridLayout_7")
self.uiRemoteServersLabel = QtWidgets.QLabel(self.uiRemoteServersGroupBox)
self.uiRemoteServersLabel.setObjectName("uiRemoteServersLabel")
self.gridLayout_7.addWidget(self.uiRemoteServersLabel, 0, 0, 1, 1)
self.uiRemoteServersComboBox = QtWidgets.QComboBox(self.uiRemoteServersGroupBox)
self.uiRemoteServersComboBox.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiRemoteServersComboBox.sizePolicy().hasHeightForWidth())
self.uiRemoteServersComboBox.setSizePolicy(sizePolicy)
self.uiRemoteServersComboBox.setObjectName("uiRemoteServersComboBox")
self.gridLayout_7.addWidget(self.uiRemoteServersComboBox, 0, 1, 1, 1)
self.gridLayout_2.addWidget(self.uiRemoteServersGroupBox, 1, 0, 1, 1)
CloudNodeWizard.addPage(self.uiServerWizardPage)
self.uiNameWizardPage = QtWidgets.QWizardPage()
self.uiNameWizardPage.setObjectName("uiNameWizardPage")
self.gridLayout = QtWidgets.QGridLayout(self.uiNameWizardPage)
self.gridLayout.setObjectName("gridLayout")
self.uiNameLabel = QtWidgets.QLabel(self.uiNameWizardPage)
self.uiNameLabel.setObjectName("uiNameLabel")
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
self.uiNameLineEdit = QtWidgets.QLineEdit(self.uiNameWizardPage)
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
CloudNodeWizard.addPage(self.uiNameWizardPage)
self.retranslateUi(CloudNodeWizard)
QtCore.QMetaObject.connectSlotsByName(CloudNodeWizard)
def retranslateUi(self, CloudNodeWizard):
_translate = QtCore.QCoreApplication.translate
CloudNodeWizard.setWindowTitle(_translate("CloudNodeWizard", "New cloud node template"))
self.uiServerWizardPage.setTitle(_translate("CloudNodeWizard", "Server"))
self.uiServerWizardPage.setSubTitle(_translate("CloudNodeWizard", "Please choose a server type to run your new cloud node."))
self.uiServerTypeGroupBox.setTitle(_translate("CloudNodeWizard", "Server type"))
self.uiRemoteRadioButton.setText(_translate("CloudNodeWizard", "Run the cloud node on a remote computers"))
self.uiVMRadioButton.setText(_translate("CloudNodeWizard", "Run the cloud node on the GNS3 VM"))
self.uiLocalRadioButton.setText(_translate("CloudNodeWizard", "Run the cloud node on your local computer"))
self.uiRemoteServersGroupBox.setTitle(_translate("CloudNodeWizard", "Remote server"))
self.uiRemoteServersLabel.setText(_translate("CloudNodeWizard", "Run on:"))
self.uiNameWizardPage.setTitle(_translate("CloudNodeWizard", "Name"))
self.uiNameWizardPage.setSubTitle(_translate("CloudNodeWizard", "Please choose a descriptive name for the new cloud node."))
self.uiNameLabel.setText(_translate("CloudNodeWizard", "Name:"))

View File

@@ -6,15 +6,15 @@
<rect>
<x>0</x>
<y>0</y>
<width>381</width>
<height>270</height>
<width>507</width>
<height>352</height>
</rect>
</property>
<property name="windowTitle">
<string>Ethernet hub</string>
</property>
<layout class="QGridLayout">
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="uiSettingsGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@@ -33,17 +33,58 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="uiNameLineEdit"/>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="uiDefaultNameFormatLabel">
<property name="text">
<string>Default name format:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<item row="1" column="2">
<widget class="QLineEdit" name="uiDefaultNameFormatLineEdit"/>
</item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="uiSymbolLabel">
<property name="text">
<string>Symbol:</string>
</property>
</widget>
</item>
<item row="2" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLineEdit" name="uiSymbolLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="uiSymbolToolButton">
<property name="text">
<string>&amp;Browse...</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="uiCategoryLabel">
<property name="text">
<string>Category:</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QComboBox" name="uiCategoryComboBox"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="uiPortsLabel">
<property name="text">
<string>Number of ports:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="4" column="2">
<widget class="QSpinBox" name="uiPortsSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@@ -62,7 +103,7 @@
</property>
</widget>
</item>
<item row="2" column="1">
<item row="5" column="2">
<spacer name="spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -75,6 +116,9 @@
</property>
</spacer>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="uiNameLineEdit"/>
</item>
</layout>
</widget>
</item>

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