mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-06-08 11:33:44 +03:00
Compare commits
196 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
450fbc9af3 | ||
|
|
469ee8fab8 | ||
|
|
6ccfcaf76e | ||
|
|
520e857874 | ||
|
|
012c7b4241 | ||
|
|
1d71cd5bf0 | ||
|
|
d96277882a | ||
|
|
ecec917752 | ||
|
|
ea9c1a8ee1 | ||
|
|
cfbb09fb57 | ||
|
|
dc8aa1fb92 | ||
|
|
786cc8aa65 | ||
|
|
4a353e08e3 | ||
|
|
2a59013604 | ||
|
|
7732aaf9a5 | ||
|
|
1f566a31cf | ||
|
|
10d75e15da | ||
|
|
17def7e00a | ||
|
|
f68a8ea829 | ||
|
|
50066b2f12 | ||
|
|
21a99d4376 | ||
|
|
f97d3041b8 | ||
|
|
31d6a065b0 | ||
|
|
8f077456b1 | ||
|
|
a29f3e35c0 | ||
|
|
fc3781550a | ||
|
|
d285e62c04 | ||
|
|
44d70de687 | ||
|
|
752c516f82 | ||
|
|
e1ec6c5771 | ||
|
|
e8308869d9 | ||
|
|
484c5abe9d | ||
|
|
fe222b873f | ||
|
|
f8bb6661dd | ||
|
|
0f9aab9230 | ||
|
|
a5cf5e16b7 | ||
|
|
097458d108 | ||
|
|
f4cafac9c7 | ||
|
|
7f132fdc36 | ||
|
|
6b7d629755 | ||
|
|
b7ccc37ea5 | ||
|
|
bbe2826c77 | ||
|
|
68e2a0ee39 | ||
|
|
52418ed94a | ||
|
|
a1496bffd4 | ||
|
|
911f6305fa | ||
|
|
c6594d4845 | ||
|
|
538adc4817 | ||
|
|
961c5652ea | ||
|
|
c0ecf3ccc4 | ||
|
|
cf73db25b4 | ||
|
|
dd79939140 | ||
|
|
45b3c17c97 | ||
|
|
1ddf3e6388 | ||
|
|
1c8e166393 | ||
|
|
ec324f9b01 | ||
|
|
cbfd59498e | ||
|
|
0216bc8b4d | ||
|
|
a8477597ab | ||
|
|
e240dbad6b | ||
|
|
8d183a3283 | ||
|
|
3af5046d0f | ||
|
|
c8397a1ef7 | ||
|
|
b419891950 | ||
|
|
2c1ba697bd | ||
|
|
3000a9aa7f | ||
|
|
2f8541c543 | ||
|
|
5c3d4b2ab6 | ||
|
|
f44ac8cba5 | ||
|
|
7ef49fbca7 | ||
|
|
5ccf5778a2 | ||
|
|
6030d5e019 | ||
|
|
6de8880937 | ||
|
|
08c89c4fac | ||
|
|
e411d497c4 | ||
|
|
e037835769 | ||
|
|
a5f4ec0135 | ||
|
|
154f10a686 | ||
|
|
e5320c318f | ||
|
|
07ea6207c1 | ||
|
|
12398881f8 | ||
|
|
27a8e3c7f8 | ||
|
|
d92ff1abe3 | ||
|
|
e97b3b6a42 | ||
|
|
5ee3f73213 | ||
|
|
a30aa2f5f1 | ||
|
|
98bb6590aa | ||
|
|
4250e961a3 | ||
|
|
3c46a3a72d | ||
|
|
c55442a517 | ||
|
|
45e0080726 | ||
|
|
95558ec2e6 | ||
|
|
5b56d54030 | ||
|
|
d5ee1ea5d2 | ||
|
|
69b8c07c0a | ||
|
|
dbe73eb8d7 | ||
|
|
706f89debb | ||
|
|
ec0be9e22b | ||
|
|
0e6fa597ec | ||
|
|
f81450c65a | ||
|
|
38cbe70aaa | ||
|
|
47d335f4c9 | ||
|
|
20d4f73f56 | ||
|
|
5204184029 | ||
|
|
9915beeb8e | ||
|
|
1ea383fce2 | ||
|
|
2744e669b4 | ||
|
|
6ab2d63bdc | ||
|
|
0de6bfe7e1 | ||
|
|
f144103bca | ||
|
|
c0b26aff48 | ||
|
|
9601e4e6f2 | ||
|
|
88708c2a8d | ||
|
|
8eff12194d | ||
|
|
b0520b2bd4 | ||
|
|
17d2c023bf | ||
|
|
ce9fdea0a0 | ||
|
|
24d7dacb4e | ||
|
|
bb36765407 | ||
|
|
250db92ce0 | ||
|
|
d59ec39505 | ||
|
|
5e9ae04dc1 | ||
|
|
ddb0fccda3 | ||
|
|
9b22a52f14 | ||
|
|
948878bfdd | ||
|
|
7340abbaa9 | ||
|
|
4ea0528bf2 | ||
|
|
49005e6add | ||
|
|
5484c039b5 | ||
|
|
daaf71b6d2 | ||
|
|
450f0e006b | ||
|
|
a6a967fbde | ||
|
|
1a6293709e | ||
|
|
2ed53225e0 | ||
|
|
b8798fbda5 | ||
|
|
368de32faa | ||
|
|
98d01cbfa0 | ||
|
|
ad62bb7832 | ||
|
|
637061663a | ||
|
|
c137198985 | ||
|
|
946efb61de | ||
|
|
4c610acfa4 | ||
|
|
37f74824f1 | ||
|
|
5ccf8c414d | ||
|
|
913f0d5e4a | ||
|
|
061bac0cc6 | ||
|
|
ec59cd87bd | ||
|
|
05d9ee8499 | ||
|
|
a72ece5c18 | ||
|
|
63baa2eff0 | ||
|
|
b91fd4a0c2 | ||
|
|
718217e332 | ||
|
|
c202c5e4be | ||
|
|
71830dd69f | ||
|
|
37a7fdfa68 | ||
|
|
0efe006cad | ||
|
|
4a663a5910 | ||
|
|
a559bd4ae4 | ||
|
|
5ebb3011d3 | ||
|
|
81300fd40e | ||
|
|
d4dda2a285 | ||
|
|
5a4342d4b8 | ||
|
|
94fc5e6c4f | ||
|
|
a3e81fbf2e | ||
|
|
514eb97eac | ||
|
|
7637039cb2 | ||
|
|
ac989b191b | ||
|
|
c971cef31b | ||
|
|
c1af2df780 | ||
|
|
eaaa141be9 | ||
|
|
226169cdc6 | ||
|
|
42a4c89f20 | ||
|
|
1482b0e804 | ||
|
|
8ebe3435c4 | ||
|
|
a1cd34d7c4 | ||
|
|
1e4a44135c | ||
|
|
a407f1ec90 | ||
|
|
faab113384 | ||
|
|
c158b7fc46 | ||
|
|
16de9e830f | ||
|
|
25c625c0bb | ||
|
|
bf42d1a355 | ||
|
|
1c0f3493ee | ||
|
|
c3c1f87c5e | ||
|
|
6b80914385 | ||
|
|
a114d9ace7 | ||
|
|
4dca4d057a | ||
|
|
17af21e29a | ||
|
|
7fbce0266d | ||
|
|
d5cdbdbf90 | ||
|
|
e5a790f4b2 | ||
|
|
f3769df0d6 | ||
|
|
a21db74941 | ||
|
|
d1e1f6dfb6 | ||
|
|
cc45c9631a | ||
|
|
d16a52e389 |
@@ -11,6 +11,8 @@ before_deploy:
|
||||
- sudo pip install urllib3[secure]
|
||||
deploy:
|
||||
provider: pypi
|
||||
edge:
|
||||
branch: v1.8.45
|
||||
user: noplay
|
||||
password:
|
||||
secure: FofcqlJjgqf2jaDaXpLHeigVoexbrOz3WwnDuiJpwJxeFUlPY8s2cQs/Bm+dzxzZaOaGiVE0A83v/Xa10yD5tflThHt4sqYJK3iQCinA7wgeAlDimB4xrWUNplfNJZ/Eod5Ssa++E02W+3i29PxpXY//mjCY7qDxaoxul1gnFJY=
|
||||
|
||||
173
CHANGELOG
173
CHANGELOG
@@ -1,5 +1,178 @@
|
||||
# Change Log
|
||||
|
||||
## 2.1.20 29/05/2019
|
||||
|
||||
* Fix KeyError: 'endpoint' issue. Fixes #2802
|
||||
|
||||
## 2.1.19 28/05/2019
|
||||
|
||||
* Fix wrong aligment of symbols in saved/exported projects. Fixes #2800
|
||||
* Replace urllib.request by Qt implementation for local server synchronous check. Fixes #2793
|
||||
* Set grid's minimum to 5. Fixes #2795
|
||||
|
||||
## 2.1.18 22/05/2019
|
||||
|
||||
* Fix error in HTTPConnection.request for Python3.6. Fixes #2793
|
||||
* Catch more OSError/PermissionError when checking md5 on remote images. Fixes #2582
|
||||
* Fix exception when grid size is 0. Fixes #2790
|
||||
* Catch PermissionError when scanning local image directories. Fixes #2791
|
||||
* Revert "Make sure the latest PyQt5 version 5.12.x is used on Windows." Ref #2778
|
||||
|
||||
## 2.1.17 17/05/2019
|
||||
|
||||
* No changes.
|
||||
|
||||
## 2.1.16 15/04/2019
|
||||
|
||||
* Do not make NPF or NPCAP service mandatory to start the local server on Windows.
|
||||
* Fix OverflowError error with progress dialog. Fixes #2767
|
||||
* More fixes for stuck progress window. Fixes #2765
|
||||
* Fix adding multiple devices - stuck progress window. Fixes #2765
|
||||
* Make sure the latest PyQt5 version 5.12.x is used on Windows.
|
||||
* Show a warning when a config export is not supported. Ref #2762
|
||||
|
||||
## 2.1.15 21/03/2019
|
||||
|
||||
* No changes on the GUI.
|
||||
|
||||
## 2.1.14 27/02/2019
|
||||
|
||||
* Better description to why an appliance cannot be installed.
|
||||
|
||||
## 2.1.13 26/02/2019
|
||||
|
||||
* Disable computer hibernation detection mechanism. Ref #2678
|
||||
* Add some advice for request timeout message. Fixes #2652
|
||||
* Show/Hide interface labels when status points are not shown. Fixes #2690
|
||||
* Do not print critical message twice on stderr. Replace QMessageBox calls with no parent by log.error()/log.warning().
|
||||
* Show critical messages before the main window runs.
|
||||
* Avoid using PyQt5.Qt, which imports unneeded stuff. Fixes #2592
|
||||
* Fix SIP import error with recent PyQt versions. Fixes #2709
|
||||
* Upgrade to Qt 5.12. Fixes #2636
|
||||
* Adjust the setup wizard (VMware image size, layouts).
|
||||
|
||||
## 2.1.12 23/01/2019
|
||||
|
||||
* Option to resize SVG symbols that are too big (height above 80px, activated by default). Ref #2674.
|
||||
* Update VMware banners and links.
|
||||
* Allow users to refresh the template list in the nodes view panel.
|
||||
* Fix Dynamips decompress doesn't work with relative images. Fixes #2648.
|
||||
* Update download URL for "Check For Update".
|
||||
|
||||
## 2.1.11 28/09/2018
|
||||
|
||||
* Handle deleted SIP objects.
|
||||
* Update paths for UltraVNC and VirtViewer.
|
||||
* Indicate if Solar-PuTTY is included or not. Fixes #2595
|
||||
* Fix bad link to installation instructions in README.rst. Fixes #2590
|
||||
* Downgrade to Qt 5.9. Fixes #2592.
|
||||
|
||||
## 2.1.10 15/09/2018
|
||||
|
||||
* Fix small errors like unhandled exceptions etc.
|
||||
* Fix when appliance version is not available for Dynamips/IOU/Qemu. Fixes #2585.
|
||||
* Fix issue when installing appliance with no version selected. Fixes #2585.
|
||||
* Check for existing appliance name across all emulator types. Fixes #2584.
|
||||
* Improve the invalid port format detection. Fixes https://github.com/GNS3/gns3-gui/issues/2580
|
||||
* Catch OSError/PermissionError when checking md5 on remote image. Fixes #2582.
|
||||
* Fix UnicodeDecodeError in file editor. Fixes #2581.
|
||||
* Catch import error for win32serviceutil. Fixes #2583.
|
||||
* Fix bug with empty project ID when creating a new node. Fixes #2366
|
||||
* Fix various small errors, mostly about non-existing C/C++ objects.
|
||||
* Send extra controller and compute information in crash reports.
|
||||
* Update setup.py and fix minor issues.
|
||||
* Set the default delay console all value to 1500ms if using Solar-PuTTY.
|
||||
* Make Solar-Putty the default if installed. Ref #2519.
|
||||
* Fix issue with custom appliance. Fixes https://github.com/GNS3/gns3-registry/issues/361
|
||||
* Forbid controller and compute servers to be different versions. Report last compute server error to clients and display in the server summary.
|
||||
* Fix issue with appliance categories. Fixes https://github.com/GNS3/gns3-registry/issues/361
|
||||
* Add compute information to crash reports.
|
||||
* Add controller version in Sentry bug reports.
|
||||
* Backport: Fix "Network session error" issues. Fixes #2560.
|
||||
* Add SolarPutty command line. Fixes #2519.
|
||||
* Add missing Qemu boot priority values. Fixes https://github.com/GNS3/gns3-server/issues/1385
|
||||
* Update PyQt5 from version 5.8 to version 5.10. Fixes #2564.
|
||||
|
||||
## 2.1.9 13/08/2018
|
||||
|
||||
* Fix incorrect short port names in topology summary. Fixes https://github.com/GNS3/gns3-gui/issues/2562
|
||||
* Add compute version in server summary tooltip.
|
||||
* Fix test for Qemu boot priority. Fixes #2548.
|
||||
* Fix boot priority missing when installing an appliance. Fixes #2548.
|
||||
* Support PATH with UTF-8 characters in OSX telnet console, fixes #2537
|
||||
* Allow users to accept different MD5 hashes for preconfigured appliances. Fixes #2526.
|
||||
* Do not try to update drawing if it is being deleted. Ref #2483.
|
||||
* Catch exception when loading invalid appliance file.
|
||||
|
||||
## 2.1.8 14/06/2018
|
||||
|
||||
* Add error information when cannot access/read IOS/IOU config file. Ref #2501
|
||||
* Fallback when using process name to bring console to front.
|
||||
* Use process name to bring console to front. Fixes #2514.
|
||||
|
||||
## 2.1.7 12/06/2018
|
||||
|
||||
* Do not try to update link if it is being deleted. Fixes #2483.
|
||||
* Fix can't add SVG image to project. Fixes #2502
|
||||
* Remove unwanted trailing characters and other white spaces when reading .md5sum files. Fixes #2498.
|
||||
* Update interface sequence number check. Fixes #2491.
|
||||
* Logo should not have context menu, Fixes: #2507
|
||||
* Update logo position only when changes, Fixes: #2506
|
||||
|
||||
## 2.1.6 22/05/2018
|
||||
|
||||
* Ask for global variables when project is loaded
|
||||
* Add/Edit global variables of project
|
||||
* Rename tabs at Edit Project
|
||||
* Global variables tab on Edit project
|
||||
* Support of supplier logo and url
|
||||
* Add missing crowdfunder name in About dialog.
|
||||
* Project variables and supplier
|
||||
* No timeout when duplicating a project.
|
||||
* No timeout when restoring snapshot.
|
||||
* Add advanced settings for docker and ExtraHosts param, Ref. #2482
|
||||
* Replace "not supported" by "none" in topology summary view.
|
||||
|
||||
## 2.1.5 18/04/2018
|
||||
|
||||
* Fix Qemu binary list locks when a version is deleted. Fixes #2474.
|
||||
* Fix invalid answer from the PyPi server. Fixes #2473.
|
||||
* Fix wrong wizard page name.
|
||||
* Grid size support for projects. Fixes #2469.
|
||||
* Remove 'include INSTALL' from MANIFEST. Fixes #2470.
|
||||
* Check for valid IP address and prevent to run on non-Windows platforms.
|
||||
|
||||
## 2.1.4 12/03/2018
|
||||
|
||||
* Update node on server on any change, Fixes: #2429
|
||||
* Mark IOU layer 1 keepalive messages feature as non-functional. Fixes #2431.
|
||||
* Images refresh when added via settings, Fixes:#2423
|
||||
* Emit project_loaded_signal after project creation
|
||||
* Add option Show interface labels on new project, Ref. #2308
|
||||
* Improve finding pyuic3.exe on Windows
|
||||
* Use debug for error downloading file messages. Fixes #2398.
|
||||
* Refresh buttons in the cloud node to query the server for available interfaces. Fixes #2416.
|
||||
* Handle Certifacte Error, Ref. gns3-server#1262
|
||||
* Backward compatibility for tests, Ref. #2405?
|
||||
* Use UTF-8 for IOURC file migration.
|
||||
* Look for symbols on controller, Ref. #2405
|
||||
* Display an error message if Telnet console program cannot be executed.
|
||||
|
||||
## 2.1.3 19/01/2018
|
||||
|
||||
* Change messages when there are different client and server versions. Fixes #2391.
|
||||
* Fix "Transport selection via DSN is deprecated" message. Sync is configured with HTTPTransport.
|
||||
* Refresh CPU/RAM info every 1 second. Ref #2262.
|
||||
* Only check for AVG on Windows
|
||||
* Improve the search for VBoxManage.
|
||||
* Allow telnet console to node with name containing double quotes. Fixes #2371.
|
||||
|
||||
## 2.1.2 08/01/2018
|
||||
|
||||
* Update VMware promotion in setup wizard.
|
||||
* Confirm exit. Fixes #2359.
|
||||
* Fix with .exe build
|
||||
|
||||
## 2.1.1 22/12/2017
|
||||
|
||||
* Fix dragging appliance into topology from nodes window, fixes: #2363
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Run tests inside a container
|
||||
FROM ubuntu:yakkety
|
||||
FROM ubuntu:17.10
|
||||
|
||||
MAINTAINER GNS3 Team
|
||||
|
||||
#ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --force-yes python3.5 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.5-dev xvfb
|
||||
RUN apt-get install -y --force-yes python3.6 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.6-dev xvfb
|
||||
RUN apt-get clean
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ ADD . /src
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
CMD xvfb-run python3.5 -m pytest -vv
|
||||
CMD xvfb-run python3.6 -m pytest -vv
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
include README.rst
|
||||
include AUTHORS
|
||||
include INSTALL
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
|
||||
@@ -13,7 +13,7 @@ GNS3 GUI repository.
|
||||
Installation
|
||||
------------
|
||||
|
||||
https://gns3.com/support/docs
|
||||
Please see https://docs.gns3.com/
|
||||
|
||||
Development
|
||||
-------------
|
||||
|
||||
@@ -63,14 +63,14 @@ class ApplianceManager(QtCore.QObject):
|
||||
|
||||
def _listAppliancesCallback(self, result, error=False, **kwargs):
|
||||
if error is True:
|
||||
log.error("Error while getting appliances list: {}".format(result["message"]))
|
||||
log.error("Error while getting appliances list: {}".format(result.get("message", "unknown")))
|
||||
return
|
||||
self._appliances = result
|
||||
self.appliances_changed_signal.emit()
|
||||
|
||||
def _listApplianceTemplateCallback(self, result, error=False, **kwargs):
|
||||
if error is True:
|
||||
log.error("Error while getting appliance templates list: {}".format(result["message"]))
|
||||
log.error("Error while getting appliance templates list: {}".format(result.get("message", "unknown")))
|
||||
return
|
||||
self._appliance_templates = result
|
||||
self.appliances_changed_signal.emit()
|
||||
@@ -80,8 +80,15 @@ class ApplianceManager(QtCore.QObject):
|
||||
if appliance["appliance_id"] == appliance_id:
|
||||
break
|
||||
|
||||
project_id = project.id()
|
||||
if not self._controller.connected():
|
||||
log.error("Cannot create node: not connected to any controller server")
|
||||
return
|
||||
|
||||
if not project or not project.id():
|
||||
log.error("Cannot create node: please create a project first!")
|
||||
return
|
||||
|
||||
project_id = project.id()
|
||||
if appliance.get("compute_id") is None:
|
||||
from .main_window import MainWindow
|
||||
server = server_select(MainWindow.instance(), node_type=appliance["node_type"])
|
||||
@@ -92,12 +99,14 @@ class ApplianceManager(QtCore.QObject):
|
||||
"x": int(x),
|
||||
"y": int(y)
|
||||
},
|
||||
showProgress=False,
|
||||
timeout=None)
|
||||
else:
|
||||
self._controller.post("/projects/" + project_id + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
|
||||
"x": int(x),
|
||||
"y": int(y)
|
||||
},
|
||||
showProgress=False,
|
||||
timeout=None)
|
||||
return True
|
||||
|
||||
|
||||
@@ -336,13 +336,16 @@ class BaseNode(QtCore.QObject):
|
||||
"""
|
||||
|
||||
if not hasattr(self, "configFiles"):
|
||||
return
|
||||
return False
|
||||
|
||||
for file in self.configFiles():
|
||||
self.controllerHttpGet("/nodes/{node_id}/files/{file}".format(node_id=self._node_id, file=file),
|
||||
self._exportConfigToDirectoryCallback,
|
||||
context={"directory": directory, "file": file},
|
||||
raw=True)
|
||||
|
||||
return True
|
||||
|
||||
def _exportConfigToDirectoryCallback(self, result, error=False, raw_body=None, context={}, **kwargs):
|
||||
"""
|
||||
Callback for exportConfigToDirectory.
|
||||
@@ -352,8 +355,7 @@ class BaseNode(QtCore.QObject):
|
||||
"""
|
||||
|
||||
if error:
|
||||
# The file could be missing if you have not private config for
|
||||
# exemple
|
||||
# The file could be missing if you have not private config for example
|
||||
return
|
||||
export_directory = context["directory"]
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ class Compute:
|
||||
self._password = None
|
||||
self._cpu_usage_percent = None
|
||||
self._memory_usage_percent = None
|
||||
self._last_error = None
|
||||
self._capabilities = {
|
||||
"node_types": []
|
||||
}
|
||||
@@ -97,6 +98,12 @@ class Compute:
|
||||
def capabilities(self):
|
||||
return self._capabilities
|
||||
|
||||
def setLastError(self, last_error):
|
||||
self._last_error = last_error
|
||||
|
||||
def lastError(self):
|
||||
return self._last_error
|
||||
|
||||
def setCapabilities(self, val):
|
||||
self._capabilities = val
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class ComputeManager(QtCore.QObject):
|
||||
def _refreshComputesSlot(self):
|
||||
if self._refreshingComputes:
|
||||
return
|
||||
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 5:
|
||||
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 1:
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
self._refreshingComputes = True
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
|
||||
@@ -84,6 +84,7 @@ class ComputeManager(QtCore.QObject):
|
||||
Called when we received data from a compute
|
||||
node.
|
||||
"""
|
||||
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
|
||||
new_node = False
|
||||
@@ -101,6 +102,7 @@ class ComputeManager(QtCore.QObject):
|
||||
self._computes[compute_id].setCpuUsagePercent(compute["cpu_usage_percent"])
|
||||
self._computes[compute_id].setMemoryUsagePercent(compute["memory_usage_percent"])
|
||||
self._computes[compute_id].setCapabilities(compute["capabilities"])
|
||||
self._computes[compute_id].setLastError(compute.get("last_error"))
|
||||
|
||||
if new_node:
|
||||
self.created_signal.emit(compute_id)
|
||||
|
||||
@@ -62,19 +62,26 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
|
||||
text = "{} CPU {}%, RAM {}%".format(text, self._compute.cpuUsagePercent(), self._compute.memoryUsagePercent())
|
||||
|
||||
self.setText(0, text)
|
||||
self.setToolTip(0, text + " on " + self._compute.capabilities().get("platform", ""))
|
||||
|
||||
if self._compute.connected():
|
||||
self._status = "connected"
|
||||
self.setToolTip(0, "Server {} version {} running on {}".format(self._compute.name(),
|
||||
self._compute.capabilities().get("version", "n/a"),
|
||||
self._compute.capabilities().get("platform", "")))
|
||||
if usage is None or (self._compute.cpuUsagePercent() < 90 and self._compute.memoryUsagePercent() < 90):
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
else:
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_yellow.svg'))
|
||||
else:
|
||||
if self._status == "unknown":
|
||||
last_error = self._compute.lastError()
|
||||
if last_error:
|
||||
self.setToolTip(0, "Failed to connect to {}: {}".format(self._compute.name(), last_error))
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
elif self._status == "unknown":
|
||||
self.setToolTip(0, "Discovering or connecting to {}...".format(self._compute.name()))
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_gray.svg'))
|
||||
else:
|
||||
self._status = "stopped"
|
||||
self.setToolTip(0, "{} is stopped or cannot be reached".format(self._compute.name()))
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
self._parent.sortItems(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Handles commands typed in the GNS3 console.
|
||||
import sys
|
||||
import cmd
|
||||
import struct
|
||||
import sip
|
||||
from .qt import sip
|
||||
import json
|
||||
|
||||
from .node import Node
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import sip
|
||||
from .qt import sip
|
||||
import struct
|
||||
import inspect
|
||||
import datetime
|
||||
import platform
|
||||
|
||||
from .qt import QtCore, Qt
|
||||
from .qt import QtCore
|
||||
from .topology import Topology
|
||||
from .version import __version__
|
||||
from .console_cmd import ConsoleCmd
|
||||
@@ -75,7 +75,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {} and PyQt {}.\n" \
|
||||
"Copyright (c) 2006-{} GNS3 Technologies.\n" \
|
||||
"Use Help -> GNS3 Doctor to detect common issues." \
|
||||
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, Qt.PYQT_VERSION_STR, current_year)
|
||||
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, QtCore.PYQT_VERSION_STR, current_year)
|
||||
|
||||
# Parent class initialization
|
||||
try:
|
||||
|
||||
@@ -41,6 +41,7 @@ class Controller(QtCore.QObject):
|
||||
super().__init__()
|
||||
self._connected = False
|
||||
self._connecting = False
|
||||
self._version = None
|
||||
self._cache_directory = tempfile.mkdtemp()
|
||||
self._http_client = None
|
||||
# If it's the first error we display an alert box to the user
|
||||
@@ -55,6 +56,9 @@ class Controller(QtCore.QObject):
|
||||
def host(self):
|
||||
return self._http_client.host()
|
||||
|
||||
def version(self):
|
||||
return self._version
|
||||
|
||||
def isRemote(self):
|
||||
"""
|
||||
:returns Boolean: True if the controller is remote
|
||||
@@ -121,7 +125,7 @@ class Controller(QtCore.QObject):
|
||||
|
||||
def _versionGetSlot(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Called after the inital version get
|
||||
Called after the initial version get
|
||||
"""
|
||||
if error:
|
||||
if self._first_error:
|
||||
@@ -142,6 +146,7 @@ class Controller(QtCore.QObject):
|
||||
if self._error_dialog:
|
||||
self._error_dialog.reject()
|
||||
self._error_dialog = None
|
||||
self._version = result.get("version")
|
||||
|
||||
def _httpClientConnectedSlot(self):
|
||||
if not self._connected:
|
||||
@@ -208,9 +213,6 @@ class Controller(QtCore.QObject):
|
||||
if self._http_client:
|
||||
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
return self._http_client.getSynchronous(endpoint, timeout)
|
||||
|
||||
def connectWebSocket(self, path, *args):
|
||||
return self._http_client.connectWebSocket(path)
|
||||
|
||||
@@ -237,13 +239,9 @@ class Controller(QtCore.QObject):
|
||||
if not self._http_client:
|
||||
return
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(url.encode())
|
||||
if ".svg" in url:
|
||||
extension = ".svg"
|
||||
else:
|
||||
extension = ".png"
|
||||
path = os.path.join(self._cache_directory, m.hexdigest() + extension)
|
||||
|
||||
path = self.getStaticCachedPath(url)
|
||||
|
||||
if os.path.exists(path):
|
||||
callback(path)
|
||||
elif path in self._static_asset_download_queue:
|
||||
@@ -263,8 +261,7 @@ class Controller(QtCore.QObject):
|
||||
self.getStatic(fallback, callback)
|
||||
fallback_used = True
|
||||
if fallback_used:
|
||||
log.error("Error while downloading file: {}".format(url))
|
||||
log.error("Error while downloading file: {}".format(url))
|
||||
log.debug("Error while downloading file: {}".format(url))
|
||||
del self._static_asset_download_queue[path]
|
||||
return
|
||||
try:
|
||||
@@ -278,6 +275,21 @@ class Controller(QtCore.QObject):
|
||||
callback(path)
|
||||
del self._static_asset_download_queue[path]
|
||||
|
||||
def getStaticCachedPath(self, url):
|
||||
"""
|
||||
Returns static cached (hashed) path
|
||||
:param url:
|
||||
:return:
|
||||
"""
|
||||
m = hashlib.md5()
|
||||
m.update(url.encode())
|
||||
if ".svg" in url:
|
||||
extension = ".svg"
|
||||
else:
|
||||
extension = ".png"
|
||||
path = os.path.join(self._cache_directory, m.hexdigest() + extension)
|
||||
return path
|
||||
|
||||
def getSymbolIcon(self, symbol_id, callback, fallback=None):
|
||||
"""
|
||||
Get a QIcon for a symbol from the controller
|
||||
@@ -298,6 +310,9 @@ class Controller(QtCore.QObject):
|
||||
icon.addFile(path)
|
||||
callback(icon)
|
||||
|
||||
def getSymbols(self, callback):
|
||||
self.get('/symbols', callback=callback)
|
||||
|
||||
def deleteProject(self, project_id, callback=None):
|
||||
Controller.instance().delete("/projects/{}".format(project_id), qpartial(self._deleteProjectCallback, callback=callback, project_id=project_id))
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "sync+https://c66a939550054db9a330a82c26a2e47a:ffd37ed644064b638526f48d81b2201e@sentry.io/38506"
|
||||
DSN = "https://84ef3f39811242728d4b151f4d5ca5a4:eb9df95eb15f4672a4abb17ddb8322fe@sentry.io/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
@@ -71,6 +71,8 @@ class CrashReport:
|
||||
def captureException(self, exception, value, tb):
|
||||
from .local_server import LocalServer
|
||||
from .local_config import LocalConfig
|
||||
from .controller import Controller
|
||||
from .compute_manager import ComputeManager
|
||||
|
||||
local_server = LocalServer.instance().localServerSettings()
|
||||
if local_server["report_errors"]:
|
||||
@@ -102,10 +104,24 @@ class CrashReport:
|
||||
sys.version_info[2]),
|
||||
"python:bit": struct.calcsize("P") * 8,
|
||||
"python:encoding": sys.getdefaultencoding(),
|
||||
"python:frozen": "{}".format(hasattr(sys, "frozen"))
|
||||
"python:frozen": "{}".format(hasattr(sys, "frozen")),
|
||||
}
|
||||
|
||||
# extra controller and compute information
|
||||
extra_context = {"controller:version": Controller.instance().version(),
|
||||
"controller:host": Controller.instance().host(),
|
||||
"controller:connected": Controller.instance().connected()}
|
||||
for index, compute in enumerate(ComputeManager.instance().computes()):
|
||||
extra_context["compute{}:id".format(index)] = compute.id()
|
||||
extra_context["compute{}:name".format(index)] = compute.name(),
|
||||
extra_context["compute{}:host".format(index)] = compute.host(),
|
||||
extra_context["compute{}:connected".format(index)] = compute.connected()
|
||||
extra_context["compute{}:platform".format(index)] = compute.capabilities().get("platform")
|
||||
extra_context["compute{}:version".format(index)] = compute.capabilities().get("version")
|
||||
|
||||
context = self._add_qt_information(context)
|
||||
client.tags_context(context)
|
||||
client.extra_context(extra_context)
|
||||
try:
|
||||
report = client.captureException((exception, value, tb))
|
||||
except Exception as e:
|
||||
@@ -116,7 +132,7 @@ class CrashReport:
|
||||
def _add_qt_information(self, context):
|
||||
try:
|
||||
from .qt import QtCore
|
||||
import sip
|
||||
from .qt import sip
|
||||
except ImportError:
|
||||
return context
|
||||
context["psutil:version"] = psutil.__version__
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sip
|
||||
from ..qt import sip
|
||||
import shutil
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
|
||||
@@ -34,6 +34,9 @@ from ..controller import Controller
|
||||
from ..local_config import LocalConfig
|
||||
from ..image_upload_manager import ImageUploadManager
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
images_changed_signal = QtCore.Signal()
|
||||
@@ -41,6 +44,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
def __init__(self, parent, path):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
self.images_changed_signal.connect(self._refreshVersions)
|
||||
self.versions_changed_signal.connect(self._versionRefreshedSlot)
|
||||
@@ -76,16 +80,27 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiLocalRadioButton.setText("Install the appliance on the main server")
|
||||
else:
|
||||
if not path.endswith('.builtin.gns3a'):
|
||||
destination = Config().appliances_dir
|
||||
destination = None
|
||||
try:
|
||||
os.makedirs(destination, exist_ok=True)
|
||||
destination = os.path.join(destination, os.path.basename(path))
|
||||
shutil.copy(path, destination)
|
||||
destination = Config().appliances_dir
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Can't copy {} to {}".format(path, destination), str(e))
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", "Could not find configuration file: {}".format(e))
|
||||
except ValueError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", "Invalid configuration file: {}".format(e))
|
||||
if destination:
|
||||
try:
|
||||
os.makedirs(destination, exist_ok=True)
|
||||
destination = os.path.join(destination, os.path.basename(path))
|
||||
shutil.copy(path, destination)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Cannot copy {} to {}".format(path, destination), str(e))
|
||||
|
||||
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
|
||||
|
||||
# symbols loaded from controller
|
||||
self._symbols = []
|
||||
|
||||
|
||||
def initializePage(self, page_id):
|
||||
"""
|
||||
Initialize Wizard pages.
|
||||
@@ -110,6 +125,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
type = "dynamips"
|
||||
|
||||
if self.page(page_id) == self.uiInfoWizardPage:
|
||||
Controller.instance().getSymbols(self._getSymbolsCallback)
|
||||
|
||||
self.uiInfoWizardPage.setTitle(self._appliance["product_name"])
|
||||
self.uiDescriptionLabel.setText(self._appliance["description"])
|
||||
|
||||
@@ -316,9 +333,15 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(1)
|
||||
self._refreshing = False
|
||||
|
||||
def _getSymbolsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.warning("Cannot load symbols from controller")
|
||||
else:
|
||||
self._symbols = result
|
||||
|
||||
def _refreshDialogWorker(self):
|
||||
"""
|
||||
Scan local directory in order to found the images on disk
|
||||
Scan local directory in order to find the images on disk
|
||||
"""
|
||||
|
||||
# Docker do not have versions
|
||||
@@ -415,15 +438,16 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
image = Image(self._appliance.emulator(), path, filename=disk["filename"])
|
||||
try:
|
||||
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct file. The MD5 sum is {} and should be {}.".format(image.md5sum, disk["md5sum"]))
|
||||
return
|
||||
reply = QtWidgets.QMessageBox.question(self, "Add appliance",
|
||||
"This is not the correct file. The MD5 sum is {} and should be {}.\nDo you want to accept it at your own risks?".format(image.md5sum, disk["md5sum"]),
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
|
||||
return
|
||||
|
||||
image_upload_manger = ImageUploadManager(
|
||||
image, Controller.instance(), self._compute_id,
|
||||
self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
|
||||
image_upload_manger = ImageUploadManager(image, Controller.instance(), self._compute_id, self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
|
||||
image_upload_manger.upload()
|
||||
|
||||
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
|
||||
@@ -459,12 +483,15 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
try:
|
||||
config = Config()
|
||||
except OSError as e:
|
||||
except (OSError, ValueError) as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
return False
|
||||
|
||||
if version is None:
|
||||
appliance_configuration = self._appliance.copy()
|
||||
if not "docker" in appliance_configuration:
|
||||
# only Docker do not have version
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
@@ -482,7 +509,10 @@ 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, self._compute_id), allowed_exceptions=[ConfigException, OSError])
|
||||
worker = WaitForLambdaWorker(
|
||||
lambda: config.add_appliance(appliance_configuration, self._compute_id, self._symbols),
|
||||
allowed_exceptions=[ConfigException, OSError])
|
||||
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if not progress_dialog.exec_():
|
||||
@@ -541,8 +571,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if version is None:
|
||||
return False
|
||||
appliance = current.data(2, QtCore.Qt.UserRole)
|
||||
if not self._appliance.is_version_installable(version["name"]):
|
||||
QtWidgets.QMessageBox.warning(self, "Appliance", "Sorry, you cannot install {} with missing files".format(appliance["name"]))
|
||||
try:
|
||||
self._appliance.search_images_for_version(version["name"])
|
||||
except ApplianceError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Appliance", "Cannot install {} version {}: {}".format(appliance["name"], version["name"], e))
|
||||
return False
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {} version {}?".format(appliance["name"], version["name"]),
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
@@ -552,7 +584,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
elif self.currentPage() == self.uiUsageWizardPage:
|
||||
if self._image_uploading_count > 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for image uploading")
|
||||
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for all images to be uploaded...")
|
||||
return False
|
||||
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
|
||||
@@ -95,13 +95,14 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
def checkAVGInstalled(self):
|
||||
"""Checking if AVG software is not installed"""
|
||||
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(["exe"])
|
||||
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
|
||||
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
if sys.platform.startswith("win32"):
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(["exe"])
|
||||
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
|
||||
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
return (0, None)
|
||||
|
||||
def checkFreeRam(self):
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtWidgets
|
||||
from ..qt import QtWidgets, QtCore, qslot, qpartial
|
||||
from ..topology import Topology
|
||||
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog
|
||||
|
||||
@@ -36,6 +36,68 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
self.uiProjectAutoStartCheckBox.setChecked(self._project.autoStart())
|
||||
self.uiSceneWidthSpinBox.setValue(self._project.sceneWidth())
|
||||
self.uiSceneHeightSpinBox.setValue(self._project.sceneHeight())
|
||||
self.uiGridSizeSpinBox.setValue(self._project.gridSize())
|
||||
|
||||
self.uiGlobalVariablesGrid.setAlignment(QtCore.Qt.AlignTop)
|
||||
|
||||
self.uiNewVarButton = QtWidgets.QPushButton('Add new variable', self)
|
||||
self.uiNewVarButton.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
self.uiNewVarButton.clicked.connect(self.onAddNewVariable)
|
||||
self.uiGlobalVariablesGrid.addWidget(self.uiNewVarButton, 0, 3, QtCore.Qt.AlignRight)
|
||||
|
||||
self._variables = self.setUpVariables()
|
||||
self.updateGlobalVariables()
|
||||
|
||||
def setUpVariables(self):
|
||||
new_variable = {"name": "", "value": ""}
|
||||
variables = self._project.variables()
|
||||
|
||||
if variables is not None:
|
||||
variables.append(new_variable)
|
||||
else:
|
||||
variables = [new_variable]
|
||||
return variables
|
||||
|
||||
def updateGlobalVariables(self):
|
||||
while True:
|
||||
item = self.uiGlobalVariablesGrid.takeAt(1)
|
||||
if item is None:
|
||||
break
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
for i, variable in enumerate(self._variables, start=1):
|
||||
nameLabel = QtWidgets.QLabel()
|
||||
nameLabel.setText("Name:")
|
||||
self.uiGlobalVariablesGrid.addWidget(nameLabel, i, 0)
|
||||
|
||||
nameEdit = QtWidgets.QLineEdit()
|
||||
nameEdit.setText(variable.get("name", ""))
|
||||
nameEdit.textChanged.connect(qpartial(self.onNameChange, variable))
|
||||
self.uiGlobalVariablesGrid.addWidget(nameEdit, i, 1)
|
||||
|
||||
valueLabel = QtWidgets.QLabel()
|
||||
valueLabel.setText("Value:")
|
||||
self.uiGlobalVariablesGrid.addWidget(valueLabel, i, 2)
|
||||
|
||||
valueEdit = QtWidgets.QLineEdit()
|
||||
valueEdit.setText(variable.get("value", ""))
|
||||
valueEdit.textChanged.connect(qpartial(self.onValueChange, variable))
|
||||
self.uiGlobalVariablesGrid.addWidget(valueEdit, i, 3)
|
||||
|
||||
@qslot
|
||||
def onAddNewVariable(self, event):
|
||||
self._variables += [{"name": "", "value": ""}]
|
||||
self.updateGlobalVariables()
|
||||
|
||||
def onNameChange(self, variable, text):
|
||||
variable["name"] = text
|
||||
|
||||
def onValueChange(self, variable, text):
|
||||
variable["value"] = text
|
||||
|
||||
def _cleanVariables(self):
|
||||
return [v for v in self._variables if v.get("name", "").strip() != ""]
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
@@ -51,5 +113,7 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
|
||||
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
|
||||
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
|
||||
self._project.setGridSize(self.uiGridSizeSpinBox.value())
|
||||
self._project.setVariables(self._cleanVariables())
|
||||
self._project.update()
|
||||
super().done(result)
|
||||
|
||||
@@ -66,7 +66,7 @@ class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
|
||||
|
||||
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
|
||||
if not error:
|
||||
self.uiFileTextEdit.setText(raw_body.decode("utf-8"))
|
||||
self.uiFileTextEdit.setText(raw_body.decode("utf-8", errors="ignore"))
|
||||
elif result.get("status") == 404:
|
||||
if self._default:
|
||||
self.uiFileTextEdit.setText(self._default)
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
from ..qt import QtGui, QtWidgets, qslot
|
||||
from ..ui.filter_dialog_ui import Ui_FilterDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
|
||||
|
||||
@@ -30,6 +33,7 @@ class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._link = link
|
||||
self._filters = {}
|
||||
self._link.updated_link_signal.connect(self._updateUiSlot)
|
||||
self._link.listAvailableFilters(self._listAvailableFiltersCallback)
|
||||
self._initialized = False
|
||||
@@ -40,7 +44,7 @@ class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
|
||||
|
||||
def _listAvailableFiltersCallback(self, result, error=False, *args, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Link", "Error while listing information about the link: {}".format(result["message"]))
|
||||
log.warning("Error while listing information about the link: {}".format(result["message"]))
|
||||
return
|
||||
self._filters = result
|
||||
self._initialized = True
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qslot
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qslot, sip_is_deleted
|
||||
from ..ui.project_dialog_ui import Ui_ProjectDialog
|
||||
from ..controller import Controller
|
||||
from ..topology import Topology
|
||||
@@ -91,6 +91,8 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
|
||||
projects_to_delete = set()
|
||||
for project in self.uiProjectsTreeWidget.selectedItems():
|
||||
if sip_is_deleted(project):
|
||||
continue
|
||||
project_id = project.data(0, QtCore.Qt.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
@@ -106,6 +108,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
Controller.instance().deleteProject(project_id)
|
||||
|
||||
def _duplicateProjectSlot(self):
|
||||
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Duplicate project", "No project selected")
|
||||
return
|
||||
@@ -135,12 +138,16 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
if Controller.instance().isRemote():
|
||||
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
|
||||
self._duplicateCallback,
|
||||
body={"name": name})
|
||||
body={"name": name},
|
||||
progressText="Duplicating project '{}'...".format(name),
|
||||
timeout=None)
|
||||
else:
|
||||
project_location = os.path.join(Topology.instance().projectsDirPath(), name)
|
||||
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
|
||||
self._duplicateCallback,
|
||||
body={"name": name, "path": project_location})
|
||||
body={"name": name, "path": project_location},
|
||||
progressText="Duplicating project '{}'...".format(name),
|
||||
timeout=None)
|
||||
|
||||
def _duplicateCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
|
||||
88
gns3/dialogs/project_welcome_dialog.py
Normal file
88
gns3/dialogs/project_welcome_dialog.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore, qpartial
|
||||
from gns3.ui.project_welcome_dialog_ui import Ui_ProjectWelcomeDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
|
||||
"""
|
||||
This dialog shows when project is imported and global variables assigned to the project are missing.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project):
|
||||
|
||||
super().__init__(parent)
|
||||
self._project = project
|
||||
self.setupUi(self)
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
self.gridLayout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.label.setOpenExternalLinks(True)
|
||||
|
||||
self._variables = self._getVariables(project)
|
||||
|
||||
self._loadReadme()
|
||||
self._addMisingVariablesEdits()
|
||||
|
||||
def _getVariables(self, project):
|
||||
variables = copy.copy(self._project.variables())
|
||||
if variables is None:
|
||||
variables = []
|
||||
return variables
|
||||
|
||||
def _addMisingVariablesEdits(self):
|
||||
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
|
||||
for i, variable in enumerate(missing, start=0):
|
||||
nameLabel = QtWidgets.QLabel()
|
||||
nameLabel.setText(variable.get("name", ""))
|
||||
self.gridLayout.addWidget(nameLabel, i, 0)
|
||||
|
||||
valueEdit = QtWidgets.QLineEdit()
|
||||
valueEdit.setText(variable.get("value", ""))
|
||||
valueEdit.textChanged.connect(qpartial(self.onValueChange, variable))
|
||||
self.gridLayout.addWidget(valueEdit, i, 1)
|
||||
|
||||
def _loadReadme(self):
|
||||
self._project.get("/files/README.txt", self._loadedReadme)
|
||||
|
||||
def _loadedReadme(self, result, error=False, raw_body=None, context={}, **kwargs):
|
||||
if not error:
|
||||
self.label.setText(raw_body.decode("utf-8"))
|
||||
|
||||
def onValueChange(self, variable, text):
|
||||
variable["value"] = text
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
|
||||
if len(missing) > 0:
|
||||
reply = QtWidgets.QMessageBox.warning(
|
||||
self, 'Missing values',
|
||||
'Are you sure you want to continue without providing missing values?',
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
self._project.setVariables(self._variables)
|
||||
self._project.update()
|
||||
self.accept()
|
||||
|
||||
@@ -86,9 +86,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
self.uiLocalServerHostComboBox.addItem(address_string, address.toString())
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.jpg"))
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.png"))
|
||||
else:
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.jpg"))
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.png"))
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
self.uiVMRadioButton.setText("Run the topologies in an isolated and standard VM")
|
||||
@@ -116,9 +116,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
def _VMwareBannerButtonClickedSlot(self):
|
||||
if sys.platform.startswith("darwin"):
|
||||
url = "http://send.onenetworkdirect.net/z/616461/CD225091/"
|
||||
url = "http://send.onenetworkdirect.net/z/621395/CD225091/"
|
||||
else:
|
||||
url = "http://send.onenetworkdirect.net/z/616460/CD225091/"
|
||||
url = "http://send.onenetworkdirect.net/z/616207/CD225091/"
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
|
||||
|
||||
def _listVMwareVMsSlot(self):
|
||||
@@ -263,7 +263,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
def _saveSettingsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while save settings: {}".format(result["message"]))
|
||||
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while saving settings: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def _addSummaryEntry(self, name, value):
|
||||
|
||||
@@ -22,8 +22,6 @@ Dialog to manage the snapshots.
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..ui.snapshots_dialog_ui import Ui_SnapshotsDialog
|
||||
from ..controller import Controller
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..utils.create_snapshot_worker import CreateSnapshotWorker
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@@ -87,21 +85,21 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
|
||||
if ok and snapshot_name and self._project:
|
||||
snapshot_worker = CreateSnapshotWorker(self._project, snapshot_name)
|
||||
snapshot_worker.finished.connect(self._createSnapshotsCallback)
|
||||
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()),
|
||||
self._createSnapshotsCallback,
|
||||
{"name": snapshot_name},
|
||||
progressText="Creation of snapshot '{}' in progress...".format(snapshot_name),
|
||||
timeout=None)
|
||||
|
||||
progress_dialog = ProgressDialog(snapshot_worker, "Snapshot progress", "Creation of snapshot in progress...",
|
||||
"Cancel", busy=True, parent=self, create_thread=False, cancelable=True)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
|
||||
def _createSnapshotsCallback(self):
|
||||
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
else:
|
||||
log.error("Cannot create snapshot of project")
|
||||
return
|
||||
self._listSnapshots()
|
||||
|
||||
def _createSnapshotsErrorCallback(self, message, error):
|
||||
log.error(message)
|
||||
|
||||
def _deleteSnapshotSlot(self):
|
||||
"""
|
||||
Slot to delete a snapshot.
|
||||
@@ -113,6 +111,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
Controller.instance().delete("/projects/{}/snapshots/{}".format(self._project.id(), snapshot_id), self._deleteSnapshotsCallback)
|
||||
|
||||
def _deleteSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
@@ -135,13 +134,16 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
:param snapshot_id: id of the snapshot
|
||||
"""
|
||||
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
|
||||
|
||||
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken, would you like to proceed?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
|
||||
if reply == QtWidgets.QMessageBox.Cancel:
|
||||
return
|
||||
|
||||
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback, timeout=300)
|
||||
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id),
|
||||
self._restoreSnapshotsCallback, progressText="Restoring snapshot...", timeout=None)
|
||||
|
||||
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
|
||||
@@ -22,7 +22,7 @@ Dialog to change node symbols.
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qpartial, sip_is_deleted
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from ..ui.symbol_selection_dialog_ui import Ui_SymbolSelectionDialog
|
||||
from ..local_server import LocalServer
|
||||
@@ -91,6 +91,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
item.setIcon(icon)
|
||||
|
||||
def render(item, path):
|
||||
if sip_is_deleted(item):
|
||||
return
|
||||
svg_renderer = QImageSvgRenderer(path)
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
@@ -184,7 +186,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
|
||||
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while uploading symbol: {}".format(path))
|
||||
log.error("Error while uploading symbol: {}: {}".format(path, result.get("message", "unknown")))
|
||||
return
|
||||
self.uiSymbolLineEdit.clear()
|
||||
self.uiSymbolLineEdit.setText(path)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Text editor to edit Note items.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtWidgets, qslot
|
||||
from ..qt import QtCore, QtWidgets, qslot, sip_is_deleted
|
||||
from ..ui.text_editor_dialog_ui import Ui_TextEditorDialog
|
||||
|
||||
|
||||
@@ -98,6 +98,8 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
"""
|
||||
|
||||
for item in self._items:
|
||||
if sip_is_deleted(item):
|
||||
continue
|
||||
item.setFont(self.uiPlainTextEdit.font())
|
||||
if self.uiApplyColorToAllItemsCheckBox.isChecked():
|
||||
item.setDefaultTextColor(self._color)
|
||||
|
||||
@@ -21,7 +21,7 @@ Graphical view on the scene where items are drawn.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sip
|
||||
from .qt import sip
|
||||
import sys
|
||||
|
||||
from .qt import QtCore, QtGui, QtNetwork, QtWidgets, qpartial, qslot
|
||||
@@ -60,6 +60,7 @@ from .items.rectangle_item import RectangleItem
|
||||
from .items.line_item import LineItem
|
||||
from .items.ellipse_item import EllipseItem
|
||||
from .items.image_item import ImageItem
|
||||
from .items.logo_item import LogoItem
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -88,6 +89,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_line = False
|
||||
self._newlink = None
|
||||
self._dragging = False
|
||||
self._grid_size = 75
|
||||
self._last_mouse_position = None
|
||||
self._topology = Topology.instance()
|
||||
self._background_warning_msgbox = QtWidgets.QErrorMessage(self)
|
||||
@@ -127,6 +129,24 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
factor = zoom / 100.
|
||||
self.scale(factor, factor)
|
||||
|
||||
def setGridSize(self, grid_size):
|
||||
"""
|
||||
Sets the grid size.
|
||||
|
||||
:param grid_size: integer
|
||||
"""
|
||||
|
||||
self._grid_size = grid_size
|
||||
|
||||
def gridSize(self):
|
||||
"""
|
||||
Returns the grid size
|
||||
|
||||
:returns: integer
|
||||
"""
|
||||
|
||||
return self._grid_size
|
||||
|
||||
def setEnabled(self, enabled):
|
||||
|
||||
if enabled is False:
|
||||
@@ -275,6 +295,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self.scene().addItem(image_item)
|
||||
self._topology.addDrawing(image_item)
|
||||
|
||||
def addLogo(self, logo_path, logo_url):
|
||||
logo_item = LogoItem(logo_path, logo_url, self._topology.project())
|
||||
self.scene().addItem(logo_item)
|
||||
|
||||
def addLink(self, source_node, source_port, destination_node, destination_port, **link_data):
|
||||
"""
|
||||
Creates a Link instance representing a connection between 2 devices.
|
||||
@@ -395,12 +419,16 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
is_not_link = True
|
||||
is_not_logo = True
|
||||
|
||||
item = self.itemAt(event.pos())
|
||||
if item and sip.isdeleted(item):
|
||||
return
|
||||
|
||||
if item and (isinstance(item, LinkItem) or isinstance(item.parentItem(), LinkItem)):
|
||||
is_not_link = False
|
||||
if item and (isinstance(item, LogoItem) or isinstance(item.parentItem(), LogoItem)):
|
||||
is_not_logo = False
|
||||
else:
|
||||
for it in self.scene().items():
|
||||
if isinstance(it, LinkItem):
|
||||
@@ -420,7 +448,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
item.setSelected(False)
|
||||
else:
|
||||
item.setSelected(True)
|
||||
elif is_not_link and event.button() == QtCore.Qt.RightButton and not self._adding_link:
|
||||
elif is_not_link and is_not_logo and event.button() == QtCore.Qt.RightButton and not self._adding_link:
|
||||
if item and not sip.isdeleted(item):
|
||||
# Prevent right clicking on a selected item from de-selecting all other items
|
||||
if not item.isSelected():
|
||||
@@ -1546,22 +1574,22 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
def drawBackground(self, painter, rect):
|
||||
super().drawBackground(painter, rect)
|
||||
if self._main_window.uiShowGridAction.isChecked():
|
||||
gridSize = 75
|
||||
if self._main_window.uiShowGridAction.isChecked() and self.gridSize():
|
||||
grid_size = self.gridSize()
|
||||
painter.save()
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor(190, 190, 190)))
|
||||
|
||||
left = int(rect.left()) - (int(rect.left()) % gridSize)
|
||||
top = int(rect.top()) - (int(rect.top()) % gridSize)
|
||||
left = int(rect.left()) - (int(rect.left()) % grid_size)
|
||||
top = int(rect.top()) - (int(rect.top()) % grid_size)
|
||||
|
||||
x = left
|
||||
while x < rect.right():
|
||||
painter.drawLine(x, rect.top(), x, rect.bottom())
|
||||
x += gridSize
|
||||
x += grid_size
|
||||
y = top
|
||||
while y < rect.bottom():
|
||||
painter.drawLine(rect.left(), y, rect.right(), y)
|
||||
y += gridSize
|
||||
y += grid_size
|
||||
painter.restore()
|
||||
|
||||
def toggleUiDeviceMenu(self):
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sip
|
||||
from .qt import sip
|
||||
import json
|
||||
import copy
|
||||
import http
|
||||
@@ -47,7 +47,7 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
HTTP client.
|
||||
|
||||
:param settings: Dictionnary with connection information to the server
|
||||
:param settings: Dictionary with connection information to the server
|
||||
:param network_manager: A QT network manager
|
||||
"""
|
||||
|
||||
@@ -209,16 +209,16 @@ class HTTPClient(QtCore.QObject):
|
||||
Called when a query upload progress
|
||||
"""
|
||||
if not sip_is_deleted(HTTPClient._progress_callback):
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(total))
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
|
||||
|
||||
def _notify_progress_download(self, query_id, sent, total):
|
||||
"""
|
||||
Called when a query download progress
|
||||
"""
|
||||
if not sip_is_deleted(HTTPClient._progress_callback):
|
||||
# abs() for maxium because sometimes the system send negative
|
||||
# abs() for maximum because sometimes the system send negative
|
||||
# values
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(abs(total)))
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
|
||||
|
||||
@classmethod
|
||||
def setProgressCallback(cls, progress_callback):
|
||||
@@ -303,16 +303,17 @@ class HTTPClient(QtCore.QObject):
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
# TODO: clean this
|
||||
# We try to detect computer hibernation
|
||||
# if time between two query is too long we trigger a disconnect
|
||||
if self._max_time_difference_between_queries:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
if self._last_query_timestamp is not None and now > self._last_query_timestamp + self._max_time_difference_between_queries:
|
||||
log.warning("Synchronisation lost with the server.")
|
||||
self.disconnect()
|
||||
self._last_query_timestamp = None
|
||||
return
|
||||
self._last_query_timestamp = now
|
||||
# if self._max_time_difference_between_queries:
|
||||
# now = datetime.datetime.now().timestamp()
|
||||
# if self._last_query_timestamp is not None and now > self._last_query_timestamp + self._max_time_difference_between_queries:
|
||||
# log.warning("Synchronisation lost with the server.")
|
||||
# self.disconnect()
|
||||
# self._last_query_timestamp = None
|
||||
# return
|
||||
# self._last_query_timestamp = now
|
||||
|
||||
request = qpartial(self._executeHTTPQuery, method, path, qpartial(callback), body, context,
|
||||
downloadProgressCallback=downloadProgressCallback,
|
||||
@@ -391,21 +392,22 @@ class HTTPClient(QtCore.QObject):
|
||||
return
|
||||
|
||||
if params["version"].split("-")[0] != __version__.split("-")[0]:
|
||||
msg = "Client version {} differs with server version {}".format(__version__, params["version"])
|
||||
log.error(msg)
|
||||
msg = "Client version {} is not the same as server (controller) version {}".format(__version__, params["version"])
|
||||
# Stable release
|
||||
if __version_info__[3] == 0:
|
||||
log.error(msg)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
return
|
||||
# We don't allow different major version to interact even with dev build
|
||||
elif parse_version(__version__)[:2] != parse_version(params["version"])[:2]:
|
||||
log.error(msg)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
return
|
||||
log.warning("Use a different client and server version can create bugs. Use it at your own risk.")
|
||||
log.warning("{}\nUsing different versions may result in unexpected problems. Please upgrade or use at your own risk.".format(msg))
|
||||
|
||||
self._connected = True
|
||||
self._retry = 0
|
||||
@@ -490,7 +492,7 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
def _paramsToQueryString(self, params):
|
||||
"""
|
||||
:param params: Dictionnary of query string parameters
|
||||
:param params: Dictionary of query string parameters
|
||||
:returns: String of the query string
|
||||
"""
|
||||
if params == {}:
|
||||
@@ -618,7 +620,7 @@ class HTTPClient(QtCore.QObject):
|
||||
# We check if we received HTTP headers
|
||||
if not sip.isdeleted(response) and response.isRunning() and not len(response.rawHeaderList()) > 0:
|
||||
if not response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
log.warning("Timeout after {} seconds for request {}".format(timeout, response.url().toString()))
|
||||
log.warning("Timeout after {} seconds for request {}. Please check the connection is not blocked by a firewall or an anti-virus.".format(timeout, response.url().toString()))
|
||||
response.abort()
|
||||
|
||||
def disconnect(self):
|
||||
@@ -631,14 +633,14 @@ class HTTPClient(QtCore.QObject):
|
||||
def _requestCanceled(self, response, context):
|
||||
|
||||
if response.isRunning() and not response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
log.warn("Aborting request for {}".format(response.url().toString()))
|
||||
log.warning("Aborting request for {}".format(response.url().toString()))
|
||||
response.abort()
|
||||
if "query_id" in context:
|
||||
self._notify_progress_end_query(context["query_id"])
|
||||
|
||||
def _processError(self, response, server, callback, context, request_body, ignore_errors, error_code):
|
||||
if error_code != QtNetwork.QNetworkReply.NoError:
|
||||
error_message = response.errorString()
|
||||
error_message = "{} ({}:{})".format(response.errorString(), self._host, self._port)
|
||||
|
||||
if not ignore_errors:
|
||||
log.debug("Response error: %s for %s (error: %d)", error_message, response.url().toString(), error_code)
|
||||
@@ -648,7 +650,10 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
if error_code < 200 or error_code == 403:
|
||||
if error_code == QtNetwork.QNetworkReply.OperationCanceledError: # It's legit to cancel do not disconnect
|
||||
error_message = "Operation timeout" # It's more clear than cancel, because cancel is trigger by us when we timeout
|
||||
error_message = "Operation timeout" # It's clearer than cancel because cancel is triggered by us when we timeout
|
||||
elif error_code == QtNetwork.QNetworkReply.NetworkSessionFailedError:
|
||||
# ignore the network session failed error to let the network manager recover from it
|
||||
return
|
||||
elif not ignore_errors:
|
||||
self.disconnect()
|
||||
if callback is not None:
|
||||
@@ -722,46 +727,54 @@ class HTTPClient(QtCore.QObject):
|
||||
e = HttpBadRequest(body)
|
||||
raise e
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
def getSynchronous(self, method, endpoint, prefix="/v2", timeout=2):
|
||||
"""
|
||||
Synchronous check if a server is running
|
||||
|
||||
:returns: Tuple (Status code, json of anwser). Status 0 is a non HTTP error
|
||||
:returns: Tuple (Status code, json of answer). Status 0 is a non HTTP error
|
||||
"""
|
||||
try:
|
||||
url = "{protocol}://{host}:{port}/v2/{endpoint}".format(protocol=self._protocol, host=self._host, port=self._port, endpoint=endpoint)
|
||||
|
||||
if self._user is not None and len(self._user) > 0:
|
||||
log.debug("Synchronous get {} with user '{}'".format(url, self._user))
|
||||
auth_handler = urllib.request.HTTPBasicAuthHandler()
|
||||
auth_handler.add_password(realm="GNS3 server",
|
||||
uri=url,
|
||||
user=self._user,
|
||||
passwd=self._password)
|
||||
opener = urllib.request.build_opener(auth_handler)
|
||||
urllib.request.install_opener(opener)
|
||||
else:
|
||||
log.debug("Synchronous get {} (no authentication)".format(url))
|
||||
response = urllib.request.urlopen(url, timeout=timeout)
|
||||
content_type = response.getheader("CONTENT-TYPE")
|
||||
if response.status == 200:
|
||||
host = self._getHostForQuery()
|
||||
|
||||
log.debug("{method} {protocol}://{host}:{port}{prefix}{endpoint}".format(method=method, protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
if self._user:
|
||||
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, user=self._user, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
else:
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
|
||||
request = self._request(url)
|
||||
request = self._addAuth(request)
|
||||
request.setRawHeader(b"User-Agent", "GNS3 QT Client v{version}".format(version=__version__).encode())
|
||||
|
||||
try:
|
||||
response = self._network_manager.sendCustomRequest(request, method.encode())
|
||||
except SystemError as e:
|
||||
log.error("Can't send query: {}".format(str(e)))
|
||||
return
|
||||
|
||||
loop = QtCore.QEventLoop()
|
||||
response.finished.connect(loop.quit)
|
||||
|
||||
if timeout is not None:
|
||||
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response, timeout))
|
||||
|
||||
if not loop.isRunning():
|
||||
loop.exec_()
|
||||
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
log.debug("Error while connecting to local server {}".format(response.errorString()))
|
||||
return status, None
|
||||
else:
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
if status == 200:
|
||||
if content_type == "application/json":
|
||||
content = response.read()
|
||||
content = bytes(response.readAll())
|
||||
json_data = json.loads(content.decode("utf-8"))
|
||||
return response.status, json_data
|
||||
return status, json_data
|
||||
else:
|
||||
return response.status, None
|
||||
except http.client.InvalidURL as e:
|
||||
log.warn("Invalid local server url: {}".format(e))
|
||||
return 0, None
|
||||
except urllib.error.URLError:
|
||||
# Connection refused. It's a normal behavior if server is not started
|
||||
return 0, None
|
||||
except urllib.error.HTTPError as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return e.code, None
|
||||
except (OSError, http.client.BadStatusLine, ValueError) as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return status, None
|
||||
|
||||
return 0, None
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -43,6 +43,7 @@ class DrawingItem:
|
||||
|
||||
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, rotation=0, **kws):
|
||||
self._id = drawing_id
|
||||
self._deleting = False
|
||||
if self._id is None:
|
||||
self._id = str(uuid.uuid4())
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
@@ -81,13 +82,13 @@ class DrawingItem:
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up drawing: {}".format(result["message"]))
|
||||
log.error("Error while creating drawing: {}".format(result["message"]))
|
||||
return False
|
||||
self._id = result["drawing_id"]
|
||||
self.updateDrawingCallback(result)
|
||||
|
||||
def updateDrawing(self):
|
||||
if self._id:
|
||||
if self._id and not self.deleting() and self._project:
|
||||
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__(), showProgress=False)
|
||||
|
||||
@qslot
|
||||
@@ -101,7 +102,7 @@ class DrawingItem:
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up drawing: {}".format(result["message"]))
|
||||
log.error("Error while updating drawing: {}".format(result["message"]))
|
||||
return False
|
||||
self.setPos(QtCore.QPoint(result["x"], result["y"]))
|
||||
self.setZValue(result["z"])
|
||||
@@ -173,6 +174,20 @@ class DrawingItem:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def deleting(self):
|
||||
"""
|
||||
Is the link being deleted
|
||||
"""
|
||||
|
||||
return self._deleting
|
||||
|
||||
def setDeleting(self):
|
||||
"""
|
||||
Mark this drawing as being deleted
|
||||
"""
|
||||
|
||||
self._deleting = True
|
||||
|
||||
def delete(self, skip_controller=False):
|
||||
"""
|
||||
Deletes this drawing.
|
||||
@@ -180,6 +195,7 @@ class DrawingItem:
|
||||
:param skip_controller: Do not replicate change on the controller (usefull when it's already deleted on controller)
|
||||
"""
|
||||
|
||||
self.setDeleting()
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeDrawing(self)
|
||||
@@ -188,11 +204,11 @@ class DrawingItem:
|
||||
|
||||
def itemChange(self, change, value):
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
GRID_SIZE = 75
|
||||
grid_size = self._graphics_view.gridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
tmp_x = (GRID_SIZE * round((self.x() + mid_x) / GRID_SIZE)) - mid_x
|
||||
tmp_x = (grid_size * round((self.x() + mid_x) / grid_size)) - mid_x
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
tmp_y = (GRID_SIZE * round((self.y() + mid_y) / GRID_SIZE)) - mid_y
|
||||
tmp_y = (grid_size * round((self.y() + mid_y) / grid_size)) - mid_y
|
||||
if tmp_x != self.x() and tmp_y != self.y():
|
||||
self.setPos(tmp_x, tmp_y)
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ class EthernetLinkItem(LinkItem):
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
if not self._adding_flag and self._settings["draw_link_status_points"]:
|
||||
if not self._adding_flag:
|
||||
|
||||
# points disappears if nodes are too close to each others.
|
||||
if self.length < 100:
|
||||
@@ -151,7 +151,8 @@ class EthernetLinkItem(LinkItem):
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(point1)
|
||||
if self._settings["draw_link_status_points"]:
|
||||
painter.drawPoint(point1)
|
||||
|
||||
if self._link.suspended() or self._destination_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
@@ -192,6 +193,7 @@ class EthernetLinkItem(LinkItem):
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(point2)
|
||||
if self._settings["draw_link_status_points"]:
|
||||
painter.drawPoint(point2)
|
||||
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -21,7 +21,7 @@ Link items are graphical representation of a link on the QGraphicsScene
|
||||
"""
|
||||
|
||||
import math
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot, sip_is_deleted
|
||||
|
||||
from ..packet_capture import PacketCapture
|
||||
from ..dialogs.filter_dialog import FilterDialog
|
||||
@@ -286,14 +286,15 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
QtWidgets.QApplication.sendEvent(MainWindow.instance(), key)
|
||||
return
|
||||
|
||||
# create the contextual menu
|
||||
self.setAcceptHoverEvents(False)
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateLinkContextualMenu(menu)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._hovered = False
|
||||
self.adjust()
|
||||
if not sip_is_deleted(self):
|
||||
# create the contextual menu
|
||||
self.setAcceptHoverEvents(False)
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateLinkContextualMenu(menu)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._hovered = False
|
||||
self.adjust()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
|
||||
136
gns3/items/logo_item.py
Normal file
136
gns3/items/logo_item.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import urllib.parse
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from ..controller import Controller
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LogoItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
Margin for the logo
|
||||
"""
|
||||
MARGIN = 20
|
||||
|
||||
"""
|
||||
Logo for the scene.
|
||||
|
||||
:param logo_path: Path to the logo (remote)
|
||||
:param logo_url: URL which needs to be open user clicks on the logo
|
||||
:param project: Current project
|
||||
"""
|
||||
|
||||
def __init__(self, logo_path, logo_url, project):
|
||||
super().__init__()
|
||||
|
||||
self._logo_path = logo_path
|
||||
self._logo_url = logo_url
|
||||
self._project = project
|
||||
|
||||
# Temporary symbol during loading
|
||||
renderer = QImageSvgRenderer(":/icons/reload.svg")
|
||||
renderer.setObjectName("symbol_loading")
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
effect = QtWidgets.QGraphicsColorizeEffect()
|
||||
effect.setColor(QtGui.QColor("black"))
|
||||
effect.setStrength(0.8)
|
||||
self.setGraphicsEffect(effect)
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
# set graphical settings for this item
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._main_window = MainWindow.instance()
|
||||
self._settings = self._main_window.uiGraphicsView.settings()
|
||||
|
||||
self.updatePosition()
|
||||
|
||||
self._main_window.uiGraphicsView.viewport().installEventFilter(self)
|
||||
|
||||
remote_file = urllib.parse.quote('project-files/images/{}'.format(logo_path))
|
||||
|
||||
Controller.instance().getStatic(
|
||||
'/projects/{}/files/{}'.format(project.id(), remote_file),
|
||||
self.updateImage
|
||||
)
|
||||
|
||||
# make it the last one
|
||||
self.setZValue(-2)
|
||||
|
||||
def eventFilter(self, source, event):
|
||||
if event.type() == QtCore.QEvent.Paint:
|
||||
self.updatePosition()
|
||||
return QtWidgets.QWidget.eventFilter(self, source, event)
|
||||
|
||||
|
||||
def updateImage(self, local_path):
|
||||
renderer = QImageSvgRenderer(local_path)
|
||||
renderer.setObjectName("project_logo")
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
|
||||
def updatePosition(self):
|
||||
"""
|
||||
Updates position to be located in the right bottom corner
|
||||
"""
|
||||
logo_rect = self.boundingRect()
|
||||
width = self._main_window.uiGraphicsView.viewport().width()
|
||||
height = self._main_window.uiGraphicsView.viewport().height()
|
||||
rect = self._main_window.uiGraphicsView.mapToScene(QtCore.QRect(0, 0, width, height)).boundingRect()
|
||||
x = rect.x() + rect.width() - self.MARGIN - logo_rect.width()
|
||||
y = rect.y() + rect.height() - self.MARGIN - logo_rect.height()
|
||||
|
||||
# update only when changes
|
||||
if [int(self.x()), int(self.y())] != [int(x), int(y)]:
|
||||
self.setX(x)
|
||||
self.setY(y)
|
||||
self.update()
|
||||
|
||||
def hoverEnterEvent(self, event):
|
||||
"""
|
||||
Handles all hover enter events for this item.
|
||||
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
if self._logo_url is not None:
|
||||
self.graphicsEffect().setEnabled(True)
|
||||
|
||||
def hoverLeaveEvent(self, event):
|
||||
"""
|
||||
Handles all hover leave events for this item.
|
||||
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
url = QtCore.QUrl(self._logo_url)
|
||||
if not QtGui.QDesktopServices.openUrl(url):
|
||||
QtWidgets.QMessageBox.warning(self, 'Open Url', 'Could not open url')
|
||||
@@ -19,7 +19,7 @@
|
||||
Graphical representation of a node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import sip
|
||||
from ..qt import sip
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
@@ -41,7 +41,6 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
GRID_SIZE = 75
|
||||
|
||||
def __init__(self, node):
|
||||
super().__init__()
|
||||
@@ -100,26 +99,16 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._main_window = MainWindow.instance()
|
||||
if self._main_window.uiSnapToGridAction.isChecked():
|
||||
self._snapToGrid()
|
||||
self._settings = self._main_window.uiGraphicsView.settings()
|
||||
|
||||
if node.initialized():
|
||||
self.createdSlot(node.id())
|
||||
|
||||
def _snapToGrid(self):
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
x = (self.GRID_SIZE * round((self.x() + mid_x) / self.GRID_SIZE)) - mid_x
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
y = (self.GRID_SIZE * round((self.y() + mid_y) / self.GRID_SIZE)) - mid_y
|
||||
self.setPos(x, y)
|
||||
|
||||
def updateNode(self):
|
||||
"""
|
||||
Sync change to the node
|
||||
"""
|
||||
if self._initialized:
|
||||
self._node.setGraphics(self)
|
||||
self._node.setGraphics(self)
|
||||
|
||||
@qslot
|
||||
def setSymbol(self, symbol):
|
||||
@@ -144,9 +133,14 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
@qslot
|
||||
def _symbolLoadedCallback(self, path, *args):
|
||||
|
||||
renderer = QImageSvgRenderer(path, fallback=":/icons/cancel.svg")
|
||||
renderer.setObjectName(path)
|
||||
self.setSharedRenderer(renderer)
|
||||
if self._settings["limit_size_node_symbols"] is True and renderer.defaultSize().height() > 80:
|
||||
# resize the SVG
|
||||
renderer.resize(80)
|
||||
self.setSharedRenderer(renderer)
|
||||
if self._node.settings().get("symbol") != self._symbol:
|
||||
self.updateNode()
|
||||
if not self._initialized:
|
||||
@@ -458,10 +452,11 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
grid_size = self._main_window.uiGraphicsView.gridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
value.setX((self.GRID_SIZE * round((value.x() + mid_x) / self.GRID_SIZE)) - mid_x)
|
||||
value.setX((grid_size * round((value.x() + mid_x) / grid_size)) - mid_x)
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
value.setY((self.GRID_SIZE * round((value.y() + mid_y) / self.GRID_SIZE)) - mid_y)
|
||||
value.setY((grid_size * round((value.y() + mid_y) / grid_size)) - mid_y)
|
||||
|
||||
# dynamically change the renderer when this node item is selected/unselected.
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
|
||||
@@ -107,7 +107,7 @@ class SerialLinkItem(LinkItem):
|
||||
|
||||
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
|
||||
if not self._adding_flag and self._settings["draw_link_status_points"]:
|
||||
if not self._adding_flag:
|
||||
|
||||
# points disappears if nodes are too close to each others.
|
||||
if self.length < 80:
|
||||
@@ -140,7 +140,8 @@ class SerialLinkItem(LinkItem):
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.source_point)
|
||||
if self._settings["draw_link_status_points"]:
|
||||
painter.drawPoint(self.source_point)
|
||||
|
||||
# destination point color
|
||||
if self._link.suspended() or self._destination_port.status() == Port.suspended:
|
||||
@@ -170,6 +171,7 @@ class SerialLinkItem(LinkItem):
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.destination_point)
|
||||
if self._settings["draw_link_status_points"]:
|
||||
painter.drawPoint(self.destination_point)
|
||||
|
||||
self._drawSymbol()
|
||||
|
||||
28
gns3/link.py
28
gns3/link.py
@@ -21,10 +21,10 @@ Manages and stores everything needed for a connection between 2 devices.
|
||||
|
||||
import os
|
||||
import re
|
||||
import sip
|
||||
from .qt import sip
|
||||
import uuid
|
||||
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .qt import QtCore
|
||||
from .controller import Controller
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ class Link(QtCore.QObject):
|
||||
self._destination_label = None
|
||||
self._link_id = link_id
|
||||
self._capturing = False
|
||||
self._deleting = False
|
||||
self._capture_file_path = None
|
||||
self._capture_file = None
|
||||
self._initialized = False
|
||||
@@ -158,7 +159,7 @@ class Link(QtCore.QObject):
|
||||
self._updateLabels()
|
||||
|
||||
def update(self):
|
||||
if not self._link_id:
|
||||
if not self._link_id or self.deleting():
|
||||
return
|
||||
body = self._prepareParams()
|
||||
Controller.instance().put("/projects/{project_id}/links/{link_id}".format(project_id=self._source_node.project().id(), link_id=self._link_id), self.updateLinkCallback, body=body)
|
||||
@@ -171,7 +172,7 @@ class Link(QtCore.QObject):
|
||||
|
||||
def updateLinkCallback(self, result, error=False, *args, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Update link", "Error while updating link: {}".format(result["message"]))
|
||||
log.warning("Error while updating link: {}".format(result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
@@ -221,7 +222,7 @@ class Link(QtCore.QObject):
|
||||
|
||||
def _linkCreatedCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Create link", "Error while creating link: {}".format(result["message"]))
|
||||
log.warning("Error while creating link: {}".format(result["message"]))
|
||||
self.deleteLink(skip_controller=True)
|
||||
return
|
||||
|
||||
@@ -244,6 +245,19 @@ class Link(QtCore.QObject):
|
||||
def link_id(self):
|
||||
return self._link_id
|
||||
|
||||
def deleting(self):
|
||||
"""
|
||||
Is the link being deleted
|
||||
"""
|
||||
return self._deleting
|
||||
|
||||
def setDeleting(self):
|
||||
"""
|
||||
Mark this link as being deleted
|
||||
"""
|
||||
|
||||
self._deleting = True
|
||||
|
||||
def capturing(self):
|
||||
"""
|
||||
Is a capture running on the link?
|
||||
@@ -306,8 +320,10 @@ class Link(QtCore.QObject):
|
||||
if skip_controller:
|
||||
self._linkDeletedCallback({})
|
||||
else:
|
||||
self.setDeleting()
|
||||
Controller.instance().delete("/projects/{project_id}/links/{link_id}".format(project_id=self.project().id(),
|
||||
link_id=self._link_id), self._linkDeletedCallback)
|
||||
link_id=self._link_id),
|
||||
self._linkDeletedCallback)
|
||||
|
||||
def _linkDeletedCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -216,9 +216,12 @@ class LocalConfig(QtCore.QObject):
|
||||
# settings from 1.6.1 with 1.5.1 you will have an error
|
||||
if "version" in self._settings:
|
||||
if parse_version(self._settings["version"])[:2] > parse_version(__version__)[:2]:
|
||||
QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
|
||||
QtWidgets.QMessageBox.critical(None, "Version error", "Your settings are for version {} of GNS3. You cannot use a previous version of GNS3 without risking losing data. If you want to reset delete the settings in {}".format(self._settings["version"], self.configDirectory()))
|
||||
app = QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
|
||||
error_message = "Your settings are for version {} of GNS3. You cannot use a previous version of GNS3 without risking losing data. If you want to reset delete the settings in {}".format(self._settings["version"], self.configDirectory())
|
||||
QtWidgets.QMessageBox.critical(False, "Version error", error_message)
|
||||
# Exit immediately not clean but we want to avoid any side effect that could corrupt the file
|
||||
QtCore.QTimer.singleShot(0, app.quit)
|
||||
app.exec_()
|
||||
sys.exit(1)
|
||||
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.0alpha1"):
|
||||
@@ -265,11 +268,13 @@ class LocalConfig(QtCore.QObject):
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
|
||||
if "IOU" in self._settings and "iourc_path" in self._settings["IOU"] and "iourc_content" not in self._settings["IOU"]:
|
||||
try:
|
||||
with open(self._settings["IOU"]["iourc_path"], "r") as f:
|
||||
with open(self._settings["IOU"]["iourc_path"], "r", encoding="utf-8") as f:
|
||||
self._settings["IOU"]["iourc_content"] = f.read().replace("\r\n", "\n")
|
||||
del self._settings["IOU"]["iourc_path"]
|
||||
except OSError as e:
|
||||
log.warn("Can't import IOU licence {}: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
|
||||
log.warning("Can't import IOU licence {}: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
|
||||
except UnicodeDecodeError as e:
|
||||
log.warning("Non ascii characters in iourc file {}, please remove them: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
|
||||
|
||||
def _readConfig(self, config_path):
|
||||
"""
|
||||
@@ -317,7 +322,7 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
if Controller.instance().connected() and self._settings_retrieved_from_controller:
|
||||
# We save only non user specific sections
|
||||
section_to_save_on_controller = ["Builtin", "Docker", "IOU", "Qemu", "VMware", "VPCS", "VirtualBox", "GraphicsView", "Dynamips"]
|
||||
section_to_save_on_controller = ["Builtin", "Docker", "IOU", "Qemu", "VMware", "VPCS", "TraceNG", "VirtualBox", "GraphicsView", "Dynamips"]
|
||||
controller_settings = {}
|
||||
for key, val in self._settings.items():
|
||||
if key in section_to_save_on_controller:
|
||||
@@ -475,6 +480,21 @@ class LocalConfig(QtCore.QObject):
|
||||
settings["direct_file_upload"] = value
|
||||
self.saveSectionSettings("MainWindow", settings)
|
||||
|
||||
def showInterfaceLabelsOnNewProject(self):
|
||||
"""
|
||||
:returns: Boolean. True if show_interface_labels_on_new_project is enabled
|
||||
"""
|
||||
|
||||
from gns3.settings import GRAPHICS_VIEW_SETTINGS
|
||||
return self.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS) \
|
||||
.get("show_interface_labels_on_new_project", False)
|
||||
|
||||
def setShowInterfaceLabelsOnNewProject(self, value):
|
||||
from gns3.settings import GRAPHICS_VIEW_SETTINGS
|
||||
settings = self.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS)
|
||||
settings["show_interface_labels_on_new_project"] = value
|
||||
self.saveSectionSettings("GraphicsView", settings)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
|
||||
@@ -36,7 +36,6 @@ from gns3.local_config import LocalConfig
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.http import getSynchronous
|
||||
from gns3.utils.sudo import sudo
|
||||
from gns3.http_client import HTTPClient
|
||||
from gns3.controller import Controller
|
||||
@@ -124,9 +123,13 @@ class LocalServer(QtCore.QObject):
|
||||
return self._parent
|
||||
|
||||
def _checkWindowsService(self, service_name):
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
try:
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
except ImportError as e:
|
||||
log.error("Could not check if the {} service is running: {}".format(service_name, e))
|
||||
|
||||
try:
|
||||
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
|
||||
@@ -136,6 +139,7 @@ class LocalServer(QtCore.QObject):
|
||||
return False
|
||||
else:
|
||||
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
|
||||
|
||||
return True
|
||||
|
||||
def _checkUbridgePermissions(self):
|
||||
@@ -367,15 +371,13 @@ class LocalServer(QtCore.QObject):
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Error", "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
|
||||
return False
|
||||
log.warning("The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
|
||||
|
||||
self._port = self._settings["port"]
|
||||
|
||||
# check the local server path
|
||||
local_server_path = self.localServerPath()
|
||||
if not local_server_path:
|
||||
log.warn("No local server is configured")
|
||||
log.warning("No local server is configured")
|
||||
return False
|
||||
if not os.path.isfile(local_server_path):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not find local server {}".format(local_server_path))
|
||||
@@ -512,9 +514,7 @@ class LocalServer(QtCore.QObject):
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
status, json_data = getSynchronous(self._settings["protocol"], self._settings["host"], self._port, "version",
|
||||
timeout=2, user=self._settings["user"], password=self._settings["password"])
|
||||
|
||||
status, json_data = HTTPClient(self._settings).getSynchronous("GET", "/version", timeout=2)
|
||||
if status == 401: # Auth issue that need to be solved later
|
||||
return True
|
||||
elif json_data is None:
|
||||
|
||||
11
gns3/main.py
11
gns3/main.py
@@ -145,7 +145,8 @@ def main():
|
||||
frozen_dirs = [
|
||||
frozen_dir,
|
||||
os.path.normpath(os.path.join(frozen_dir, 'dynamips')),
|
||||
os.path.normpath(os.path.join(frozen_dir, 'vpcs'))
|
||||
os.path.normpath(os.path.join(frozen_dir, 'vpcs')),
|
||||
os.path.normpath(os.path.join(frozen_dir, 'traceng'))
|
||||
]
|
||||
|
||||
os.environ["PATH"] = os.pathsep.join(frozen_dirs) + os.pathsep + os.environ.get("PATH", "")
|
||||
@@ -267,7 +268,10 @@ def main():
|
||||
# issue when people run GNS3 from the .dmg
|
||||
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
|
||||
if not os.path.realpath(sys.executable).startswith("/Applications"):
|
||||
QtWidgets.QMessageBox.critical(None, "Error", "You need to copy GNS3 in your /Applications folder before using it.")
|
||||
error_message = "GNS3.app must be moved to the '/Applications' folder before it can be used"
|
||||
QtWidgets.QMessageBox.critical(False, "Loading error", error_message)
|
||||
QtCore.QTimer.singleShot(0, app.quit)
|
||||
app.exec_()
|
||||
sys.exit(1)
|
||||
|
||||
global mainwindow
|
||||
@@ -291,7 +295,6 @@ def main():
|
||||
mainwindow.show()
|
||||
|
||||
exit_code = app.exec_()
|
||||
|
||||
signal.signal(signal.SIGINT, orig_sigint)
|
||||
signal.signal(signal.SIGTERM, orig_sigterm)
|
||||
|
||||
@@ -300,7 +303,7 @@ def main():
|
||||
# We force deleting the app object otherwise it's segfault on Fedora
|
||||
del app
|
||||
# We force a full garbage collect before exit
|
||||
# for unknow reason otherwise Qt Segfault on OSX in some
|
||||
# for unknown reason otherwise Qt Segfault on OSX in some
|
||||
# conditions
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
@@ -54,6 +54,7 @@ from .dialogs.new_appliance_dialog import NewApplianceDialog
|
||||
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
|
||||
from .status_bar import StatusBarHandler
|
||||
from .registry.appliance import ApplianceError
|
||||
from .appliance_manager import ApplianceManager
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -69,6 +70,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
# signal to tell the view if the user is adding a link or not
|
||||
adding_link_signal = QtCore.pyqtSignal(bool)
|
||||
|
||||
# Signal of settings updates
|
||||
settings_updated_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None, open_file=None):
|
||||
"""
|
||||
:param open_file: Open this file instead of asking for a new project
|
||||
@@ -110,6 +114,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._local_config_timer.timeout.connect(local_config.checkConfigChanged)
|
||||
self._local_config_timer.start(1000) # milliseconds
|
||||
self._analytics_client = AnalyticsClient()
|
||||
self._appliance_manager = ApplianceManager()
|
||||
|
||||
# restore the geometry and state of the main window.
|
||||
self.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["geometry"].encode()))
|
||||
@@ -271,6 +276,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
# connect the signal to the view
|
||||
self.adding_link_signal.connect(self.uiGraphicsView.addingLinkSlot)
|
||||
|
||||
# connect to the signal when settings change
|
||||
self.settings_updated_signal.connect(self.settingsChangedSlot)
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
Loads the settings from the persistent settings file.
|
||||
@@ -303,6 +311,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._settings.update(new_settings)
|
||||
# save the settings
|
||||
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
|
||||
self.settings_updated_signal.emit()
|
||||
|
||||
def _openWebInterfaceActionSlot(self):
|
||||
if Controller.instance().connected():
|
||||
@@ -491,6 +500,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._project_dialog = None
|
||||
self._refreshVisibleWidgets()
|
||||
|
||||
@qslot
|
||||
def settingsChangedSlot(self, *args):
|
||||
"""
|
||||
Called when settings are updated
|
||||
"""
|
||||
# It covers case when project is not set
|
||||
# and we need to refresh appliance manager
|
||||
project = Topology.instance().project()
|
||||
if project is None:
|
||||
self._appliance_manager.instance().refresh()
|
||||
|
||||
def _refreshVisibleWidgets(self):
|
||||
"""
|
||||
Refresh widgets that should be visible or not
|
||||
@@ -611,7 +631,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
for item in self.uiGraphicsView.scene().items():
|
||||
if isinstance(item, LinkItem):
|
||||
item.adjust()
|
||||
|
||||
|
||||
def _updateZoomSettings(self, zoom=None):
|
||||
"""
|
||||
Updates zoom settings
|
||||
@@ -1050,6 +1070,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
:param event: QCloseEvent
|
||||
"""
|
||||
|
||||
if Topology.instance().project():
|
||||
reply = QtWidgets.QMessageBox.question(self, "Confirm Exit", "Are you sure you want to exit GNS3?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
event.ignore()
|
||||
return
|
||||
|
||||
progress = Progress.instance()
|
||||
progress.setAllowCancelQuery(True)
|
||||
progress.setCancelButtonText("Force quit")
|
||||
|
||||
@@ -19,9 +19,11 @@ from gns3.modules.builtin import Builtin
|
||||
from gns3.modules.dynamips import Dynamips
|
||||
from gns3.modules.iou import IOU
|
||||
from gns3.modules.vpcs import VPCS
|
||||
from gns3.modules.traceng import TraceNG
|
||||
from gns3.modules.virtualbox import VirtualBox
|
||||
from gns3.modules.qemu import Qemu
|
||||
from gns3.modules.vmware import VMware
|
||||
from gns3.modules.docker import Docker
|
||||
|
||||
#FIXME: removed TraceNG
|
||||
MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker]
|
||||
|
||||
@@ -58,18 +58,19 @@ class Cloud(Node):
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
def update(self, new_settings):
|
||||
def update(self, new_settings, force=False):
|
||||
"""
|
||||
Updates the settings for this cloud.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
:param force: force this node to update
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
if params or force:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
|
||||
@@ -50,6 +50,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiEthernetWarningPushButton.clicked.connect(self._EthernetWarningSlot)
|
||||
self.uiAddEthernetPushButton.clicked.connect(self._EthernetAddSlot)
|
||||
self.uiAddAllEthernetPushButton.clicked.connect(self._EthernetAddAllSlot)
|
||||
self.uiRefreshEthernetPushButton.clicked.connect(self._EthernetRefreshSlot)
|
||||
self.uiDeleteEthernetPushButton.clicked.connect(self._EthernetDeleteSlot)
|
||||
|
||||
# connect TAP slots
|
||||
@@ -57,6 +58,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiTAPListWidget.itemSelectionChanged.connect(self._TAPChangedSlot)
|
||||
self.uiAddTAPPushButton.clicked.connect(self._TAPAddSlot)
|
||||
self.uiAddAllTAPPushButton.clicked.connect(self._TAPAddAllSlot)
|
||||
self.uiRefreshTAPPushButton.clicked.connect(self._TAPRefreshSlot)
|
||||
self.uiDeleteTAPPushButton.clicked.connect(self._TAPDeleteSlot)
|
||||
|
||||
# connect UDP slots
|
||||
@@ -74,6 +76,19 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
icon = QtGui.QIcon(':/icons/dialog-warning.svg')
|
||||
self.uiEthernetWarningPushButton.setIcon(icon)
|
||||
|
||||
def _refreshInterfaces(self):
|
||||
"""
|
||||
Refresh the network interfaces.
|
||||
"""
|
||||
|
||||
if self._node:
|
||||
self._interfaces = self._node.interfaces()
|
||||
self._loadNetworkInterfaces(self._interfaces)
|
||||
try:
|
||||
self._node.updated_signal.disconnect(self._refreshInterfaces)
|
||||
except (TypeError, RuntimeError):
|
||||
pass # was not connected
|
||||
|
||||
def _EthernetChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
@@ -121,6 +136,15 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
interface = self.uiEthernetComboBox.itemText(index)
|
||||
self._EthernetAddSlot(interface)
|
||||
|
||||
def _EthernetRefreshSlot(self):
|
||||
"""
|
||||
Refresh all Ethernet interfaces.
|
||||
"""
|
||||
|
||||
if self._node:
|
||||
self._node.update({}, force=True)
|
||||
self._node.updated_signal.connect(self._refreshInterfaces)
|
||||
|
||||
def _EthernetDeleteSlot(self):
|
||||
"""
|
||||
Deletes the selected Ethernet interface.
|
||||
@@ -199,6 +223,15 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
interface = self.uiTAPComboBox.itemText(index)
|
||||
self._TAPAddSlot(interface)
|
||||
|
||||
def _TAPRefreshSlot(self):
|
||||
"""
|
||||
Refresh all TAP interfaces.
|
||||
"""
|
||||
|
||||
if self._node:
|
||||
self._node.update({}, force=True)
|
||||
self._node.updated_signal.connect(self._refreshInterfaces)
|
||||
|
||||
def _TAPDeleteSlot(self):
|
||||
"""
|
||||
Deletes a TAP interface.
|
||||
|
||||
@@ -223,8 +223,8 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
for port_info in settings["ports_mapping"]:
|
||||
item = TreeWidgetItem(self.uiPortsTreeWidget)
|
||||
item.setText(0, str(port_info["port_number"]))
|
||||
item.setText(1, str(port_info["vlan"]))
|
||||
item.setText(2, port_info["type"])
|
||||
item.setText(1, str(port_info.get("vlan", 1)))
|
||||
item.setText(2, port_info.get("type", "access"))
|
||||
item.setText(3, port_info.get("ethertype", ""))
|
||||
self.uiPortsTreeWidget.addTopLevelItem(item)
|
||||
self._ports[port_info["port_number"]] = port_info
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>821</width>
|
||||
<height>363</height>
|
||||
<width>1000</width>
|
||||
<height>378</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -57,7 +57,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<item row="0" column="5">
|
||||
<widget class="QPushButton" name="uiDeleteEthernetPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
@@ -67,7 +67,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="5">
|
||||
<item row="1" column="0" colspan="6">
|
||||
<widget class="QListWidget" name="uiEthernetListWidget">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
@@ -91,6 +91,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QPushButton" name="uiRefreshEthernetPushButton">
|
||||
<property name="text">
|
||||
<string>&Refresh</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<zorder>uiEthernetListWidget</zorder>
|
||||
<zorder>uiEthernetComboBox</zorder>
|
||||
@@ -99,13 +106,14 @@
|
||||
<zorder>uiAddAllEthernetPushButton</zorder>
|
||||
<zorder>uiShowSpecialInterfacesCheckBox</zorder>
|
||||
<zorder>uiEthernetWarningPushButton</zorder>
|
||||
<zorder>uiRefreshEthernetPushButton</zorder>
|
||||
</widget>
|
||||
<widget class="QWidget" name="TAPTab">
|
||||
<attribute name="title">
|
||||
<string>TAP interfaces</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="1" column="4">
|
||||
<item row="1" column="5">
|
||||
<widget class="QPushButton" name="uiDeleteTAPPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
@@ -115,7 +123,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="5">
|
||||
<item row="2" column="0" colspan="6">
|
||||
<widget class="QListWidget" name="uiTAPListWidget">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
@@ -142,7 +150,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="4">
|
||||
<item row="0" column="1" colspan="5">
|
||||
<widget class="QComboBox" name="uiTAPComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
@@ -165,6 +173,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="4">
|
||||
<widget class="QPushButton" name="uiRefreshTAPPushButton">
|
||||
<property name="text">
|
||||
<string>&Refresh</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="UDPTab">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.9
|
||||
# Created by: PyQt5 UI code generator 5.5.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -11,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_cloudConfigPageWidget(object):
|
||||
def setupUi(self, cloudConfigPageWidget):
|
||||
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
|
||||
cloudConfigPageWidget.resize(821, 363)
|
||||
cloudConfigPageWidget.resize(1000, 378)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
|
||||
@@ -39,11 +39,11 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiDeleteEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiDeleteEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteEthernetPushButton.setObjectName("uiDeleteEthernetPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 4, 1, 1)
|
||||
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 5, 1, 1)
|
||||
self.uiEthernetListWidget = QtWidgets.QListWidget(self.EthernetTab)
|
||||
self.uiEthernetListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.uiEthernetListWidget.setObjectName("uiEthernetListWidget")
|
||||
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 5)
|
||||
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 6)
|
||||
self.uiEthernetWarningPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiEthernetWarningPushButton.setText("")
|
||||
self.uiEthernetWarningPushButton.setObjectName("uiEthernetWarningPushButton")
|
||||
@@ -51,6 +51,9 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiShowSpecialInterfacesCheckBox = QtWidgets.QCheckBox(self.EthernetTab)
|
||||
self.uiShowSpecialInterfacesCheckBox.setObjectName("uiShowSpecialInterfacesCheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiShowSpecialInterfacesCheckBox, 2, 0, 1, 2)
|
||||
self.uiRefreshEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiRefreshEthernetPushButton.setObjectName("uiRefreshEthernetPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiRefreshEthernetPushButton, 0, 4, 1, 1)
|
||||
self.uiEthernetListWidget.raise_()
|
||||
self.uiEthernetComboBox.raise_()
|
||||
self.uiAddEthernetPushButton.raise_()
|
||||
@@ -58,6 +61,7 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiAddAllEthernetPushButton.raise_()
|
||||
self.uiShowSpecialInterfacesCheckBox.raise_()
|
||||
self.uiEthernetWarningPushButton.raise_()
|
||||
self.uiRefreshEthernetPushButton.raise_()
|
||||
self.uiTabWidget.addTab(self.EthernetTab, "")
|
||||
self.TAPTab = QtWidgets.QWidget()
|
||||
self.TAPTab.setObjectName("TAPTab")
|
||||
@@ -66,11 +70,11 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiDeleteTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
|
||||
self.uiDeleteTAPPushButton.setEnabled(False)
|
||||
self.uiDeleteTAPPushButton.setObjectName("uiDeleteTAPPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiDeleteTAPPushButton, 1, 4, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.uiDeleteTAPPushButton, 1, 5, 1, 1)
|
||||
self.uiTAPListWidget = QtWidgets.QListWidget(self.TAPTab)
|
||||
self.uiTAPListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.uiTAPListWidget.setObjectName("uiTAPListWidget")
|
||||
self.gridLayout_2.addWidget(self.uiTAPListWidget, 2, 0, 1, 5)
|
||||
self.gridLayout_2.addWidget(self.uiTAPListWidget, 2, 0, 1, 6)
|
||||
self.uiTAPLineEdit = QtWidgets.QLineEdit(self.TAPTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
@@ -91,10 +95,13 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiTAPComboBox.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
|
||||
self.uiTAPComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
self.uiTAPComboBox.setObjectName("uiTAPComboBox")
|
||||
self.gridLayout_2.addWidget(self.uiTAPComboBox, 0, 1, 1, 4)
|
||||
self.gridLayout_2.addWidget(self.uiTAPComboBox, 0, 1, 1, 5)
|
||||
self.uiAddAllTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
|
||||
self.uiAddAllTAPPushButton.setObjectName("uiAddAllTAPPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiAddAllTAPPushButton, 1, 3, 1, 1)
|
||||
self.uiRefreshTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
|
||||
self.uiRefreshTAPPushButton.setObjectName("uiRefreshTAPPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiRefreshTAPPushButton, 1, 4, 1, 1)
|
||||
self.uiTabWidget.addTab(self.TAPTab, "")
|
||||
self.UDPTab = QtWidgets.QWidget()
|
||||
self.UDPTab.setObjectName("UDPTab")
|
||||
@@ -241,11 +248,13 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiDeleteEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiEthernetListWidget.setSortingEnabled(True)
|
||||
self.uiShowSpecialInterfacesCheckBox.setText(_translate("cloudConfigPageWidget", "&Show special Ethernet interfaces"))
|
||||
self.uiRefreshEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Refresh"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.EthernetTab), _translate("cloudConfigPageWidget", "Ethernet interfaces"))
|
||||
self.uiDeleteTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiTAPListWidget.setSortingEnabled(True)
|
||||
self.uiAddTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiAddAllTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
|
||||
self.uiRefreshTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Refresh"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.TAPTab), _translate("cloudConfigPageWidget", "TAP interfaces"))
|
||||
self.uiUDPTunnelSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "UDP tunnel settings"))
|
||||
self.uiRemoteHostLineEdit.setText(_translate("cloudConfigPageWidget", "127.0.0.1"))
|
||||
|
||||
@@ -135,6 +135,6 @@ class DockerVMWizard(VMWizard, Ui_DockerVMWizard):
|
||||
"name": name,
|
||||
"environment": self.uiEnvironmentTextEdit.toPlainText(),
|
||||
"start_command": start_command,
|
||||
"console_type": self.uiConsoleTypeComboBox.currentText()
|
||||
"console_type": self.uiConsoleTypeComboBox.currentText(),
|
||||
}
|
||||
return settings
|
||||
|
||||
@@ -49,7 +49,8 @@ class DockerVM(Node):
|
||||
"console_type": DOCKER_CONTAINER_SETTINGS["console_type"],
|
||||
"console_resolution": DOCKER_CONTAINER_SETTINGS["console_resolution"],
|
||||
"console_http_port": DOCKER_CONTAINER_SETTINGS["console_http_port"],
|
||||
"console_http_path": DOCKER_CONTAINER_SETTINGS["console_http_path"]}
|
||||
"console_http_path": DOCKER_CONTAINER_SETTINGS["console_http_path"],
|
||||
"extra_hosts": DOCKER_CONTAINER_SETTINGS["extra_hosts"]}
|
||||
|
||||
self.settings().update(docker_vm_settings)
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
|
||||
self.uiConsoleResolutionComboBox.setCurrentIndex(self.uiConsoleResolutionComboBox.findText(settings["console_resolution"]))
|
||||
self.uiConsoleHttpPortSpinBox.setValue(settings["console_http_port"])
|
||||
self.uiHttpConsolePathLineEdit.setText(settings["console_http_path"])
|
||||
self.uiExtraHostsTextEdit.setText(settings["extra_hosts"])
|
||||
|
||||
if not group:
|
||||
self.uiNameLineEdit.setText(settings["name"])
|
||||
@@ -128,6 +129,7 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
|
||||
settings["console_resolution"] = self.uiConsoleResolutionComboBox.currentText()
|
||||
settings["console_http_port"] = self.uiConsoleHttpPortSpinBox.value()
|
||||
settings["console_http_path"] = self.uiHttpConsolePathLineEdit.text()
|
||||
settings["extra_hosts"] = self.uiExtraHostsTextEdit.toPlainText()
|
||||
|
||||
if not group:
|
||||
adapters = self.uiAdapterSpinBox.value()
|
||||
|
||||
@@ -61,7 +61,6 @@ class DockerVMPreferencesPage(QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidge
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, docker_image):
|
||||
|
||||
self.uiDockerVMInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
@@ -79,6 +78,9 @@ class DockerVMPreferencesPage(QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidge
|
||||
if docker_image["environment"]:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Environment:", str(docker_image["environment"])])
|
||||
|
||||
if docker_image["extra_hosts"]:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Extra hosts:", str(docker_image["extra_hosts"])])
|
||||
|
||||
self.uiDockerVMInfoTreeWidget.expandAll()
|
||||
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(1)
|
||||
|
||||
@@ -38,5 +38,6 @@ DOCKER_CONTAINER_SETTINGS = {
|
||||
"console_type": "telnet",
|
||||
"console_resolution": "1024x768",
|
||||
"console_http_port": 80,
|
||||
"console_http_path": "/"
|
||||
"console_http_path": "/",
|
||||
"extra_hosts": ""
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>613</width>
|
||||
<height>519</height>
|
||||
<height>524</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -24,7 +24,16 @@
|
||||
<string>General settings</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item row="0" column="1">
|
||||
@@ -263,6 +272,45 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="advancedTab">
|
||||
<attribute name="title">
|
||||
<string>Advanced</string>
|
||||
</attribute>
|
||||
<widget class="QLabel" name="uiExtraHostsLabel">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>152</width>
|
||||
<height>82</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Extra hosts added to
|
||||
/etc/hosts file.
|
||||
(hostname:IP, one per line)</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="uiExtraHostsTextEdit">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>168</x>
|
||||
<y>10</y>
|
||||
<width>413</width>
|
||||
<height>82</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/docker/ui/docker_vm_configuration_page.ui'
|
||||
# Form implementation generated from reading ui file '/home/dominik/projects/gns3-gui/gns3/modules/docker/ui/docker_vm_configuration_page.ui'
|
||||
#
|
||||
# Created: Thu Jan 5 14:49:45 2017
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
# Created by: PyQt5 UI code generator 5.8.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_dockerVMConfigPageWidget(object):
|
||||
def setupUi(self, dockerVMConfigPageWidget):
|
||||
dockerVMConfigPageWidget.setObjectName("dockerVMConfigPageWidget")
|
||||
dockerVMConfigPageWidget.resize(613, 519)
|
||||
dockerVMConfigPageWidget.resize(613, 524)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(dockerVMConfigPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(dockerVMConfigPageWidget)
|
||||
@@ -122,6 +121,21 @@ class Ui_dockerVMConfigPageWidget(object):
|
||||
self.uiHttpConsolePathLineEdit.setObjectName("uiHttpConsolePathLineEdit")
|
||||
self.gridLayout.addWidget(self.uiHttpConsolePathLineEdit, 9, 1, 1, 1)
|
||||
self.uiTabWidget.addTab(self.tab, "")
|
||||
self.advancedTab = QtWidgets.QWidget()
|
||||
self.advancedTab.setObjectName("advancedTab")
|
||||
self.uiExtraHostsLabel = QtWidgets.QLabel(self.advancedTab)
|
||||
self.uiExtraHostsLabel.setGeometry(QtCore.QRect(10, 10, 152, 82))
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiExtraHostsLabel.sizePolicy().hasHeightForWidth())
|
||||
self.uiExtraHostsLabel.setSizePolicy(sizePolicy)
|
||||
self.uiExtraHostsLabel.setWordWrap(True)
|
||||
self.uiExtraHostsLabel.setObjectName("uiExtraHostsLabel")
|
||||
self.uiExtraHostsTextEdit = QtWidgets.QTextEdit(self.advancedTab)
|
||||
self.uiExtraHostsTextEdit.setGeometry(QtCore.QRect(168, 10, 413, 82))
|
||||
self.uiExtraHostsTextEdit.setObjectName("uiExtraHostsTextEdit")
|
||||
self.uiTabWidget.addTab(self.advancedTab, "")
|
||||
self.verticalLayout.addWidget(self.uiTabWidget)
|
||||
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout.addItem(spacerItem)
|
||||
@@ -164,4 +178,8 @@ class Ui_dockerVMConfigPageWidget(object):
|
||||
self.uiConsoleResolutionLabel.setText(_translate("dockerVMConfigPageWidget", "VNC console resolution:"))
|
||||
self.label_2.setText(_translate("dockerVMConfigPageWidget", "HTTP path:"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab), _translate("dockerVMConfigPageWidget", "General settings"))
|
||||
self.uiExtraHostsLabel.setText(_translate("dockerVMConfigPageWidget", "Extra hosts added to \n"
|
||||
"/etc/hosts file.\n"
|
||||
"(hostname:IP, one per line)"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.advancedTab), _translate("dockerVMConfigPageWidget", "Advanced"))
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ from gns3.node import Node
|
||||
from ..ui.ios_router_configuration_page_ui import Ui_iosRouterConfigPageWidget
|
||||
from ..settings import CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
|
||||
@@ -323,14 +326,12 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
|
||||
self.uiNPEComboBox.clear()
|
||||
self.uiNPEComboBox.addItems(["npe-100", "npe-150", "npe-175", "npe-200", "npe-225", "npe-300", "npe-400", "npe-g2"])
|
||||
|
||||
if settings["midplane"]:
|
||||
index = self.uiMidplaneComboBox.findText(settings["midplane"])
|
||||
if index != -1:
|
||||
self.uiMidplaneComboBox.setCurrentIndex(index)
|
||||
if settings["npe"]:
|
||||
index = self.uiNPEComboBox.findText(settings["npe"])
|
||||
if index != -1:
|
||||
self.uiNPEComboBox.setCurrentIndex(index)
|
||||
index = self.uiMidplaneComboBox.findText(settings.get("midplane", "vxr"))
|
||||
if index != -1:
|
||||
self.uiMidplaneComboBox.setCurrentIndex(index)
|
||||
index = self.uiNPEComboBox.findText(settings.get("npe", "npe-400"))
|
||||
if index != -1:
|
||||
self.uiNPEComboBox.setCurrentIndex(index)
|
||||
|
||||
if node:
|
||||
# load the sensor settings
|
||||
@@ -513,7 +514,7 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
|
||||
if self._configFileValid(startup_config):
|
||||
settings["startup_config"] = startup_config
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Startup-config", "Cannot read the startup-config file")
|
||||
QtWidgets.QMessageBox.critical(self, "Startup-config", "Cannot access or read the startup-config file")
|
||||
|
||||
private_config = self.uiPrivateConfigLineEdit.text().strip()
|
||||
if not private_config:
|
||||
@@ -522,7 +523,7 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
|
||||
if self._configFileValid(private_config):
|
||||
settings["private_config"] = private_config
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Private-config", "Cannot read the private-config file")
|
||||
QtWidgets.QMessageBox.critical(self, "Private-config", "Cannot access or read the private-config file")
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
settings["symbol"] = symbol_path
|
||||
@@ -620,7 +621,7 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
|
||||
if node:
|
||||
settings["wic" + str(wic_number)] = node.settings().get("wic" + str(wic_number))
|
||||
|
||||
if settings["wic" + str(wic_number)] and settings["wic" + str(wic_number)] != wic_name:
|
||||
if settings.get("wic" + str(wic_number)) and settings["wic" + str(wic_number)] != wic_name:
|
||||
if node:
|
||||
self._checkForLinkConnectedToWIC(wic_number, settings, node)
|
||||
settings["wic" + str(wic_number)] = wic_name
|
||||
@@ -634,6 +635,13 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
|
||||
"""
|
||||
Return true if it's a valid configuration file
|
||||
"""
|
||||
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(LocalServer.instance().localServerSettings()["configs_path"], path)
|
||||
return os.access(path, os.R_OK)
|
||||
result = os.access(path, os.R_OK)
|
||||
if not result:
|
||||
if not os.path.exists(path):
|
||||
log.error("Cannot access config file '{}'".format(path))
|
||||
else:
|
||||
log.error("Cannot read config file '{}'".format(path))
|
||||
return result
|
||||
|
||||
@@ -294,8 +294,10 @@ class IOSRouterPreferencesPage(QtWidgets.QWidget, Ui_IOSRouterPreferencesPageWid
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
ios_router = self._ios_routers[key]
|
||||
path = ios_router["image"]
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(self.getImageDirectory(), path)
|
||||
if not os.path.isfile(path):
|
||||
QtWidgets.QMessageBox.critical(self, "IOS image", "IOS image file {} is does not exist".format(path))
|
||||
QtWidgets.QMessageBox.critical(self, "IOS image", "IOS image file {} does not exist".format(path))
|
||||
return
|
||||
try:
|
||||
if not isIOSCompressed(path):
|
||||
|
||||
@@ -77,7 +77,7 @@ class IOUDevice(Node):
|
||||
return
|
||||
|
||||
log.debug("{} is starting".format(self.name()))
|
||||
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, progressText="{} is starting".format(self.name()))
|
||||
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, showProgress=False)
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
|
||||
@@ -30,6 +30,9 @@ from gns3.controller import Controller
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from ..ui.iou_device_configuration_page_ui import Ui_iouDeviceConfigPageWidget
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget):
|
||||
|
||||
@@ -262,7 +265,7 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
|
||||
if self._configFileValid(startup_config):
|
||||
settings["startup_config"] = startup_config
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Startup-config", "Cannot read the startup-config file")
|
||||
QtWidgets.QMessageBox.critical(self, "Startup-config", "Cannot access or read the startup-config file")
|
||||
|
||||
# save the private-config
|
||||
private_config = self.uiPrivateConfigLineEdit.text().strip()
|
||||
@@ -272,7 +275,7 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
|
||||
if self._configFileValid(private_config):
|
||||
settings["private_config"] = private_config
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Private-config", "Cannot read the private-config file")
|
||||
QtWidgets.QMessageBox.critical(self, "Private-config", "Cannot access or read the private-config file")
|
||||
|
||||
settings["symbol"] = self.uiSymbolLineEdit.text()
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
@@ -306,6 +309,13 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
|
||||
"""
|
||||
Return true if it's a valid configuration file
|
||||
"""
|
||||
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(LocalServer.instance().localServerSettings()["configs_path"], path)
|
||||
return os.access(path, os.R_OK)
|
||||
result = os.access(path, os.R_OK)
|
||||
if not result:
|
||||
if not os.path.exists(path):
|
||||
log.error("Cannot access config file '{}'".format(path))
|
||||
else:
|
||||
log.error("Cannot read config file '{}'".format(path))
|
||||
return result
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>569</width>
|
||||
<height>503</height>
|
||||
<width>767</width>
|
||||
<height>685</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -181,7 +181,7 @@
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Enable layer 1 keepalive messages (testing only)</string>
|
||||
<string>Enable layer 1 keepalive messages (non-functional)</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/iou/ui/iou_device_configuration_page.ui'
|
||||
#
|
||||
# Created: Thu Jan 5 14:49:45 2017
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
# Created by: PyQt5 UI code generator 5.5.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_iouDeviceConfigPageWidget(object):
|
||||
def setupUi(self, iouDeviceConfigPageWidget):
|
||||
iouDeviceConfigPageWidget.setObjectName("iouDeviceConfigPageWidget")
|
||||
iouDeviceConfigPageWidget.resize(569, 503)
|
||||
iouDeviceConfigPageWidget.resize(767, 685)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(iouDeviceConfigPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(iouDeviceConfigPageWidget)
|
||||
@@ -209,7 +208,7 @@ class Ui_iouDeviceConfigPageWidget(object):
|
||||
self.uiPrivateConfigToolButton.setText(_translate("iouDeviceConfigPageWidget", "&Browse..."))
|
||||
self.uiDefaultNameFormatLabel.setText(_translate("iouDeviceConfigPageWidget", "Default name format:"))
|
||||
self.uiOtherSettingsGroupBox.setTitle(_translate("iouDeviceConfigPageWidget", "Other settings"))
|
||||
self.uiL1KeepalivesCheckBox.setText(_translate("iouDeviceConfigPageWidget", "Enable layer 1 keepalive messages (testing only)"))
|
||||
self.uiL1KeepalivesCheckBox.setText(_translate("iouDeviceConfigPageWidget", "Enable layer 1 keepalive messages (non-functional)"))
|
||||
self.uiDefaultValuesCheckBox.setText(_translate("iouDeviceConfigPageWidget", "Use default IOU values for memories"))
|
||||
self.uiRamLabel.setText(_translate("iouDeviceConfigPageWidget", "RAM size:"))
|
||||
self.uiRamSpinBox.setSuffix(_translate("iouDeviceConfigPageWidget", " MB"))
|
||||
|
||||
@@ -78,9 +78,14 @@ class Module(QtCore.QObject):
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
node_names_cannot_export = []
|
||||
for node in self._nodes:
|
||||
if hasattr(node, "initialized") and node.initialized():
|
||||
node.exportConfigToDirectory(directory)
|
||||
if not node.exportConfigToDirectory(directory):
|
||||
node_names_cannot_export.append(node.name())
|
||||
|
||||
if node_names_cannot_export:
|
||||
log.warning("Config export is not supported by the following nodes: {}".format(" ".join(node_names_cannot_export)))
|
||||
|
||||
def importConfigs(self, directory):
|
||||
"""
|
||||
|
||||
@@ -25,9 +25,9 @@ import re
|
||||
from collections import OrderedDict
|
||||
from gns3.modules.qemu.dialogs.qemu_image_wizard import QemuImageWizard
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.ports.port_name_factory import StandardPortNameFactory
|
||||
from gns3.node import Node
|
||||
from gns3.qt import QtCore, QtWidgets, qpartial
|
||||
from gns3.modules.module_error import ModuleError
|
||||
from gns3.qt import QtCore, QtWidgets, qpartial, sip_is_deleted
|
||||
from gns3.dialogs.node_properties_dialog import ConfigurationError
|
||||
from gns3.image_manager import ImageManager
|
||||
|
||||
@@ -54,6 +54,10 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
self.uiBootPriorityComboBox.addItem("Network", "n")
|
||||
self.uiBootPriorityComboBox.addItem("HDD or Network", "cn")
|
||||
self.uiBootPriorityComboBox.addItem("HDD or CD/DVD-ROM", "cd")
|
||||
self.uiBootPriorityComboBox.addItem("CD/DVD-ROM or Network", "dn")
|
||||
self.uiBootPriorityComboBox.addItem("CD/DVD-ROM or HDD", "dc")
|
||||
self.uiBootPriorityComboBox.addItem("Network or HDD", "nc")
|
||||
self.uiBootPriorityComboBox.addItem("Network or CD/DVD-ROM", "nd")
|
||||
|
||||
self.uiHdaDiskImageToolButton.clicked.connect(self._hdaDiskImageBrowserSlot)
|
||||
self.uiHdbDiskImageToolButton.clicked.connect(self._hdbDiskImageBrowserSlot)
|
||||
@@ -264,6 +268,9 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if sip_is_deleted(self.uiQemuListComboBox) or sip_is_deleted(self):
|
||||
return
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu binaries", "{}".format(result["message"]))
|
||||
else:
|
||||
@@ -281,8 +288,12 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
if index != -1:
|
||||
self.uiQemuListComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu", "Could not find {} in the Qemu binaries list".format(qemu_path))
|
||||
self.uiQemuListComboBox.clear()
|
||||
index = self.uiQemuListComboBox.findData("{path}".format(path=os.path.basename(qemu_path)), flags=QtCore.Qt.MatchEndsWith)
|
||||
self.uiQemuListComboBox.setCurrentIndex(index)
|
||||
if index == -1:
|
||||
QtWidgets.QMessageBox.warning(self, "Qemu","Could not find '{}' in the Qemu binaries list, please select a new binary".format(qemu_path))
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "Qemu","Could not find '{}' in the Qemu binaries list, an alternative path has been selected".format(qemu_path))
|
||||
|
||||
def _cpuThrottlingChangedSlot(self, state):
|
||||
"""
|
||||
@@ -322,11 +333,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
QtWidgets.QMessageBox.warning(self, "Qemu", "Server {} is not running, cannot retrieve the QEMU binaries list".format(settings["server"]))
|
||||
else:
|
||||
callback = qpartial(self._getQemuBinariesFromServerCallback, qemu_path=settings["qemu_path"])
|
||||
try:
|
||||
Qemu.instance().getQemuBinariesFromServer(self._compute_id, callback)
|
||||
except ModuleError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu", "Error while getting the QEMU binaries list: {}".format(e))
|
||||
self.uiQemuListComboBox.clear()
|
||||
Qemu.instance().getQemuBinariesFromServer(self._compute_id, callback)
|
||||
|
||||
if not group:
|
||||
# set the device name
|
||||
@@ -497,25 +504,29 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
settings["symbol"] = symbol_path
|
||||
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
|
||||
port_name_format = self.uiPortNameFormatLineEdit.text()
|
||||
if '{0}' not in port_name_format and '{port0}' not in port_name_format and '{port1}' not in port_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Port name format", "The format must contain at least {0}, {port0} or {port1}")
|
||||
else:
|
||||
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
|
||||
|
||||
port_segment_size = self.uiPortSegmentSizeSpinBox.value()
|
||||
if port_segment_size and '{1}' not in port_name_format and '{segment0}' not in port_name_format and '{segment1}' not in port_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Port name format", "If the segment size is not 0, the format must contain {1}, {segment0} or {segment1}")
|
||||
else:
|
||||
settings["port_segment_size"] = port_segment_size
|
||||
first_port_name = self.uiFirstPortNameLineEdit.text().strip()
|
||||
|
||||
settings["first_port_name"] = self.uiFirstPortNameLineEdit.text().strip()
|
||||
try:
|
||||
StandardPortNameFactory(self.uiAdaptersSpinBox.value(), first_port_name, port_name_format, port_segment_size)
|
||||
except (IndexError, ValueError, KeyError):
|
||||
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
|
||||
raise ConfigurationError()
|
||||
|
||||
if self.uiQemuListComboBox.count():
|
||||
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
|
||||
settings["port_segment_size"] = port_segment_size
|
||||
settings["first_port_name"] = first_port_name
|
||||
|
||||
if self.uiQemuListComboBox.currentIndex() != -1:
|
||||
qemu_path = self.uiQemuListComboBox.itemData(self.uiQemuListComboBox.currentIndex())
|
||||
settings["qemu_path"] = qemu_path
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu binary", "Please select a Qemu binary")
|
||||
if node:
|
||||
raise ConfigurationError()
|
||||
|
||||
settings["boot_priority"] = self.uiBootPriorityComboBox.itemData(self.uiBootPriorityComboBox.currentIndex())
|
||||
settings["console_type"] = self.uiConsoleTypeComboBox.currentText().lower()
|
||||
|
||||
249
gns3/modules/traceng/__init__.py
Normal file
249
gns3/modules/traceng/__init__.py
Normal file
@@ -0,0 +1,249 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
TraceNG module implementation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import copy
|
||||
import shutil
|
||||
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
|
||||
from ..module import Module
|
||||
from .traceng_node import TraceNGNode
|
||||
from .settings import TRACENG_SETTINGS
|
||||
from .settings import TRACENG_NODES_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TraceNG(Module):
|
||||
|
||||
"""
|
||||
TraceNG module.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._settings = {}
|
||||
self._nodes = []
|
||||
self._traceng_nodes = {}
|
||||
self._working_dir = ""
|
||||
self._loadSettings()
|
||||
|
||||
def configChangedSlot(self):
|
||||
# load the settings
|
||||
self._loadSettings()
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
Loads the settings from the persistent settings file.
|
||||
"""
|
||||
|
||||
self._settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, TRACENG_SETTINGS)
|
||||
if not os.path.exists(self._settings["traceng_path"]):
|
||||
traceng_path = shutil.which("traceng")
|
||||
if traceng_path:
|
||||
self._settings["traceng_path"] = os.path.abspath(traceng_path)
|
||||
else:
|
||||
self._settings["traceng_path"] = ""
|
||||
|
||||
self._loadTraceNGNodes()
|
||||
|
||||
def _saveSettings(self):
|
||||
"""
|
||||
Saves the settings to the persistent settings file.
|
||||
"""
|
||||
|
||||
# save the settings
|
||||
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
|
||||
|
||||
server_settings = {}
|
||||
if self._settings["traceng_path"]:
|
||||
# save some settings to the server config file
|
||||
server_settings["traceng_path"] = os.path.normpath(self._settings["traceng_path"])
|
||||
|
||||
config = LocalServerConfig.instance()
|
||||
config.saveSettings(self.__class__.__name__, server_settings)
|
||||
|
||||
def _loadTraceNGNodes(self):
|
||||
"""
|
||||
Load the TraceNG nodes from the persistent settings file.
|
||||
"""
|
||||
|
||||
self._traceng_nodes = {}
|
||||
settings = LocalConfig.instance().settings()
|
||||
if "nodes" in settings.get(self.__class__.__name__, {}):
|
||||
for node in settings[self.__class__.__name__]["nodes"]:
|
||||
name = node.get("name")
|
||||
server = node.get("server")
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
if key in self._traceng_nodes or not name or not server:
|
||||
continue
|
||||
node_settings = TRACENG_NODES_SETTINGS.copy()
|
||||
node_settings.update(node)
|
||||
self._traceng_nodes[key] = node_settings
|
||||
|
||||
def _saveTraceNGNodes(self):
|
||||
"""
|
||||
Saves the TraceNG nodes to the persistent settings file.
|
||||
"""
|
||||
|
||||
self._settings["nodes"] = list(self._traceng_nodes.values())
|
||||
self._saveSettings()
|
||||
|
||||
def addNode(self, node):
|
||||
"""
|
||||
Adds a node to this module.
|
||||
|
||||
:param node: Node instance
|
||||
"""
|
||||
|
||||
self._nodes.append(node)
|
||||
|
||||
def removeNode(self, node):
|
||||
"""
|
||||
Removes a node from this module.
|
||||
|
||||
:param node: Node instance
|
||||
"""
|
||||
|
||||
if node in self._nodes:
|
||||
self._nodes.remove(node)
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns the module settings
|
||||
|
||||
:returns: module settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._settings
|
||||
|
||||
def setSettings(self, settings):
|
||||
"""
|
||||
Sets the module settings
|
||||
|
||||
:param settings: module settings (dictionary)
|
||||
"""
|
||||
|
||||
self._settings.update(settings)
|
||||
self._saveSettings()
|
||||
|
||||
def instantiateNode(self, node_class, server, project):
|
||||
"""
|
||||
Instantiate a new node.
|
||||
|
||||
:param node_class: Node object
|
||||
:param server: HTTPClient instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the module.
|
||||
"""
|
||||
|
||||
self._nodes.clear()
|
||||
|
||||
@staticmethod
|
||||
def getNodeType(name, platform=None):
|
||||
if name == "traceng":
|
||||
return TraceNGNode
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def vmConfigurationPage():
|
||||
from .pages.traceng_node_configuration_page import TraceNGNodeConfigurationPage
|
||||
return TraceNGNodeConfigurationPage
|
||||
|
||||
def VMs(self):
|
||||
"""
|
||||
Returns list of TraceNG nodes
|
||||
"""
|
||||
|
||||
return self._traceng_nodes
|
||||
|
||||
def setVMs(self, new_traceng_nodes):
|
||||
"""
|
||||
Sets TraceNG list
|
||||
|
||||
:param new_traceng_vms: TraceNG node list
|
||||
"""
|
||||
|
||||
self._traceng_nodes = new_traceng_nodes.copy()
|
||||
self._saveTraceNGNodes()
|
||||
|
||||
@staticmethod
|
||||
def classes():
|
||||
"""
|
||||
Returns all the node classes supported by this module.
|
||||
|
||||
:returns: list of classes
|
||||
"""
|
||||
|
||||
return [TraceNGNode]
|
||||
|
||||
def nodes(self):
|
||||
"""
|
||||
Returns all the node data necessary to represent a node
|
||||
in the nodes view and create a node on the scene.
|
||||
"""
|
||||
|
||||
nodes = []
|
||||
|
||||
# Add a default TraceNG not linked to a specific server
|
||||
nodes.append(
|
||||
{
|
||||
"class": TraceNGNode.__name__,
|
||||
"name": "TraceNG",
|
||||
"categories": [TraceNGNode.end_devices],
|
||||
"symbol": TraceNGNode.defaultSymbol(),
|
||||
"builtin": True
|
||||
}
|
||||
)
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
def preferencePages():
|
||||
"""
|
||||
:returns: QWidget object list
|
||||
"""
|
||||
|
||||
from .pages.traceng_preferences_page import TraceNGPreferencesPage
|
||||
from .pages.traceng_node_preferences_page import TraceNGNodePreferencesPage
|
||||
return [TraceNGPreferencesPage, TraceNGNodePreferencesPage]
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of TraceNG module.
|
||||
|
||||
:returns: instance of TraceNG
|
||||
"""
|
||||
|
||||
if not hasattr(TraceNG, "_instance"):
|
||||
TraceNG._instance = TraceNG()
|
||||
return TraceNG._instance
|
||||
0
gns3/modules/traceng/dialogs/__init__.py
Normal file
0
gns3/modules/traceng/dialogs/__init__.py
Normal file
86
gns3/modules/traceng/dialogs/traceng_node_wizard.py
Normal file
86
gns3/modules/traceng/dialogs/traceng_node_wizard.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Wizard for TraceNG nodes.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import ipaddress
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.node import Node
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.traceng_node_wizard_ui import Ui_TraceNGNodeWizard
|
||||
|
||||
|
||||
class TraceNGNodeWizard(VMWizard, Ui_TraceNGNodeWizard):
|
||||
|
||||
"""
|
||||
Wizard to create a TraceNG node template.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, traceng_nodes, parent):
|
||||
|
||||
super().__init__(traceng_nodes, parent)
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/traceng.png"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
|
||||
# TraceNG is only supported on a local server
|
||||
self.uiRemoteRadioButton.setEnabled(False)
|
||||
self.uiVMRadioButton.setEnabled(False)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the server.
|
||||
"""
|
||||
|
||||
if super().validateCurrentPage() is False:
|
||||
return False
|
||||
|
||||
if self.currentPage() == self.uiNameWizardPage:
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
QtWidgets.QMessageBox.critical(self, "TraceNG", "TraceNG can only run on Windows with a local server")
|
||||
return False
|
||||
|
||||
ip_address = self.uiIPAddressLineEdit.text()
|
||||
if ip_address:
|
||||
try:
|
||||
ipaddress.IPv4Address(ip_address)
|
||||
except ipaddress.AddressValueError:
|
||||
QtWidgets.QMessageBox.critical(self, "IP address", "Invalid IP address format")
|
||||
return False
|
||||
return True
|
||||
|
||||
def getSettings(self):
|
||||
"""
|
||||
Returns the settings set in this Wizard.
|
||||
|
||||
:return: settings dict
|
||||
"""
|
||||
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"ip_address": self.uiIPAddressLineEdit.text(),
|
||||
"symbol": ":/symbols/traceng.svg",
|
||||
"category": Node.end_devices,
|
||||
"server": self._compute_id}
|
||||
|
||||
return settings
|
||||
0
gns3/modules/traceng/pages/__init__.py
Normal file
0
gns3/modules/traceng/pages/__init__.py
Normal file
155
gns3/modules/traceng/pages/traceng_node_configuration_page.py
Normal file
155
gns3/modules/traceng/pages/traceng_node_configuration_page.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for TraceNG nodes
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.node import Node
|
||||
from gns3.controller import Controller
|
||||
|
||||
from ..ui.traceng_node_configuration_page_ui import Ui_TraceNGNodeConfigPageWidget
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.dialogs.node_properties_dialog import ConfigurationError
|
||||
|
||||
class TraceNGNodeConfigurationPage(QtWidgets.QWidget, Ui_TraceNGNodeConfigPageWidget):
|
||||
|
||||
"""
|
||||
QWidget configuration page for TraceNG nodes.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
self._default_configs_dir = LocalServer.instance().localServerSettings()["configs_path"]
|
||||
if Controller.instance().isRemote():
|
||||
self.uiScriptFileToolButton.hide()
|
||||
|
||||
# add the categories
|
||||
for name, category in Node.defaultCategories().items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
|
||||
def _symbolBrowserSlot(self):
|
||||
"""
|
||||
Slot to open the symbol browser and select a new symbol.
|
||||
"""
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
new_symbol_path = dialog.getSymbol()
|
||||
self.uiSymbolLineEdit.setText(new_symbol_path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
|
||||
|
||||
def loadSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Loads the TraceNG node settings.
|
||||
|
||||
:param settings: the settings (dictionary)
|
||||
:param node: Node instance
|
||||
:param group: indicates the settings apply to a group of routers
|
||||
"""
|
||||
|
||||
if not group:
|
||||
self.uiNameLineEdit.setText(settings["name"])
|
||||
self.uiIPAddressLineEdit.setText(settings["ip_address"])
|
||||
self.uiDefaultDestinationLineEdit.setText(settings["default_destination"])
|
||||
else:
|
||||
self.uiIPAddressLabel.hide()
|
||||
self.uiIPAddressLineEdit.hide()
|
||||
self.uiDefaultDestinationLabel.hide()
|
||||
self.uiDefaultDestinationLineEdit.hide()
|
||||
self.uiNameLabel.hide()
|
||||
self.uiNameLineEdit.hide()
|
||||
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# rename the label from "Name" to "Template name"
|
||||
self.uiNameLabel.setText("Template name:")
|
||||
|
||||
# load the default name format
|
||||
self.uiDefaultNameFormatLineEdit.setText(settings["default_name_format"])
|
||||
|
||||
# load the symbol
|
||||
self.uiSymbolLineEdit.setText(settings["symbol"])
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(settings["symbol"]))
|
||||
|
||||
# load the category
|
||||
index = self.uiCategoryComboBox.findData(settings["category"])
|
||||
if index != -1:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
self.uiDefaultNameFormatLabel.hide()
|
||||
self.uiDefaultNameFormatLineEdit.hide()
|
||||
self.uiSymbolLabel.hide()
|
||||
self.uiSymbolLineEdit.hide()
|
||||
self.uiSymbolToolButton.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Saves the TraceNG node settings.
|
||||
|
||||
:param settings: the settings (dictionary)
|
||||
:param node: Node instance
|
||||
:param group: indicates the settings apply to a group of routers
|
||||
"""
|
||||
|
||||
# these settings cannot be shared by nodes and updated
|
||||
# in the node properties dialog.
|
||||
if not group:
|
||||
# set the node name
|
||||
name = self.uiNameLineEdit.text()
|
||||
if not name:
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "TraceNG node name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
|
||||
ip_address = self.uiIPAddressLineEdit.text().strip()
|
||||
if ip_address:
|
||||
try:
|
||||
ipaddress.IPv4Address(ip_address)
|
||||
settings["ip_address"] = ip_address
|
||||
except ipaddress.AddressValueError:
|
||||
QtWidgets.QMessageBox.critical(self, "IP address", "Invalid IP address format")
|
||||
if node:
|
||||
raise ConfigurationError()
|
||||
|
||||
settings["default_destination"] = self.uiDefaultDestinationLineEdit.text().strip()
|
||||
|
||||
if not node:
|
||||
default_name_format = self.uiDefaultNameFormatLineEdit.text().strip()
|
||||
if '{0}' not in default_name_format and '{id}' not in default_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Default name format", "The default name format must contain at least {0} or {id}")
|
||||
else:
|
||||
settings["default_name_format"] = default_name_format
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
settings["symbol"] = symbol_path
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
return settings
|
||||
189
gns3/modules/traceng/pages/traceng_node_preferences_page.py
Normal file
189
gns3/modules/traceng/pages/traceng_node_preferences_page.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for TraceNG node preferences.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets, qpartial
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.controller import Controller
|
||||
|
||||
from .. import TraceNG
|
||||
from ..settings import TRACENG_NODES_SETTINGS
|
||||
from ..ui.traceng_node_preferences_page_ui import Ui_TraceNGNodePageWidget
|
||||
from ..pages.traceng_node_configuration_page import TraceNGNodeConfigurationPage
|
||||
from ..dialogs.traceng_node_wizard import TraceNGNodeWizard
|
||||
|
||||
|
||||
class TraceNGNodePreferencesPage(QtWidgets.QWidget, Ui_TraceNGNodePageWidget):
|
||||
"""
|
||||
QWidget preference page for TraceNG node preferences.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = MainWindow.instance()
|
||||
self._traceng_nodes = {}
|
||||
self._items = []
|
||||
|
||||
self.uiNewTraceNGPushButton.clicked.connect(self._newTraceNGSlot)
|
||||
self.uiEditTraceNGPushButton.clicked.connect(self._editTraceNGSlot)
|
||||
self.uiDeleteTraceNGPushButton.clicked.connect(self._deleteTraceNGSlot)
|
||||
self.uiTraceNGTreeWidget.itemSelectionChanged.connect(self._tracengChangedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiTraceNGInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
font = section_item.font(0)
|
||||
font.setBold(True)
|
||||
section_item.setFont(0, font)
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, traceng_node):
|
||||
|
||||
self.uiTraceNGInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", traceng_node["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["IP address:", traceng_node["ip_address"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", traceng_node["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(traceng_node["server"]).name()])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.uiTraceNGInfoTreeWidget.expandAll()
|
||||
self.uiTraceNGInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiTraceNGInfoTreeWidget.resizeColumnToContents(1)
|
||||
self.uiTraceNGTreeWidget.setMaximumWidth(self.uiTraceNGTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _tracengChangedSlot(self):
|
||||
"""
|
||||
Loads a selected TraceNG node template from the tree widget.
|
||||
"""
|
||||
|
||||
selection = self.uiTraceNGTreeWidget.selectedItems()
|
||||
self.uiDeleteTraceNGPushButton.setEnabled(len(selection) != 0)
|
||||
single_selected = len(selection) == 1
|
||||
self.uiEditTraceNGPushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
traceng_node = self._traceng_nodes[key]
|
||||
self._refreshInfo(traceng_node)
|
||||
else:
|
||||
self.uiTraceNGInfoTreeWidget.clear()
|
||||
|
||||
def _newTraceNGSlot(self):
|
||||
"""
|
||||
Creates a new TraceNG node template.
|
||||
"""
|
||||
|
||||
wizard = TraceNGNodeWizard(self._traceng_nodes, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_traceng_node_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_traceng_node_settings["server"], name=new_traceng_node_settings["name"])
|
||||
self._traceng_nodes[key] = TRACENG_NODES_SETTINGS.copy()
|
||||
self._traceng_nodes[key].update(new_traceng_node_settings)
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiTraceNGTreeWidget)
|
||||
item.setText(0, self._traceng_nodes[key]["name"])
|
||||
Controller.instance().getSymbolIcon(self._traceng_nodes[key]["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiTraceNGTreeWidget.setCurrentItem(item)
|
||||
|
||||
def _editTraceNGSlot(self):
|
||||
"""
|
||||
Edits a TraceNG node template.
|
||||
"""
|
||||
|
||||
item = self.uiTraceNGTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
traceng_node = self._traceng_nodes[key]
|
||||
dialog = ConfigurationDialog(traceng_node["name"], traceng_node, TraceNGNodeConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(traceng_node["symbol"], qpartial(self._setItemIcon, item))
|
||||
if traceng_node["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(server=traceng_node["server"], name=traceng_node["name"])
|
||||
if new_key in self._traceng_nodes:
|
||||
QtWidgets.QMessageBox.critical(self, "TraceNG node", "TraceNG node name {} already exists for server {}".format(traceng_node["name"],
|
||||
traceng_node["server"]))
|
||||
traceng_node["name"] = item.text(0)
|
||||
return
|
||||
self._traceng_nodes[new_key] = self._traceng_nodes[key]
|
||||
del self._traceng_nodes[key]
|
||||
item.setText(0, traceng_node["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(traceng_node)
|
||||
|
||||
def _deleteTraceNGSlot(self):
|
||||
"""
|
||||
Deletes a TraceNG node template.
|
||||
"""
|
||||
|
||||
for item in self.uiTraceNGTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._traceng_nodes[key]
|
||||
self.uiTraceNGTreeWidget.takeTopLevelItem(self.uiTraceNGTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
def loadPreferences(self):
|
||||
"""
|
||||
Loads the TraceNG node preferences.
|
||||
"""
|
||||
|
||||
traceng_module = TraceNG.instance()
|
||||
self._traceng_nodes = copy.deepcopy(traceng_module.VMs())
|
||||
self._items.clear()
|
||||
|
||||
for key, node in self._traceng_nodes.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiTraceNGTreeWidget)
|
||||
item.setText(0, node["name"])
|
||||
Controller.instance().getSymbolIcon(node["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiTraceNGTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiTraceNGTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiTraceNGTreeWidget.setMaximumWidth(self.uiTraceNGTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
item.setIcon(0, icon)
|
||||
self.uiTraceNGTreeWidget.setMaximumWidth(self.uiTraceNGTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
Saves the TraceNG node preferences.
|
||||
"""
|
||||
|
||||
TraceNG.instance().setVMs(self._traceng_nodes)
|
||||
127
gns3/modules/traceng/pages/traceng_preferences_page.py
Normal file
127
gns3/modules/traceng/pages/traceng_preferences_page.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for TraceNG preferences.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
|
||||
from .. import TraceNG
|
||||
from ..ui.traceng_preferences_page_ui import Ui_TraceNGPreferencesPageWidget
|
||||
from ..settings import TRACENG_SETTINGS
|
||||
|
||||
|
||||
class TraceNGPreferencesPage(QtWidgets.QWidget, Ui_TraceNGPreferencesPageWidget):
|
||||
|
||||
"""
|
||||
QWidget preference page for TraceNG
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
# connect signals
|
||||
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
|
||||
self.uiTraceNGPathToolButton.clicked.connect(self._tracengPathBrowserSlot)
|
||||
|
||||
def _tracengPathBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select traceng
|
||||
"""
|
||||
|
||||
filter = ""
|
||||
if sys.platform.startswith("win"):
|
||||
filter = "Executable (*.exe);;All files (*.*)"
|
||||
traceng_path = shutil.which("traceng")
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select TraceNG", traceng_path, filter)
|
||||
if not path:
|
||||
return
|
||||
|
||||
if self._checkTraceNGPath(path):
|
||||
self.uiTraceNGPathLineEdit.setText(os.path.normpath(path))
|
||||
|
||||
def _checkTraceNGPath(self, path):
|
||||
"""
|
||||
Checks that the TraceNG path is valid.
|
||||
|
||||
:param path: TraceNG path
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
if not os.path.exists(path):
|
||||
QtWidgets.QMessageBox.critical(self, "TraceNG", '"{}" does not exist'.format(path))
|
||||
return False
|
||||
|
||||
if not os.access(path, os.X_OK):
|
||||
QtWidgets.QMessageBox.critical(self, "TraceNG", "{} is not an executable".format(os.path.basename(path)))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _restoreDefaultsSlot(self):
|
||||
"""
|
||||
Slot to populate the page widgets with the default settings.
|
||||
"""
|
||||
|
||||
self._populateWidgets(TRACENG_SETTINGS)
|
||||
|
||||
def _useLocalServerSlot(self, state):
|
||||
"""
|
||||
Slot to enable or not local server settings.
|
||||
"""
|
||||
|
||||
if state:
|
||||
self.uiTraceNGPathLineEdit.setEnabled(True)
|
||||
self.uiTraceNGPathToolButton.setEnabled(True)
|
||||
else:
|
||||
self.uiTraceNGPathLineEdit.setEnabled(False)
|
||||
self.uiTraceNGPathToolButton.setEnabled(False)
|
||||
|
||||
def _populateWidgets(self, settings):
|
||||
"""
|
||||
Populates the widgets with the settings.
|
||||
|
||||
:param settings: TraceNG settings
|
||||
"""
|
||||
|
||||
self.uiTraceNGPathLineEdit.setText(settings["traceng_path"])
|
||||
|
||||
def loadPreferences(self):
|
||||
"""
|
||||
Loads TraceNG preferences.
|
||||
"""
|
||||
|
||||
traceng_settings = TraceNG.instance().settings()
|
||||
self._populateWidgets(traceng_settings)
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
Saves TraceNG preferences.
|
||||
"""
|
||||
|
||||
traceng_path = self.uiTraceNGPathLineEdit.text().strip()
|
||||
if traceng_path and not self._checkTraceNGPath(traceng_path):
|
||||
return
|
||||
new_settings = {"traceng_path": traceng_path}
|
||||
TraceNG.instance().setSettings(new_settings)
|
||||
36
gns3/modules/traceng/settings.py
Normal file
36
gns3/modules/traceng/settings.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Default TraceNG settings.
|
||||
"""
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
TRACENG_SETTINGS = {
|
||||
"traceng_path": "",
|
||||
}
|
||||
|
||||
TRACENG_NODES_SETTINGS = {
|
||||
"name": "",
|
||||
"ip_address": "",
|
||||
"default_destination": "",
|
||||
"default_name_format": "TraceNG{0}",
|
||||
"console_type": "none",
|
||||
"symbol": ":/symbols/traceng.svg",
|
||||
"category": Node.end_devices,
|
||||
}
|
||||
172
gns3/modules/traceng/traceng_node.py
Normal file
172
gns3/modules/traceng/traceng_node.py
Normal file
@@ -0,0 +1,172 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
TraceNG node implementation.
|
||||
"""
|
||||
|
||||
from gns3.node import Node
|
||||
from gns3.qt import QtWidgets
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TraceNGNode(Node):
|
||||
"""
|
||||
TraceNG node.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
URL_PREFIX = "traceng"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
super().__init__(module, server, project)
|
||||
|
||||
traceng_settings = {"console_host": None,
|
||||
"console": None,
|
||||
"console_type": "none",
|
||||
"ip_address": "",
|
||||
"default_destination": ""}
|
||||
|
||||
self._last_destination = ""
|
||||
self.settings().update(traceng_settings)
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this TraceNG node.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts this node instance.
|
||||
"""
|
||||
|
||||
if self.isStarted():
|
||||
log.debug("{} is already running".format(self.name()))
|
||||
return
|
||||
|
||||
if self._last_destination:
|
||||
destination = self._last_destination
|
||||
else:
|
||||
destination = self.settings()["default_destination"]
|
||||
destination, ok = QtWidgets.QInputDialog.getText(self.parent(), "TraceNG", "Destination host or IP address:", text=destination)
|
||||
if ok:
|
||||
if not destination:
|
||||
QtWidgets.QMessageBox.critical(self, "TraceNG", "Please provide a host or IP address to trace")
|
||||
return
|
||||
ip_address = self.settings()["ip_address"]
|
||||
if destination == ip_address:
|
||||
QtWidgets.QMessageBox.critical(self, "TraceNG", "Destination cannot be the same as this node IP address ({})".format(ip_address))
|
||||
return
|
||||
self._last_destination = destination
|
||||
params = {"destination": destination}
|
||||
log.debug("{} is starting".format(self.name()))
|
||||
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, body=params, timeout=None, showProgress=False)
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this TraceNG node.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
if self.status() == Node.started:
|
||||
state = "started"
|
||||
else:
|
||||
state = "stopped"
|
||||
|
||||
info = """Node {name} is {state}
|
||||
Local node ID is {id}
|
||||
Server's VPCS node ID is {node_id}
|
||||
TraceNG's server runs on {host}, console is on port {console}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
state=state,
|
||||
host=self.compute().name(),
|
||||
console=self._settings["console"])
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " {port_name} is empty\n".format(port_name=port.name())
|
||||
else:
|
||||
port_info += " {port_name} {port_description}\n".format(port_name=port.name(),
|
||||
port_description=port.description())
|
||||
|
||||
return info + port_info
|
||||
|
||||
def console(self):
|
||||
"""
|
||||
Returns the console port for this TraceNG node.
|
||||
|
||||
:returns: port (integer)
|
||||
"""
|
||||
|
||||
return self._settings["console"]
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
from .pages.traceng_node_configuration_page import TraceNGNodeConfigurationPage
|
||||
return TraceNGNodeConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/traceng.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "TraceNG"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the node panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.end_devices]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "TraceNG node"
|
||||
0
gns3/modules/traceng/ui/__init__.py
Normal file
0
gns3/modules/traceng/ui/__init__.py
Normal file
119
gns3/modules/traceng/ui/traceng_node_configuration_page.ui
Executable file
119
gns3/modules/traceng/ui/traceng_node_configuration_page.ui
Executable file
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TraceNGNodeConfigPageWidget</class>
|
||||
<widget class="QWidget" name="TraceNGNodeConfigPageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>846</width>
|
||||
<height>340</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>TraceNG node configuration</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiNameLabel">
|
||||
<property name="text">
|
||||
<string>Name:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiIPAddressLabel">
|
||||
<property name="text">
|
||||
<string>IP address:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QLabel" name="uiDefaultDestinationLabel">
|
||||
<property name="text">
|
||||
<string>Default destination:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="3">
|
||||
<widget class="QLabel" name="uiDefaultNameFormatLabel">
|
||||
<property name="text">
|
||||
<string>Default name format:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="QLineEdit" name="uiDefaultNameFormatLineEdit"/>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="uiSymbolLabel">
|
||||
<property name="text">
|
||||
<string>Symbol:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="3">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="uiSymbolLineEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="uiSymbolToolButton">
|
||||
<property name="text">
|
||||
<string>&Browse...</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextOnly</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QLabel" name="uiCategoryLabel">
|
||||
<property name="text">
|
||||
<string>Category:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="3">
|
||||
<widget class="QComboBox" name="uiCategoryComboBox"/>
|
||||
</item>
|
||||
<item row="6" column="1" colspan="3">
|
||||
<spacer name="spacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>263</width>
|
||||
<height>212</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QLineEdit" name="uiDefaultDestinationLineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QLineEdit" name="uiIPAddressLineEdit"/>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QLineEdit" name="uiNameLineEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
<zorder>uiNameLabel</zorder>
|
||||
<zorder>uiNameLineEdit</zorder>
|
||||
<zorder>uiDefaultNameFormatLabel</zorder>
|
||||
<zorder>uiDefaultNameFormatLineEdit</zorder>
|
||||
<zorder>uiSymbolLabel</zorder>
|
||||
<zorder>uiCategoryLabel</zorder>
|
||||
<zorder>uiCategoryComboBox</zorder>
|
||||
<zorder>uiIPAddressLabel</zorder>
|
||||
<zorder>uiIPAddressLineEdit</zorder>
|
||||
<zorder>uiDefaultDestinationLabel</zorder>
|
||||
<zorder>uiDefaultDestinationLineEdit</zorder>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/traceng/ui/traceng_node_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.5.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_TraceNGNodeConfigPageWidget(object):
|
||||
def setupUi(self, TraceNGNodeConfigPageWidget):
|
||||
TraceNGNodeConfigPageWidget.setObjectName("TraceNGNodeConfigPageWidget")
|
||||
TraceNGNodeConfigPageWidget.resize(846, 340)
|
||||
self.gridLayout = QtWidgets.QGridLayout(TraceNGNodeConfigPageWidget)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.uiNameLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
|
||||
self.uiNameLabel.setObjectName("uiNameLabel")
|
||||
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
|
||||
self.uiIPAddressLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
|
||||
self.uiIPAddressLabel.setObjectName("uiIPAddressLabel")
|
||||
self.gridLayout.addWidget(self.uiIPAddressLabel, 1, 0, 1, 1)
|
||||
self.uiDefaultDestinationLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
|
||||
self.uiDefaultDestinationLabel.setObjectName("uiDefaultDestinationLabel")
|
||||
self.gridLayout.addWidget(self.uiDefaultDestinationLabel, 2, 0, 1, 2)
|
||||
self.uiDefaultNameFormatLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
|
||||
self.uiDefaultNameFormatLabel.setObjectName("uiDefaultNameFormatLabel")
|
||||
self.gridLayout.addWidget(self.uiDefaultNameFormatLabel, 3, 0, 1, 3)
|
||||
self.uiDefaultNameFormatLineEdit = QtWidgets.QLineEdit(TraceNGNodeConfigPageWidget)
|
||||
self.uiDefaultNameFormatLineEdit.setObjectName("uiDefaultNameFormatLineEdit")
|
||||
self.gridLayout.addWidget(self.uiDefaultNameFormatLineEdit, 3, 3, 1, 1)
|
||||
self.uiSymbolLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
|
||||
self.uiSymbolLabel.setObjectName("uiSymbolLabel")
|
||||
self.gridLayout.addWidget(self.uiSymbolLabel, 4, 0, 1, 1)
|
||||
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
|
||||
self.uiSymbolLineEdit = QtWidgets.QLineEdit(TraceNGNodeConfigPageWidget)
|
||||
self.uiSymbolLineEdit.setObjectName("uiSymbolLineEdit")
|
||||
self.horizontalLayout_7.addWidget(self.uiSymbolLineEdit)
|
||||
self.uiSymbolToolButton = QtWidgets.QToolButton(TraceNGNodeConfigPageWidget)
|
||||
self.uiSymbolToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
|
||||
self.uiSymbolToolButton.setObjectName("uiSymbolToolButton")
|
||||
self.horizontalLayout_7.addWidget(self.uiSymbolToolButton)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_7, 4, 3, 1, 1)
|
||||
self.uiCategoryLabel = QtWidgets.QLabel(TraceNGNodeConfigPageWidget)
|
||||
self.uiCategoryLabel.setObjectName("uiCategoryLabel")
|
||||
self.gridLayout.addWidget(self.uiCategoryLabel, 5, 0, 1, 2)
|
||||
self.uiCategoryComboBox = QtWidgets.QComboBox(TraceNGNodeConfigPageWidget)
|
||||
self.uiCategoryComboBox.setObjectName("uiCategoryComboBox")
|
||||
self.gridLayout.addWidget(self.uiCategoryComboBox, 5, 3, 1, 1)
|
||||
spacerItem = QtWidgets.QSpacerItem(263, 212, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem, 6, 1, 1, 3)
|
||||
self.uiDefaultDestinationLineEdit = QtWidgets.QLineEdit(TraceNGNodeConfigPageWidget)
|
||||
self.uiDefaultDestinationLineEdit.setObjectName("uiDefaultDestinationLineEdit")
|
||||
self.gridLayout.addWidget(self.uiDefaultDestinationLineEdit, 2, 3, 1, 1)
|
||||
self.uiIPAddressLineEdit = QtWidgets.QLineEdit(TraceNGNodeConfigPageWidget)
|
||||
self.uiIPAddressLineEdit.setObjectName("uiIPAddressLineEdit")
|
||||
self.gridLayout.addWidget(self.uiIPAddressLineEdit, 1, 3, 1, 1)
|
||||
self.uiNameLineEdit = QtWidgets.QLineEdit(TraceNGNodeConfigPageWidget)
|
||||
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
|
||||
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 3, 1, 1)
|
||||
self.uiNameLabel.raise_()
|
||||
self.uiNameLineEdit.raise_()
|
||||
self.uiDefaultNameFormatLabel.raise_()
|
||||
self.uiDefaultNameFormatLineEdit.raise_()
|
||||
self.uiSymbolLabel.raise_()
|
||||
self.uiCategoryLabel.raise_()
|
||||
self.uiCategoryComboBox.raise_()
|
||||
self.uiIPAddressLabel.raise_()
|
||||
self.uiIPAddressLineEdit.raise_()
|
||||
self.uiDefaultDestinationLabel.raise_()
|
||||
self.uiDefaultDestinationLineEdit.raise_()
|
||||
|
||||
self.retranslateUi(TraceNGNodeConfigPageWidget)
|
||||
QtCore.QMetaObject.connectSlotsByName(TraceNGNodeConfigPageWidget)
|
||||
|
||||
def retranslateUi(self, TraceNGNodeConfigPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
TraceNGNodeConfigPageWidget.setWindowTitle(_translate("TraceNGNodeConfigPageWidget", "TraceNG node configuration"))
|
||||
self.uiNameLabel.setText(_translate("TraceNGNodeConfigPageWidget", "Name:"))
|
||||
self.uiIPAddressLabel.setText(_translate("TraceNGNodeConfigPageWidget", "IP address:"))
|
||||
self.uiDefaultDestinationLabel.setText(_translate("TraceNGNodeConfigPageWidget", "Default destination:"))
|
||||
self.uiDefaultNameFormatLabel.setText(_translate("TraceNGNodeConfigPageWidget", "Default name format:"))
|
||||
self.uiSymbolLabel.setText(_translate("TraceNGNodeConfigPageWidget", "Symbol:"))
|
||||
self.uiSymbolToolButton.setText(_translate("TraceNGNodeConfigPageWidget", "&Browse..."))
|
||||
self.uiCategoryLabel.setText(_translate("TraceNGNodeConfigPageWidget", "Category:"))
|
||||
|
||||
160
gns3/modules/traceng/ui/traceng_node_preferences_page.ui
Normal file
160
gns3/modules/traceng/ui/traceng_node_preferences_page.ui
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TraceNGNodePageWidget</class>
|
||||
<widget class="QWidget" name="TraceNGNodePageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>546</width>
|
||||
<height>455</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>TraceNG nodes</string>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>TraceNG node templates</string>
|
||||
</property>
|
||||
<property name="accessibleDescription">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QTreeWidget" name="uiTraceNGTreeWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>160</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string notr="true">1</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="uiTraceNGInfoTreeWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="indentation">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="allColumnsShowFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>1</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiNewTraceNGPushButton">
|
||||
<property name="text">
|
||||
<string>&New</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiEditTraceNGPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Edit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiDeleteTraceNGPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>uiNewTraceNGPushButton</tabstop>
|
||||
<tabstop>uiDeleteTraceNGPushButton</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<designerdata>
|
||||
<property name="gridDeltaX">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="gridDeltaY">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="gridSnapX">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="gridSnapY">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="gridVisible">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</designerdata>
|
||||
</ui>
|
||||
83
gns3/modules/traceng/ui/traceng_node_preferences_page_ui.py
Normal file
83
gns3/modules/traceng/ui/traceng_node_preferences_page_ui.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/traceng/ui/traceng_node_preferences_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.5.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_TraceNGNodePageWidget(object):
|
||||
def setupUi(self, TraceNGNodePageWidget):
|
||||
TraceNGNodePageWidget.setObjectName("TraceNGNodePageWidget")
|
||||
TraceNGNodePageWidget.resize(546, 455)
|
||||
TraceNGNodePageWidget.setAccessibleDescription("")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout(TraceNGNodePageWidget)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.splitter = QtWidgets.QSplitter(TraceNGNodePageWidget)
|
||||
self.splitter.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.splitter.setObjectName("splitter")
|
||||
self.uiTraceNGTreeWidget = QtWidgets.QTreeWidget(self.splitter)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiTraceNGTreeWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiTraceNGTreeWidget.setSizePolicy(sizePolicy)
|
||||
self.uiTraceNGTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
|
||||
font = QtGui.QFont()
|
||||
font.setPointSize(11)
|
||||
font.setBold(True)
|
||||
font.setWeight(75)
|
||||
self.uiTraceNGTreeWidget.setFont(font)
|
||||
self.uiTraceNGTreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.uiTraceNGTreeWidget.setIconSize(QtCore.QSize(32, 32))
|
||||
self.uiTraceNGTreeWidget.setRootIsDecorated(False)
|
||||
self.uiTraceNGTreeWidget.setObjectName("uiTraceNGTreeWidget")
|
||||
self.uiTraceNGTreeWidget.headerItem().setText(0, "1")
|
||||
self.uiTraceNGTreeWidget.header().setVisible(False)
|
||||
self.layoutWidget = QtWidgets.QWidget(self.splitter)
|
||||
self.layoutWidget.setObjectName("layoutWidget")
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTraceNGInfoTreeWidget = QtWidgets.QTreeWidget(self.layoutWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiTraceNGInfoTreeWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiTraceNGInfoTreeWidget.setSizePolicy(sizePolicy)
|
||||
self.uiTraceNGInfoTreeWidget.setIndentation(10)
|
||||
self.uiTraceNGInfoTreeWidget.setAllColumnsShowFocus(True)
|
||||
self.uiTraceNGInfoTreeWidget.setObjectName("uiTraceNGInfoTreeWidget")
|
||||
self.uiTraceNGInfoTreeWidget.header().setVisible(False)
|
||||
self.verticalLayout.addWidget(self.uiTraceNGInfoTreeWidget)
|
||||
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
|
||||
self.uiNewTraceNGPushButton = QtWidgets.QPushButton(self.layoutWidget)
|
||||
self.uiNewTraceNGPushButton.setObjectName("uiNewTraceNGPushButton")
|
||||
self.horizontalLayout_5.addWidget(self.uiNewTraceNGPushButton)
|
||||
self.uiEditTraceNGPushButton = QtWidgets.QPushButton(self.layoutWidget)
|
||||
self.uiEditTraceNGPushButton.setEnabled(False)
|
||||
self.uiEditTraceNGPushButton.setObjectName("uiEditTraceNGPushButton")
|
||||
self.horizontalLayout_5.addWidget(self.uiEditTraceNGPushButton)
|
||||
self.uiDeleteTraceNGPushButton = QtWidgets.QPushButton(self.layoutWidget)
|
||||
self.uiDeleteTraceNGPushButton.setEnabled(False)
|
||||
self.uiDeleteTraceNGPushButton.setObjectName("uiDeleteTraceNGPushButton")
|
||||
self.horizontalLayout_5.addWidget(self.uiDeleteTraceNGPushButton)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout_5)
|
||||
self.verticalLayout_2.addWidget(self.splitter)
|
||||
|
||||
self.retranslateUi(TraceNGNodePageWidget)
|
||||
QtCore.QMetaObject.connectSlotsByName(TraceNGNodePageWidget)
|
||||
TraceNGNodePageWidget.setTabOrder(self.uiNewTraceNGPushButton, self.uiDeleteTraceNGPushButton)
|
||||
|
||||
def retranslateUi(self, TraceNGNodePageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
TraceNGNodePageWidget.setWindowTitle(_translate("TraceNGNodePageWidget", "TraceNG nodes"))
|
||||
TraceNGNodePageWidget.setAccessibleName(_translate("TraceNGNodePageWidget", "TraceNG node templates"))
|
||||
self.uiTraceNGInfoTreeWidget.headerItem().setText(0, _translate("TraceNGNodePageWidget", "1"))
|
||||
self.uiTraceNGInfoTreeWidget.headerItem().setText(1, _translate("TraceNGNodePageWidget", "2"))
|
||||
self.uiNewTraceNGPushButton.setText(_translate("TraceNGNodePageWidget", "&New"))
|
||||
self.uiEditTraceNGPushButton.setText(_translate("TraceNGNodePageWidget", "&Edit"))
|
||||
self.uiDeleteTraceNGPushButton.setText(_translate("TraceNGNodePageWidget", "&Delete"))
|
||||
|
||||
140
gns3/modules/traceng/ui/traceng_node_wizard.ui
Normal file
140
gns3/modules/traceng/ui/traceng_node_wizard.ui
Normal file
@@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TraceNGNodeWizard</class>
|
||||
<widget class="QWizard" name="TraceNGNodeWizard">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>706</width>
|
||||
<height>452</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>New TraceNG node template</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWizardPage" name="uiServerWizardPage">
|
||||
<property name="title">
|
||||
<string>Server</string>
|
||||
</property>
|
||||
<property name="subTitle">
|
||||
<string>Please choose a server type to run your new TraceNG node.</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QGroupBox" name="uiServerTypeGroupBox">
|
||||
<property name="title">
|
||||
<string>Server type</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="uiRemoteRadioButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Run the TraceNG node on a remote computer</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="uiVMRadioButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Run the TraceNG node on the GNS3 VM</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="uiLocalRadioButton">
|
||||
<property name="text">
|
||||
<string>Run the TraceNG node on your local computer</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QGroupBox" name="uiRemoteServersGroupBox">
|
||||
<property name="title">
|
||||
<string>Remote server</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiRemoteServersLabel">
|
||||
<property name="text">
|
||||
<string>Run on:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="uiRemoteServersComboBox">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWizardPage" name="uiNameWizardPage">
|
||||
<property name="title">
|
||||
<string>Name and IP address</string>
|
||||
</property>
|
||||
<property name="subTitle">
|
||||
<string>Please choose a descriptive name and IP address for the new TraceNG node.</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiNameLabel">
|
||||
<property name="text">
|
||||
<string>Name:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="uiNameLineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiIPAddressLabel">
|
||||
<property name="text">
|
||||
<string>IP address:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="uiIPAddressLineEdit">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>uiNameLineEdit</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
93
gns3/modules/traceng/ui/traceng_node_wizard_ui.py
Normal file
93
gns3/modules/traceng/ui/traceng_node_wizard_ui.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/traceng/ui/traceng_node_wizard.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.5.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_TraceNGNodeWizard(object):
|
||||
def setupUi(self, TraceNGNodeWizard):
|
||||
TraceNGNodeWizard.setObjectName("TraceNGNodeWizard")
|
||||
TraceNGNodeWizard.resize(706, 452)
|
||||
TraceNGNodeWizard.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.setEnabled(False)
|
||||
self.uiRemoteRadioButton.setChecked(False)
|
||||
self.uiRemoteRadioButton.setObjectName("uiRemoteRadioButton")
|
||||
self.verticalLayout.addWidget(self.uiRemoteRadioButton)
|
||||
self.uiVMRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
|
||||
self.uiVMRadioButton.setEnabled(False)
|
||||
self.uiVMRadioButton.setObjectName("uiVMRadioButton")
|
||||
self.verticalLayout.addWidget(self.uiVMRadioButton)
|
||||
self.uiLocalRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
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)
|
||||
TraceNGNodeWizard.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)
|
||||
self.uiIPAddressLabel = QtWidgets.QLabel(self.uiNameWizardPage)
|
||||
self.uiIPAddressLabel.setObjectName("uiIPAddressLabel")
|
||||
self.gridLayout.addWidget(self.uiIPAddressLabel, 1, 0, 1, 1)
|
||||
self.uiIPAddressLineEdit = QtWidgets.QLineEdit(self.uiNameWizardPage)
|
||||
self.uiIPAddressLineEdit.setText("")
|
||||
self.uiIPAddressLineEdit.setObjectName("uiIPAddressLineEdit")
|
||||
self.gridLayout.addWidget(self.uiIPAddressLineEdit, 1, 1, 1, 1)
|
||||
TraceNGNodeWizard.addPage(self.uiNameWizardPage)
|
||||
|
||||
self.retranslateUi(TraceNGNodeWizard)
|
||||
QtCore.QMetaObject.connectSlotsByName(TraceNGNodeWizard)
|
||||
|
||||
def retranslateUi(self, TraceNGNodeWizard):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
TraceNGNodeWizard.setWindowTitle(_translate("TraceNGNodeWizard", "New TraceNG node template"))
|
||||
self.uiServerWizardPage.setTitle(_translate("TraceNGNodeWizard", "Server"))
|
||||
self.uiServerWizardPage.setSubTitle(_translate("TraceNGNodeWizard", "Please choose a server type to run your new TraceNG node."))
|
||||
self.uiServerTypeGroupBox.setTitle(_translate("TraceNGNodeWizard", "Server type"))
|
||||
self.uiRemoteRadioButton.setText(_translate("TraceNGNodeWizard", "Run the TraceNG node on a remote computer"))
|
||||
self.uiVMRadioButton.setText(_translate("TraceNGNodeWizard", "Run the TraceNG node on the GNS3 VM"))
|
||||
self.uiLocalRadioButton.setText(_translate("TraceNGNodeWizard", "Run the TraceNG node on your local computer"))
|
||||
self.uiRemoteServersGroupBox.setTitle(_translate("TraceNGNodeWizard", "Remote server"))
|
||||
self.uiRemoteServersLabel.setText(_translate("TraceNGNodeWizard", "Run on:"))
|
||||
self.uiNameWizardPage.setTitle(_translate("TraceNGNodeWizard", "Name and IP address"))
|
||||
self.uiNameWizardPage.setSubTitle(_translate("TraceNGNodeWizard", "Please choose a descriptive name and IP address for the new TraceNG node."))
|
||||
self.uiNameLabel.setText(_translate("TraceNGNodeWizard", "Name:"))
|
||||
self.uiIPAddressLabel.setText(_translate("TraceNGNodeWizard", "IP address:"))
|
||||
|
||||
126
gns3/modules/traceng/ui/traceng_preferences_page.ui
Executable file
126
gns3/modules/traceng/ui/traceng_preferences_page.ui
Executable file
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TraceNGPreferencesPageWidget</class>
|
||||
<widget class="QWidget" name="TraceNGPreferencesPageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>623</width>
|
||||
<height>280</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>TraceNG</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<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="uiGeneralSettingsTabWidget">
|
||||
<attribute name="title">
|
||||
<string>Local settings</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="uiTraceNGPathLabel">
|
||||
<property name="text">
|
||||
<string>Path to TraceNG executable:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="uiTraceNGPathLineEdit">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="uiTraceNGPathToolButton">
|
||||
<property name="text">
|
||||
<string>&Browse...</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextOnly</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="spacer">
|
||||
<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>138</width>
|
||||
<height>17</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>
|
||||
67
gns3/modules/traceng/ui/traceng_preferences_page_ui.py
Normal file
67
gns3/modules/traceng/ui/traceng_preferences_page_ui.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/traceng/ui/traceng_preferences_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.5.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_TraceNGPreferencesPageWidget(object):
|
||||
def setupUi(self, TraceNGPreferencesPageWidget):
|
||||
TraceNGPreferencesPageWidget.setObjectName("TraceNGPreferencesPageWidget")
|
||||
TraceNGPreferencesPageWidget.resize(623, 280)
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout(TraceNGPreferencesPageWidget)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(TraceNGPreferencesPageWidget)
|
||||
self.uiTabWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.uiTabWidget.setObjectName("uiTabWidget")
|
||||
self.uiGeneralSettingsTabWidget = QtWidgets.QWidget()
|
||||
self.uiGeneralSettingsTabWidget.setObjectName("uiGeneralSettingsTabWidget")
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(self.uiGeneralSettingsTabWidget)
|
||||
self.verticalLayout.setContentsMargins(10, 10, 10, 10)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTraceNGPathLabel = QtWidgets.QLabel(self.uiGeneralSettingsTabWidget)
|
||||
self.uiTraceNGPathLabel.setObjectName("uiTraceNGPathLabel")
|
||||
self.verticalLayout.addWidget(self.uiTraceNGPathLabel)
|
||||
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
|
||||
self.uiTraceNGPathLineEdit = QtWidgets.QLineEdit(self.uiGeneralSettingsTabWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiTraceNGPathLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiTraceNGPathLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiTraceNGPathLineEdit.setObjectName("uiTraceNGPathLineEdit")
|
||||
self.horizontalLayout_5.addWidget(self.uiTraceNGPathLineEdit)
|
||||
self.uiTraceNGPathToolButton = QtWidgets.QToolButton(self.uiGeneralSettingsTabWidget)
|
||||
self.uiTraceNGPathToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
|
||||
self.uiTraceNGPathToolButton.setObjectName("uiTraceNGPathToolButton")
|
||||
self.horizontalLayout_5.addWidget(self.uiTraceNGPathToolButton)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout_5)
|
||||
spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout.addItem(spacerItem)
|
||||
self.uiTabWidget.addTab(self.uiGeneralSettingsTabWidget, "")
|
||||
self.verticalLayout_2.addWidget(self.uiTabWidget)
|
||||
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||
spacerItem1 = QtWidgets.QSpacerItem(138, 17, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_2.addItem(spacerItem1)
|
||||
self.uiRestoreDefaultsPushButton = QtWidgets.QPushButton(TraceNGPreferencesPageWidget)
|
||||
self.uiRestoreDefaultsPushButton.setObjectName("uiRestoreDefaultsPushButton")
|
||||
self.horizontalLayout_2.addWidget(self.uiRestoreDefaultsPushButton)
|
||||
self.verticalLayout_2.addLayout(self.horizontalLayout_2)
|
||||
|
||||
self.retranslateUi(TraceNGPreferencesPageWidget)
|
||||
self.uiTabWidget.setCurrentIndex(0)
|
||||
QtCore.QMetaObject.connectSlotsByName(TraceNGPreferencesPageWidget)
|
||||
|
||||
def retranslateUi(self, TraceNGPreferencesPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
TraceNGPreferencesPageWidget.setWindowTitle(_translate("TraceNGPreferencesPageWidget", "TraceNG"))
|
||||
self.uiTraceNGPathLabel.setText(_translate("TraceNGPreferencesPageWidget", "Path to TraceNG executable:"))
|
||||
self.uiTraceNGPathToolButton.setText(_translate("TraceNGPreferencesPageWidget", "&Browse..."))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiGeneralSettingsTabWidget), _translate("TraceNGPreferencesPageWidget", "Local settings"))
|
||||
self.uiRestoreDefaultsPushButton.setText(_translate("TraceNGPreferencesPageWidget", "Restore defaults"))
|
||||
|
||||
@@ -23,12 +23,10 @@ import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.local_config import LocalConfig
|
||||
|
||||
from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .virtualbox_vm import VirtualBoxVM
|
||||
from .settings import VBOX_SETTINGS
|
||||
from .settings import VBOX_VM_SETTINGS
|
||||
@@ -78,7 +76,8 @@ class VirtualBox(Module):
|
||||
vboxmanage_path_osx = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage"
|
||||
if os.path.exists(vboxmanage_path_osx):
|
||||
vboxmanage_path = vboxmanage_path_osx
|
||||
else:
|
||||
|
||||
if vboxmanage_path is None:
|
||||
vboxmanage_path = shutil.which("vboxmanage")
|
||||
|
||||
if vboxmanage_path is None:
|
||||
|
||||
@@ -22,6 +22,7 @@ Configuration page for VirtualBox VMs.
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.dialogs.node_properties_dialog import ConfigurationError
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.ports.port_name_factory import StandardPortNameFactory
|
||||
from gns3.node import Node
|
||||
|
||||
from ..ui.virtualbox_vm_configuration_page_ui import Ui_virtualBoxVMConfigPageWidget
|
||||
@@ -178,21 +179,21 @@ class VirtualBoxVMConfigurationPage(QtWidgets.QWidget, Ui_virtualBoxVMConfigPage
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
settings["symbol"] = symbol_path
|
||||
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
|
||||
port_name_format = self.uiPortNameFormatLineEdit.text()
|
||||
if '{0}' not in port_name_format and '{port0}' not in port_name_format and '{port1}' not in port_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Port name format", "The format must contain at least {0}, {port0} or {port1}")
|
||||
else:
|
||||
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
|
||||
|
||||
port_segment_size = self.uiPortSegmentSizeSpinBox.value()
|
||||
if port_segment_size and '{1}' not in port_name_format and '{segment0}' not in port_name_format and '{segment1}' not in port_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Port name format", "If the segment size is not 0, the format must contain {1}, {segment0} or {segment1}")
|
||||
else:
|
||||
settings["port_segment_size"] = port_segment_size
|
||||
first_port_name = self.uiFirstPortNameLineEdit.text().strip()
|
||||
|
||||
settings["first_port_name"] = self.uiFirstPortNameLineEdit.text().strip()
|
||||
try:
|
||||
StandardPortNameFactory(self.uiAdaptersSpinBox.value(), first_port_name, port_name_format, port_segment_size)
|
||||
except (IndexError, ValueError, KeyError):
|
||||
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
|
||||
raise ConfigurationError()
|
||||
|
||||
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
|
||||
settings["port_segment_size"] = port_segment_size
|
||||
settings["first_port_name"] = first_port_name
|
||||
|
||||
settings["ram"] = self.uiVMRamSpinBox.value()
|
||||
settings["adapter_type"] = self.uiAdapterTypesComboBox.currentText()
|
||||
|
||||
@@ -50,7 +50,7 @@ class VMwareVMWizard(VMWizard, Ui_VMwareVMWizard):
|
||||
if super().validateCurrentPage() is False:
|
||||
return False
|
||||
|
||||
if self.currentPage() == self.uiVirtualBoxWizardPage:
|
||||
if self.currentPage() == self.uiVMwareWizardPage:
|
||||
if not self.uiVMListComboBox.count():
|
||||
QtWidgets.QMessageBox.critical(self, "VMware VMs", "There is no VMware VM available!")
|
||||
return False
|
||||
@@ -59,7 +59,7 @@ class VMwareVMWizard(VMWizard, Ui_VMwareVMWizard):
|
||||
def initializePage(self, page_id):
|
||||
|
||||
super().initializePage(page_id)
|
||||
if self.page(page_id) == self.uiVirtualBoxWizardPage:
|
||||
if self.page(page_id) == self.uiVMwareWizardPage:
|
||||
self.uiVMListComboBox.clear()
|
||||
Controller.instance().getCompute("/vmware/vms", self._compute_id, self._getVMwareVMsFromServerCallback, progressText="Listing VMware VMs...")
|
||||
|
||||
@@ -92,6 +92,8 @@ class VMwareVMWizard(VMWizard, Ui_VMwareVMWizard):
|
||||
"""
|
||||
|
||||
index = self.uiVMListComboBox.currentIndex()
|
||||
if index == -1:
|
||||
return
|
||||
vmname = self.uiVMListComboBox.itemText(index)
|
||||
vminfo = self.uiVMListComboBox.itemData(index)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ Configuration page for VMware VMs.
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.dialogs.node_properties_dialog import ConfigurationError
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.ports.port_name_factory import StandardPortNameFactory
|
||||
from gns3.node import Node
|
||||
|
||||
from ..ui.vmware_vm_configuration_page_ui import Ui_VMwareVMConfigPageWidget
|
||||
@@ -175,21 +176,21 @@ class VMwareVMConfigurationPage(QtWidgets.QWidget, Ui_VMwareVMConfigPageWidget):
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
settings["symbol"] = symbol_path
|
||||
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
|
||||
port_name_format = self.uiPortNameFormatLineEdit.text()
|
||||
if '{0}' not in port_name_format and '{port0}' not in port_name_format and '{port1}' not in port_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Port name format", "The format must contain at least {0}, {port0} or {port1}")
|
||||
else:
|
||||
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
|
||||
|
||||
port_segment_size = self.uiPortSegmentSizeSpinBox.value()
|
||||
if port_segment_size and '{1}' not in port_name_format and '{segment0}' not in port_name_format and '{segment1}' not in port_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Port name format", "If the segment size is not 0, the format must contain {1}, {segment0} or {segment1}")
|
||||
else:
|
||||
settings["port_segment_size"] = port_segment_size
|
||||
first_port_name = self.uiFirstPortNameLineEdit.text().strip()
|
||||
|
||||
settings["first_port_name"] = self.uiFirstPortNameLineEdit.text().strip()
|
||||
try:
|
||||
StandardPortNameFactory(self.uiAdaptersSpinBox.value(), first_port_name, port_name_format, port_segment_size)
|
||||
except (IndexError, ValueError, KeyError):
|
||||
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
|
||||
raise ConfigurationError()
|
||||
|
||||
settings["port_name_format"] = self.uiPortNameFormatLineEdit.text()
|
||||
settings["port_segment_size"] = port_segment_size
|
||||
settings["first_port_name"] = first_port_name
|
||||
|
||||
settings["adapter_type"] = self.uiAdapterTypesComboBox.currentText()
|
||||
settings["use_any_adapter"] = self.uiUseAnyAdapterCheckBox.isChecked()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>598</width>
|
||||
<width>755</width>
|
||||
<height>453</height>
|
||||
</rect>
|
||||
</property>
|
||||
@@ -81,7 +81,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWizardPage" name="uiVirtualBoxWizardPage">
|
||||
<widget class="QWizardPage" name="uiVMwareWizardPage">
|
||||
<property name="title">
|
||||
<string>VMware Virtual Machine</string>
|
||||
</property>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/vmware/ui/vmware_vm_wizard.ui'
|
||||
#
|
||||
# Created: Tue Sep 20 17:45:46 2016
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
# Created by: PyQt5 UI code generator 5.5.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_VMwareVMWizard(object):
|
||||
def setupUi(self, VMwareVMWizard):
|
||||
VMwareVMWizard.setObjectName("VMwareVMWizard")
|
||||
VMwareVMWizard.resize(598, 453)
|
||||
VMwareVMWizard.resize(755, 453)
|
||||
VMwareVMWizard.setModal(True)
|
||||
self.uiServerWizardPage = QtWidgets.QWizardPage()
|
||||
self.uiServerWizardPage.setObjectName("uiServerWizardPage")
|
||||
@@ -48,14 +47,14 @@ class Ui_VMwareVMWizard(object):
|
||||
self.gridLayout_8.addWidget(self.uiRemoteServersLabel, 0, 0, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.uiRemoteServersGroupBox, 1, 0, 1, 1)
|
||||
VMwareVMWizard.addPage(self.uiServerWizardPage)
|
||||
self.uiVirtualBoxWizardPage = QtWidgets.QWizardPage()
|
||||
self.uiVirtualBoxWizardPage.setObjectName("uiVirtualBoxWizardPage")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.uiVirtualBoxWizardPage)
|
||||
self.uiVMwareWizardPage = QtWidgets.QWizardPage()
|
||||
self.uiVMwareWizardPage.setObjectName("uiVMwareWizardPage")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.uiVMwareWizardPage)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.uiVMListLabel = QtWidgets.QLabel(self.uiVirtualBoxWizardPage)
|
||||
self.uiVMListLabel = QtWidgets.QLabel(self.uiVMwareWizardPage)
|
||||
self.uiVMListLabel.setObjectName("uiVMListLabel")
|
||||
self.gridLayout.addWidget(self.uiVMListLabel, 0, 0, 1, 1)
|
||||
self.uiVMListComboBox = QtWidgets.QComboBox(self.uiVirtualBoxWizardPage)
|
||||
self.uiVMListComboBox = QtWidgets.QComboBox(self.uiVMwareWizardPage)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
@@ -63,11 +62,11 @@ class Ui_VMwareVMWizard(object):
|
||||
self.uiVMListComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiVMListComboBox.setObjectName("uiVMListComboBox")
|
||||
self.gridLayout.addWidget(self.uiVMListComboBox, 0, 1, 1, 1)
|
||||
self.uiBaseVMCheckBox = QtWidgets.QCheckBox(self.uiVirtualBoxWizardPage)
|
||||
self.uiBaseVMCheckBox = QtWidgets.QCheckBox(self.uiVMwareWizardPage)
|
||||
self.uiBaseVMCheckBox.setEnabled(True)
|
||||
self.uiBaseVMCheckBox.setObjectName("uiBaseVMCheckBox")
|
||||
self.gridLayout.addWidget(self.uiBaseVMCheckBox, 1, 0, 1, 2)
|
||||
VMwareVMWizard.addPage(self.uiVirtualBoxWizardPage)
|
||||
VMwareVMWizard.addPage(self.uiVMwareWizardPage)
|
||||
|
||||
self.retranslateUi(VMwareVMWizard)
|
||||
QtCore.QMetaObject.connectSlotsByName(VMwareVMWizard)
|
||||
@@ -82,8 +81,8 @@ class Ui_VMwareVMWizard(object):
|
||||
self.uiLocalRadioButton.setText(_translate("VMwareVMWizard", "Run this VMware VM on my local computer"))
|
||||
self.uiRemoteServersGroupBox.setTitle(_translate("VMwareVMWizard", "Remote servers"))
|
||||
self.uiRemoteServersLabel.setText(_translate("VMwareVMWizard", "Run on server:"))
|
||||
self.uiVirtualBoxWizardPage.setTitle(_translate("VMwareVMWizard", "VMware Virtual Machine"))
|
||||
self.uiVirtualBoxWizardPage.setSubTitle(_translate("VMwareVMWizard", "Please choose a VMware virtual machine from the list."))
|
||||
self.uiVMwareWizardPage.setTitle(_translate("VMwareVMWizard", "VMware Virtual Machine"))
|
||||
self.uiVMwareWizardPage.setSubTitle(_translate("VMwareVMWizard", "Please choose a VMware virtual machine from the list."))
|
||||
self.uiVMListLabel.setText(_translate("VMwareVMWizard", "VM list:"))
|
||||
self.uiBaseVMCheckBox.setText(_translate("VMwareVMWizard", "Use as a linked base VM (experimental)"))
|
||||
|
||||
|
||||
@@ -77,10 +77,10 @@ class VPCS(Module):
|
||||
# save the settings
|
||||
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
|
||||
|
||||
server_settings = copy.copy(self._settings)
|
||||
if server_settings["vpcs_path"]:
|
||||
server_settings = {}
|
||||
if self._settings["vpcs_path"]:
|
||||
# save some settings to the server config file
|
||||
server_settings["vpcs_path"] = os.path.normpath(server_settings["vpcs_path"])
|
||||
server_settings["vpcs_path"] = os.path.normpath(self._settings["vpcs_path"])
|
||||
config = LocalServerConfig.instance()
|
||||
config.saveSettings(self.__class__.__name__, server_settings)
|
||||
|
||||
|
||||
33
gns3/node.py
33
gns3/node.py
@@ -22,7 +22,7 @@ import pathlib
|
||||
from gns3.controller import Controller
|
||||
from gns3.ports.ethernet_port import EthernetPort
|
||||
from gns3.ports.serial_port import SerialPort
|
||||
from gns3.utils.bring_to_front import bring_window_to_front_from_title
|
||||
from gns3.utils.bring_to_front import bring_window_to_front_from_process_name, bring_window_to_front_from_title
|
||||
from gns3.qt import QtGui, QtCore
|
||||
|
||||
from .base_node import BaseNode
|
||||
@@ -100,15 +100,11 @@ class Node(BaseNode):
|
||||
for key in data:
|
||||
if key not in self._settings or self._settings[key] != data[key]:
|
||||
changed = True
|
||||
|
||||
if not changed:
|
||||
return
|
||||
|
||||
# If it's the initialization we don't resend it
|
||||
# to the server
|
||||
if self._settings["x"] is not None:
|
||||
self._update(data)
|
||||
else:
|
||||
self._settings.update(data)
|
||||
self._update(data)
|
||||
|
||||
def setSymbol(self, symbol):
|
||||
self._settings["symbol"] = symbol
|
||||
@@ -227,10 +223,9 @@ class Node(BaseNode):
|
||||
Update the node on the controller
|
||||
"""
|
||||
|
||||
if self.initialized():
|
||||
log.debug("{} is updating settings: {}".format(self.name(), params))
|
||||
body = self._prepareBody(params)
|
||||
self.controllerHttpPut("/nodes/{node_id}".format(node_id=self._node_id), self.updateNodeCallback, body=body, timeout=timeout, showProgress=False)
|
||||
log.debug("{} is updating settings: {}".format(self.name(), params))
|
||||
body = self._prepareBody(params)
|
||||
self.controllerHttpPut("/nodes/{node_id}".format(node_id=self._node_id), self.updateNodeCallback, body=body, timeout=timeout, showProgress=False)
|
||||
|
||||
def updateNodeCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -263,6 +258,7 @@ class Node(BaseNode):
|
||||
node_id=self._node_id),
|
||||
self._duplicateCallback,
|
||||
body=body,
|
||||
progressText="Duplicating node {}...".format(self.name()),
|
||||
timeout=None)
|
||||
|
||||
def _duplicateCallback(self, result, error=False, **kwargs):
|
||||
@@ -355,7 +351,6 @@ class Node(BaseNode):
|
||||
return False
|
||||
|
||||
result = self._parseResponse(result)
|
||||
self._created = True
|
||||
self._createCallback(result)
|
||||
|
||||
if self._loading:
|
||||
@@ -386,6 +381,8 @@ class Node(BaseNode):
|
||||
"""
|
||||
|
||||
if not skip_controller:
|
||||
for link in self.links():
|
||||
link.setDeleting()
|
||||
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback)
|
||||
else:
|
||||
self.deleted_signal.emit()
|
||||
@@ -422,7 +419,7 @@ class Node(BaseNode):
|
||||
return
|
||||
|
||||
log.debug("{} is starting".format(self.name()))
|
||||
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, timeout=None, progressText="{} is starting".format(self.name()))
|
||||
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, timeout=None, showProgress=False)
|
||||
|
||||
def _startCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -448,7 +445,7 @@ class Node(BaseNode):
|
||||
return
|
||||
|
||||
log.debug("{} is stopping".format(self.name()))
|
||||
self.controllerHttpPost("/nodes/{node_id}/stop".format(node_id=self._node_id), self._stopCallback, progressText="{} is stopping".format(self.name()), timeout=None)
|
||||
self.controllerHttpPost("/nodes/{node_id}/stop".format(node_id=self._node_id), self._stopCallback, showProgress=False, timeout=None)
|
||||
|
||||
def _stopCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -551,6 +548,14 @@ class Node(BaseNode):
|
||||
"""
|
||||
|
||||
if self.status() == Node.started:
|
||||
console_command = self.consoleCommand()
|
||||
if console_command:
|
||||
process_name = console_command.split()[0]
|
||||
if bring_window_to_front_from_process_name(process_name, self.name()):
|
||||
return True
|
||||
else:
|
||||
log.debug("Could not find process name '' and window title '{}' to bring it to front".format(process_name, self.name()))
|
||||
|
||||
if bring_window_to_front_from_title(self.name()):
|
||||
return True
|
||||
else:
|
||||
|
||||
@@ -22,7 +22,7 @@ on the QGraphics scene.
|
||||
|
||||
import tempfile
|
||||
import json
|
||||
import sip
|
||||
from .qt import sip
|
||||
|
||||
from .qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from .modules import MODULES
|
||||
@@ -98,7 +98,7 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
|
||||
if self._show_installed_appliances:
|
||||
for appliance in ApplianceManager.instance().appliances():
|
||||
if category is not None and category != CATEGORY_TO_ID[appliance["category"]]:
|
||||
if category is not None and category != CATEGORY_TO_ID.get(appliance["category"], "guest"):
|
||||
continue
|
||||
if search != "" and search.lower() not in appliance["name"].lower():
|
||||
continue
|
||||
@@ -117,7 +117,7 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
if appliance["builtin"] and not self._show_builtin_available_appliances:
|
||||
continue
|
||||
|
||||
if category is not None and category != CATEGORY_TO_ID[appliance["category"]]:
|
||||
if category is not None and category != CATEGORY_TO_ID.get(appliance["category"], "guest"):
|
||||
continue
|
||||
if search != "" and search.lower() not in appliance["name"].lower():
|
||||
continue
|
||||
@@ -131,7 +131,6 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
item.setData(1, QtCore.Qt.UserRole, "appliance_template")
|
||||
item.setSizeHint(0, QtCore.QSize(32, 32))
|
||||
Controller.instance().getSymbolIcon(appliance.get("symbol"), qpartial(self._setItemIcon, item), fallback=":/symbols/" + appliance["category"] + ".svg")
|
||||
|
||||
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
@@ -155,7 +154,7 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
"""
|
||||
|
||||
# Check that an item has been selected and right click
|
||||
if self.currentItem() is not None and event.button() == QtCore.Qt.RightButton:
|
||||
if event.button() == QtCore.Qt.RightButton:
|
||||
self._showContextualMenu()
|
||||
event.accept()
|
||||
return
|
||||
@@ -175,10 +174,12 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
|
||||
# retrieve the node class from the item data
|
||||
if item.data(1, QtCore.Qt.UserRole) == "appliance_template":
|
||||
f = tempfile.NamedTemporaryFile(mode="w+", suffix=".builtin.gns3a", delete=False)
|
||||
json.dump(item.data(0, QtCore.Qt.UserRole), f)
|
||||
f.close()
|
||||
self._getMainWindow().loadPath(f.name)
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode="w+", suffix=".builtin.gns3a", delete=False) as f:
|
||||
json.dump(item.data(0, QtCore.Qt.UserRole), f)
|
||||
self._getMainWindow().loadPath(f.name)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Appliance", "Cannot install appliance: {}".format(e))
|
||||
return
|
||||
|
||||
icon = item.icon(0)
|
||||
@@ -199,40 +200,46 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
event.accept()
|
||||
|
||||
def _showContextualMenu(self):
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
refresh_action = QtWidgets.QAction("Refresh templates", menu)
|
||||
refresh_action.setIcon(QtGui.QIcon(":/icons/reload.svg"))
|
||||
refresh_action.triggered.connect(self.refresh)
|
||||
menu.addAction(refresh_action)
|
||||
|
||||
item = self.currentItem()
|
||||
node = ApplianceManager.instance().getAppliance(item.data(0, QtCore.Qt.UserRole))
|
||||
if not node:
|
||||
return
|
||||
for module in MODULES:
|
||||
if node["node_type"] == "dynamips":
|
||||
node_class = module.getNodeType(node["node_type"], node["platform"])
|
||||
else:
|
||||
node_class = module.getNodeType(node["node_type"])
|
||||
|
||||
if node_class:
|
||||
break
|
||||
|
||||
# We can not edit stuff like EthernetSwitch
|
||||
# or without config template like VPCS
|
||||
if not node["builtin"] and hasattr(module, "vmConfigurationPage"):
|
||||
vm = None
|
||||
for vm_key, vm in module.instance().VMs().items():
|
||||
if vm["name"] == node["name"]:
|
||||
break
|
||||
if vm is None:
|
||||
if item:
|
||||
node = ApplianceManager.instance().getAppliance(item.data(0, QtCore.Qt.UserRole))
|
||||
if not node:
|
||||
return
|
||||
menu = QtWidgets.QMenu()
|
||||
configuration = QtWidgets.QAction("Configure Template", menu)
|
||||
configuration.setIcon(QtGui.QIcon(":/icons/configuration.svg"))
|
||||
configuration.triggered.connect(qpartial(self._configurationSlot, vm, module))
|
||||
menu.addAction(configuration)
|
||||
for module in MODULES:
|
||||
if node["node_type"] == "dynamips":
|
||||
node_class = module.getNodeType(node["node_type"], node["platform"])
|
||||
else:
|
||||
node_class = module.getNodeType(node["node_type"])
|
||||
|
||||
configuration = QtWidgets.QAction("Delete Template", menu)
|
||||
configuration.setIcon(QtGui.QIcon(":/icons/delete.svg"))
|
||||
configuration.triggered.connect(qpartial(self._deleteSlot, vm_key, vm, module))
|
||||
menu.addAction(configuration)
|
||||
if node_class:
|
||||
break
|
||||
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
# We can not edit stuff like EthernetSwitch
|
||||
# or without config template like VPCS
|
||||
if not node["builtin"] and hasattr(module, "vmConfigurationPage"):
|
||||
vm = None
|
||||
for vm_key, vm in module.instance().VMs().items():
|
||||
if vm["name"] == node["name"]:
|
||||
break
|
||||
if vm is not None:
|
||||
configure_action = QtWidgets.QAction("Configure template", menu)
|
||||
configure_action.setIcon(QtGui.QIcon(":/icons/configuration.svg"))
|
||||
configure_action.triggered.connect(qpartial(self._configurationSlot, vm, module))
|
||||
menu.addAction(configure_action)
|
||||
|
||||
delete_action = QtWidgets.QAction("Delete template", menu)
|
||||
delete_action.setIcon(QtGui.QIcon(":/icons/delete.svg"))
|
||||
delete_action.triggered.connect(qpartial(self._deleteSlot, vm_key, vm, module))
|
||||
menu.addAction(delete_action)
|
||||
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def _configurationSlot(self, vm, module, source):
|
||||
|
||||
@@ -249,7 +256,7 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
vms = module.instance().VMs()
|
||||
vms.pop(vm_key)
|
||||
vms.pop(vm_key, None)
|
||||
module.instance().setVMs(vms)
|
||||
LocalConfig.instance().writeConfig()
|
||||
self.refresh()
|
||||
|
||||
@@ -91,7 +91,7 @@ class PacketCapture:
|
||||
|
||||
if link:
|
||||
if link.capturing():
|
||||
if self._autostart[link] and link not in self._tail_process:
|
||||
if self._autostart.get(link) and link not in self._tail_process:
|
||||
self.startPacketCaptureReader(link)
|
||||
log.debug("Has successfully started capturing packets on {} to {}".format(link.id(), link.capture_file_path()))
|
||||
else:
|
||||
|
||||
@@ -316,8 +316,11 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
|
||||
self.uiSceneWidthSpinBox.setValue(settings["scene_width"])
|
||||
self.uiSceneHeightSpinBox.setValue(settings["scene_height"])
|
||||
self.uiGridSizeSpinBox.setValue(settings["grid_size"])
|
||||
self.uiRectangleSelectedItemCheckBox.setChecked(settings["draw_rectangle_selected_item"])
|
||||
self.uiDrawLinkStatusPointsCheckBox.setChecked(settings["draw_link_status_points"])
|
||||
self.uiShowInterfaceLabelsOnNewProject.setChecked(settings["show_interface_labels_on_new_project"])
|
||||
self.uiLimitSizeNodeSymbolCheckBox.setChecked(settings["limit_size_node_symbols"])
|
||||
|
||||
qt_font = QtGui.QFont()
|
||||
if qt_font.fromString(settings["default_label_font"]):
|
||||
@@ -378,8 +381,11 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
|
||||
new_graphics_view_settings = {"scene_width": self.uiSceneWidthSpinBox.value(),
|
||||
"scene_height": self.uiSceneHeightSpinBox.value(),
|
||||
"grid_size": self.uiGridSizeSpinBox.value(),
|
||||
"draw_rectangle_selected_item": self.uiRectangleSelectedItemCheckBox.isChecked(),
|
||||
"draw_link_status_points": self.uiDrawLinkStatusPointsCheckBox.isChecked(),
|
||||
"show_interface_labels_on_new_project": self.uiShowInterfaceLabelsOnNewProject.isChecked(),
|
||||
"limit_size_node_symbols": self.uiLimitSizeNodeSymbolCheckBox.isChecked(),
|
||||
"default_label_font": self.uiDefaultLabelStylePlainTextEdit.font().toString(),
|
||||
"default_label_color": self._default_label_color.name()}
|
||||
MainWindow.instance().uiGraphicsView.setSettings(new_graphics_view_settings)
|
||||
|
||||
@@ -20,7 +20,7 @@ Configuration page for GNS3 VM
|
||||
"""
|
||||
|
||||
import copy
|
||||
from gns3.qt import QtWidgets, QtCore, qpartial, qslot
|
||||
from gns3.qt import QtWidgets, QtCore, qpartial, qslot, sip_is_deleted
|
||||
from gns3.controller import Controller
|
||||
from ..ui.gns3_vm_preferences_page_ui import Ui_GNS3VMPreferencesPageWidget
|
||||
|
||||
@@ -74,7 +74,11 @@ class GNS3VMPreferencesPage(QtWidgets.QWidget, Ui_GNS3VMPreferencesPageWidget):
|
||||
"""
|
||||
Controller.instance().get("/gns3vm", self._getSettingsCallback)
|
||||
|
||||
@qslot
|
||||
def _getSettingsCallback(self, result, error=False, **kwargs):
|
||||
|
||||
if sip_is_deleted(self.uiRamSpinBox) or sip_is_deleted(self):
|
||||
return
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while getting settings : {}".format(result["message"]))
|
||||
@@ -95,10 +99,15 @@ class GNS3VMPreferencesPage(QtWidgets.QWidget, Ui_GNS3VMPreferencesPageWidget):
|
||||
|
||||
@qslot
|
||||
def _listEnginesCallback(self, result, error=False, ignore_error=False, **kwargs):
|
||||
|
||||
if sip_is_deleted(self.uiGNS3VMEngineComboBox) or sip_is_deleted(self):
|
||||
return
|
||||
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while getting the list of GNS3 VM engines : {}".format(result["message"]))
|
||||
return
|
||||
|
||||
self.uiGNS3VMEngineComboBox.clear()
|
||||
self._engines = result
|
||||
# We insert first the current engine to avoid triggering unexpected signals
|
||||
@@ -115,22 +124,24 @@ class GNS3VMPreferencesPage(QtWidgets.QWidget, Ui_GNS3VMPreferencesPageWidget):
|
||||
if engine_id:
|
||||
Controller.instance().get("/gns3vm/engines/{}/vms".format(engine_id), qpartial(self._listVMsCallback, ignore_error=ignore_error))
|
||||
|
||||
@qslot
|
||||
def _listVMsCallback(self, result, ignore_error=False, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
if not ignore_error:
|
||||
QtWidgets.QMessageBox.critical(self, "List vms", "Error while listing vms: {}".format(result["message"]))
|
||||
return
|
||||
self.uiVMListComboBox.clear()
|
||||
for vm in result:
|
||||
self.uiVMListComboBox.addItem(vm["vmname"], vm["vmname"])
|
||||
index = self.uiVMListComboBox.findText(self._settings["vmname"])
|
||||
if index == -1:
|
||||
index = self.uiVMListComboBox.findText("GNS3 VM")
|
||||
if not sip_is_deleted(self.uiVMListComboBox):
|
||||
self.uiVMListComboBox.clear()
|
||||
for vm in result:
|
||||
self.uiVMListComboBox.addItem(vm["vmname"], vm["vmname"])
|
||||
index = self.uiVMListComboBox.findText(self._settings["vmname"])
|
||||
if index == -1:
|
||||
index = 0
|
||||
self.uiVMListComboBox.setCurrentIndex(index)
|
||||
self._initialized = True
|
||||
index = self.uiVMListComboBox.findText("GNS3 VM")
|
||||
if index == -1:
|
||||
index = 0
|
||||
self.uiVMListComboBox.setCurrentIndex(index)
|
||||
self._initialized = True
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
@@ -162,5 +173,5 @@ class GNS3VMPreferencesPage(QtWidgets.QWidget, Ui_GNS3VMPreferencesPageWidget):
|
||||
def _saveSettingsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while save settings: {}".format(result["message"]))
|
||||
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while saving settings: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Base class for port objects.
|
||||
"""
|
||||
|
||||
import sip
|
||||
from ..qt import sip
|
||||
|
||||
from ..qt import qslot
|
||||
|
||||
@@ -232,7 +232,7 @@ class Port:
|
||||
|
||||
if self._destination_node and self._destination_port:
|
||||
if short:
|
||||
return "<-> {port} {name}".format(port=self._destination_port.shortName(),
|
||||
return "<=> {port} {name}".format(port=self._destination_port.shortName(),
|
||||
name=self._destination_node.name())
|
||||
return "connected to {name} on port {port}".format(name=self._destination_node.name(),
|
||||
port=self._destination_port.name())
|
||||
|
||||
61
gns3/ports/port_name_factory.py
Normal file
61
gns3/ports/port_name_factory.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StandardPortNameFactory:
|
||||
"""
|
||||
Generate default port names.
|
||||
"""
|
||||
|
||||
def __new__(cls, ethernet_adapters, first_port_name, port_name_format, port_segment_size):
|
||||
ports = []
|
||||
adapter_number = interface_number = segment_number = 0
|
||||
|
||||
for adapter_number in range(adapter_number, ethernet_adapters + adapter_number):
|
||||
if first_port_name and adapter_number == 0:
|
||||
port_name = first_port_name
|
||||
else:
|
||||
port_name = port_name_format.format(interface_number,
|
||||
segment_number,
|
||||
adapter=adapter_number,
|
||||
**cls._generate_replacement(interface_number, segment_number))
|
||||
interface_number += 1
|
||||
if port_segment_size:
|
||||
if interface_number % port_segment_size == 0:
|
||||
segment_number += 1
|
||||
interface_number = 0
|
||||
else:
|
||||
segment_number += 1
|
||||
ports.append(port_name)
|
||||
return ports
|
||||
|
||||
@staticmethod
|
||||
def _generate_replacement(interface_number, segment_number):
|
||||
"""
|
||||
This will generate replacement string for
|
||||
{port0} => {port9}
|
||||
{segment0} => {segment9}
|
||||
"""
|
||||
|
||||
replacements = {}
|
||||
for i in range(0, 9):
|
||||
replacements["port" + str(i)] = interface_number + i
|
||||
replacements["segment" + str(i)] = segment_number + i
|
||||
return replacements
|
||||
@@ -15,7 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sip
|
||||
from .qt import sip
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
|
||||
@@ -113,7 +113,7 @@ class Progress(QtCore.QObject):
|
||||
|
||||
@qslot
|
||||
def _rejectSlot(self, *args):
|
||||
if self._progress_dialog is not None and not sip.isdeleted(self._progress_dialog) or self._progress_dialog.wasCanceled():
|
||||
if self._progress_dialog is not None and (not sip.isdeleted(self._progress_dialog) or self._progress_dialog.wasCanceled()):
|
||||
self._progress_dialog.deleteLater()
|
||||
self._progress_dialog = None
|
||||
self._cancelSlot()
|
||||
@@ -189,7 +189,10 @@ class Progress(QtCore.QObject):
|
||||
# Due to Qt limitations for large numbers (above 32bit int) we calculate "progress" ourselves
|
||||
current, maximum = self._normalize(query['current'], query['maximum'])
|
||||
progress_dialog.setMaximum(maximum)
|
||||
progress_dialog.setValue(current)
|
||||
try:
|
||||
progress_dialog.setValue(current)
|
||||
except OverflowError:
|
||||
progress_dialog.setValue(100)
|
||||
|
||||
if text and query["maximum"] > 1000:
|
||||
text += "\n{} / {}".format(human_filesize(query["current"]), human_filesize(query["maximum"]))
|
||||
|
||||
@@ -49,6 +49,7 @@ class Project(QtCore.QObject):
|
||||
# Called when project is fully loaded
|
||||
project_loaded_signal = QtCore.Signal()
|
||||
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._id = None
|
||||
@@ -60,14 +61,20 @@ class Project(QtCore.QObject):
|
||||
self._auto_open = False
|
||||
self._auto_close = False
|
||||
|
||||
graphic_settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, GRAPHICS_VIEW_SETTINGS)
|
||||
config = LocalConfig.instance()
|
||||
|
||||
graphic_settings = LocalConfig.instance().loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS)
|
||||
self._scene_width = graphic_settings["scene_width"]
|
||||
self._scene_height = graphic_settings["scene_height"]
|
||||
self._zoom = graphic_settings.get("zoom", None)
|
||||
self._show_layers = graphic_settings.get("show_layers", False)
|
||||
self._snap_to_grid = graphic_settings.get("snap_to_grid", False)
|
||||
self._show_grid = graphic_settings.get("show_grid", False)
|
||||
self._grid_size = graphic_settings.get("grid_size", 75)
|
||||
self._show_interface_labels = graphic_settings.get("show_interface_labels", False)
|
||||
self._show_interface_labels_on_new_project = config.showInterfaceLabelsOnNewProject()
|
||||
self._variables = None
|
||||
self._supplier = None
|
||||
|
||||
self._name = "untitled"
|
||||
self._filename = None
|
||||
@@ -176,6 +183,21 @@ class Project(QtCore.QObject):
|
||||
"""
|
||||
return self._show_grid
|
||||
|
||||
def setGridSize(self, grid_size):
|
||||
"""
|
||||
Sets the grid size
|
||||
"""
|
||||
|
||||
self._grid_size = grid_size
|
||||
|
||||
def gridSize(self):
|
||||
"""
|
||||
Returns the grid size
|
||||
:return: integer
|
||||
"""
|
||||
|
||||
return self._grid_size
|
||||
|
||||
def setShowInterfaceLabels(self, show_interface_labels):
|
||||
"""
|
||||
Sets show interface labels mode
|
||||
@@ -189,6 +211,32 @@ class Project(QtCore.QObject):
|
||||
"""
|
||||
return self._show_interface_labels
|
||||
|
||||
def setVariables(self, variables):
|
||||
"""
|
||||
Sets variables of project
|
||||
"""
|
||||
self._variables = variables
|
||||
|
||||
def variables(self):
|
||||
"""
|
||||
Returns variables assigned to the project
|
||||
:return: boolean
|
||||
"""
|
||||
return self._variables
|
||||
|
||||
def setSupplier(self, supplier):
|
||||
"""
|
||||
Sets supplier of project
|
||||
"""
|
||||
self._supplier = supplier
|
||||
|
||||
def supplier(self):
|
||||
"""
|
||||
Returns supplier
|
||||
:return: boolean
|
||||
"""
|
||||
return self._supplier
|
||||
|
||||
def setName(self, name):
|
||||
"""
|
||||
Set project name
|
||||
@@ -265,12 +313,16 @@ class Project(QtCore.QObject):
|
||||
"""
|
||||
Duplicate a project
|
||||
"""
|
||||
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=self._id), qpartial(self._duplicateCallback, callback), body={"name": name, "path": path}, timeout=None)
|
||||
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=self._id),
|
||||
qpartial(self._duplicateCallback, callback),
|
||||
body={"name": name, "path": path},
|
||||
progressText="Duplicating project '{}'...".format(name),
|
||||
timeout=None)
|
||||
|
||||
def _duplicateCallback(self, callback, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
QtWidgets.QMessageBox.critical(None, "Duplicate project", "Error while duplicating: {}".format(result["message"]))
|
||||
log.error("Error while duplicating project: {}".format(result["message"]))
|
||||
return
|
||||
if callback:
|
||||
callback(result["project_id"])
|
||||
@@ -373,7 +425,8 @@ class Project(QtCore.QObject):
|
||||
"""
|
||||
body = {
|
||||
"name": self._name,
|
||||
"path": self.filesDir()
|
||||
"path": self.filesDir(),
|
||||
"show_interface_labels": self._show_interface_labels_on_new_project
|
||||
}
|
||||
Controller.instance().post("/projects", self._projectCreatedCallback, body=body)
|
||||
|
||||
@@ -392,7 +445,10 @@ class Project(QtCore.QObject):
|
||||
"show_layers": self._show_layers,
|
||||
"snap_to_grid": self._snap_to_grid,
|
||||
"show_grid": self._show_grid,
|
||||
"show_interface_labels": self._show_interface_labels
|
||||
"grid_size": self._grid_size,
|
||||
"show_interface_labels": self._show_interface_labels,
|
||||
"variables": self._variables,
|
||||
"supplier": self._supplier
|
||||
}
|
||||
self.put("", self._projectUpdatedCallback, body=body)
|
||||
|
||||
@@ -412,7 +468,9 @@ class Project(QtCore.QObject):
|
||||
self._closed = False
|
||||
self._closing = False
|
||||
self._startListenNotifications()
|
||||
|
||||
self.project_updated_signal.emit()
|
||||
self.project_loaded_signal.emit()
|
||||
|
||||
def _parseResponse(self, result):
|
||||
"""
|
||||
@@ -431,6 +489,12 @@ class Project(QtCore.QObject):
|
||||
self._show_layers = result.get("show_layers", False)
|
||||
self._snap_to_grid = result.get("snap_to_grid", False)
|
||||
self._show_grid = result.get("show_grid", False)
|
||||
self._variables = result.get("variables", None)
|
||||
self._supplier = result.get("supplier", None)
|
||||
|
||||
grid_size = result.get("grid_size", None)
|
||||
if grid_size:
|
||||
self._grid_size = grid_size
|
||||
self._show_interface_labels = result.get("show_interface_labels", False)
|
||||
|
||||
def load(self, path=None):
|
||||
@@ -444,7 +508,7 @@ class Project(QtCore.QObject):
|
||||
|
||||
def _projectOpenCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
self.project_creation_error_signal.emit(result["message"])
|
||||
self.project_creation_error_signal.emit(result.get("message", "unknown"))
|
||||
return
|
||||
|
||||
self._parseResponse(result)
|
||||
@@ -459,7 +523,7 @@ class Project(QtCore.QObject):
|
||||
|
||||
def _listNodesCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while listing project: {}".format(result["message"]))
|
||||
log.error("Error while listing project: {}".format(result.get("message", "unknown")))
|
||||
return
|
||||
topo = Topology.instance()
|
||||
for node in result:
|
||||
@@ -468,7 +532,7 @@ class Project(QtCore.QObject):
|
||||
|
||||
def _listLinksCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while listing links: {}".format(result["message"]))
|
||||
log.error("Error while listing links: {}".format(result.get("message", "unknown")))
|
||||
return
|
||||
topo = Topology.instance()
|
||||
for link in result:
|
||||
@@ -477,7 +541,7 @@ class Project(QtCore.QObject):
|
||||
|
||||
def _listDrawingsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while listing drawings: {}".format(result["message"]))
|
||||
log.error("Error while listing drawings: {}".format(result.get("message", "unknown")))
|
||||
return
|
||||
topo = Topology.instance()
|
||||
for drawing in result:
|
||||
|
||||
@@ -24,7 +24,6 @@ Compatibility layer for Qt bindings, so it is easier to switch to PySide if need
|
||||
|
||||
|
||||
import sys
|
||||
import sip
|
||||
import os
|
||||
import re
|
||||
import inspect
|
||||
@@ -33,12 +32,17 @@ import functools
|
||||
import logging
|
||||
log = logging.getLogger("qt/__init__.py")
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, Qt
|
||||
try:
|
||||
from PyQt5 import sip
|
||||
except ImportError:
|
||||
import sip
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets
|
||||
sys.modules[__name__ + '.QtCore'] = QtCore
|
||||
sys.modules[__name__ + '.QtGui'] = QtGui
|
||||
sys.modules[__name__ + '.QtNetwork'] = QtNetwork
|
||||
sys.modules[__name__ + '.QtWidgets'] = QtWidgets
|
||||
sys.modules[__name__ + '.Qt'] = Qt
|
||||
sys.modules[__name__ + '.sip'] = sip
|
||||
|
||||
try:
|
||||
from PyQt5 import QtSvg
|
||||
@@ -115,14 +119,20 @@ class LogQMessageBox(QtWidgets.QMessageBox):
|
||||
if message.startswith("QXcbConnection"): # Qt noise not relevant
|
||||
return
|
||||
LogQMessageBox._get_logger().critical(re.sub(r"<[^<]+?>", "", message), stack_info=LogQMessageBox.stack_info())
|
||||
if sip_is_deleted(parent):
|
||||
if parent is False:
|
||||
# special case to display a QMessageBox before the main window is created.
|
||||
parent = None
|
||||
elif sip_is_deleted(parent):
|
||||
return
|
||||
return super(QtWidgets.QMessageBox, QtWidgets.QMessageBox).critical(parent, title, message, *args)
|
||||
|
||||
@staticmethod
|
||||
def warning(parent, title, message, *args):
|
||||
LogQMessageBox._get_logger().warning(re.sub(r"<[^<]+?>", "", message))
|
||||
if sip_is_deleted(parent):
|
||||
if parent is False:
|
||||
# special case to display a QMessageBox before the main window is created.
|
||||
parent = None
|
||||
elif sip_is_deleted(parent):
|
||||
return
|
||||
return super(QtWidgets.QMessageBox, QtWidgets.QMessageBox).warning(parent, title, message, *args)
|
||||
|
||||
@@ -130,7 +140,7 @@ class LogQMessageBox(QtWidgets.QMessageBox):
|
||||
def _get_logger():
|
||||
"""
|
||||
Return a logger in the context of the caller
|
||||
in order to have the correct informations in the log
|
||||
in order to have the correct information in the log
|
||||
"""
|
||||
if sys.version_info < (3, 5):
|
||||
return logging.getLogger('qt')
|
||||
@@ -230,31 +240,6 @@ class StatsQtWidgetsQDialog(QtWidgets.QDialog):
|
||||
QtWidgets.QDialog = StatsQtWidgetsQDialog
|
||||
|
||||
|
||||
class PatchNetworkAccessManager(QNetworkAccessManager):
|
||||
"""
|
||||
Patch the network acces manager in order to solve
|
||||
hibernation issues on windows and Linux
|
||||
|
||||
See: https://github.com/GNS3/gns3-gui/issues/2104
|
||||
"""
|
||||
|
||||
def __init__(self, *params, **kwargs):
|
||||
super().__init__(*params, **kwargs)
|
||||
self.setNetworkAccessible(self.Accessible)
|
||||
self.networkAccessibleChanged.connect(self.networkAccessibleChangedSlot)
|
||||
|
||||
def networkAccessibleChangedSlot(self, status):
|
||||
"""
|
||||
When we lost the network we switch to another available network
|
||||
"""
|
||||
if status == self.Accessible:
|
||||
return
|
||||
self.setConfiguration(QtNetwork.QNetworkConfigurationManager().defaultConfiguration())
|
||||
|
||||
|
||||
QtNetwork.QNetworkAccessManager = PatchNetworkAccessManager
|
||||
|
||||
|
||||
def qpartial(func, *args, **kwargs):
|
||||
"""
|
||||
A functools partial that you can use on qobject. If the targeted qobject is
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from . import QtCore
|
||||
@@ -35,12 +36,14 @@ class QImageSvgRenderer(QtSvg.QSvgRenderer):
|
||||
"""
|
||||
|
||||
def __init__(self, path_or_data=None, fallback=None):
|
||||
|
||||
super().__init__()
|
||||
self._fallback = fallback
|
||||
self._svg = """<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{width}" height="{height}"></svg>"""
|
||||
self.load(path_or_data)
|
||||
|
||||
def load(self, path_or_data):
|
||||
|
||||
try:
|
||||
path_exists = os.path.exists(path_or_data)
|
||||
except ValueError: # On windows we can get an error because the path is too long (it's the svg data)
|
||||
@@ -60,6 +63,14 @@ class QImageSvgRenderer(QtSvg.QSvgRenderer):
|
||||
res = super().load(path_or_data)
|
||||
# If we can't render a SVG we load and base64 the image to create a SVG
|
||||
if self.isValid():
|
||||
if not path_or_data.startswith(":") and path_exists:
|
||||
try:
|
||||
with open(path_or_data, "rb") as f:
|
||||
self._svg = f.read().decode()
|
||||
except UnicodeError as e:
|
||||
log.error("Could not decode '{}' content: {}".format(path_or_data, e))
|
||||
except OSError as e:
|
||||
log.error("Could not read '{}': {}".format(path_or_data, e))
|
||||
return res
|
||||
except ET.ParseError:
|
||||
pass
|
||||
@@ -84,6 +95,43 @@ class QImageSvgRenderer(QtSvg.QSvgRenderer):
|
||||
res = super().load(self._svg.encode())
|
||||
return res
|
||||
|
||||
def resize(self, new_height, new_width=None):
|
||||
|
||||
if not self.isValid():
|
||||
log.error("QSvgRenderer is not valid")
|
||||
return
|
||||
|
||||
size = self.defaultSize()
|
||||
height = size.height()
|
||||
width = size.width()
|
||||
|
||||
if not new_width:
|
||||
new_width = round(width / height * new_height)
|
||||
|
||||
add_attr = []
|
||||
svg_header, svg_tag, svg_data = re.split(r'(<svg[^>]*>)', self._svg, maxsplit=1)
|
||||
|
||||
attr = 'height="{}"'.format(new_height)
|
||||
svg_tag, count = re.subn(r'height="[^"]*"', attr, svg_tag, count=1)
|
||||
if not count:
|
||||
add_attr.append(attr)
|
||||
|
||||
attr = 'width="{}"'.format(new_width)
|
||||
svg_tag, count = re.subn(r'width="[^"]*"', attr, svg_tag, count=1)
|
||||
if not count:
|
||||
add_attr.append(attr)
|
||||
|
||||
if 'viewBox="' not in svg_tag:
|
||||
add_attr.append('viewBox="0 0 {} {}"'.format(width, height))
|
||||
|
||||
if add_attr:
|
||||
svg_tag = svg_tag.replace('<svg', '<svg ' + ' '.join(add_attr), 1)
|
||||
|
||||
svg_image = svg_header + svg_tag + svg_data
|
||||
res = super().load(svg_image.encode())
|
||||
if res is False:
|
||||
log.error("Could not resize QSvgRenderer")
|
||||
|
||||
def svg(self):
|
||||
"""
|
||||
:returns: SVG source code
|
||||
|
||||
@@ -20,7 +20,10 @@
|
||||
import json
|
||||
import os
|
||||
import urllib
|
||||
import shutil
|
||||
from ssl import CertificateError
|
||||
|
||||
from gns3.controller import Controller
|
||||
from ..local_config import LocalConfig
|
||||
from ..local_server_config import LocalServerConfig
|
||||
from ..settings import LOCAL_SERVER_SETTINGS
|
||||
@@ -43,13 +46,17 @@ class Config:
|
||||
:params path: Path of the configuration file, otherwise detect it on the system
|
||||
"""
|
||||
|
||||
self.path = path
|
||||
if self.path is None:
|
||||
self.path = LocalConfig.instance().configFilePath()
|
||||
self._path = path
|
||||
if self._path is None:
|
||||
self._path = LocalConfig.instance().configFilePath()
|
||||
|
||||
with open(self.path, encoding="utf-8") as f:
|
||||
with open(self._path, encoding="utf-8") as f:
|
||||
self._config = json.load(f)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def images_dir(self):
|
||||
"""
|
||||
@@ -91,19 +98,33 @@ class Config:
|
||||
:param name: Appliance name
|
||||
:returns: True if name is not already used
|
||||
"""
|
||||
for item in self._config["Qemu"].get("vms", []):
|
||||
|
||||
appliance_names = []
|
||||
if "Qemu" in self._config:
|
||||
appliance_names.extend(self._config["Qemu"].get("vms", []))
|
||||
if "IOU" in self._config:
|
||||
appliance_names.extend(self._config["IOU"].get("devices", []))
|
||||
if "Dynamips" in self._config:
|
||||
appliance_names.extend(self._config["Dynamips"].get("routers", []))
|
||||
if "Docker" in self._config:
|
||||
appliance_names.extend(self._config["Docker"].get("containers", []))
|
||||
for item in appliance_names:
|
||||
if item["name"] == name:
|
||||
return False
|
||||
return True
|
||||
|
||||
def add_appliance(self, appliance_config, server):
|
||||
def add_appliance(self, appliance_config, server, controller_symbols=None):
|
||||
"""
|
||||
Add appliance to the user configuration
|
||||
|
||||
:param appliance_config: Dictionary with appliance configuration
|
||||
:param server
|
||||
:param controller_symbols: Symbols located on controller
|
||||
"""
|
||||
|
||||
if controller_symbols is None:
|
||||
controller_symbols = []
|
||||
|
||||
new_config = {
|
||||
"server": server,
|
||||
"name": appliance_config["name"]
|
||||
@@ -124,7 +145,7 @@ class Config:
|
||||
new_config["category"] = 1
|
||||
|
||||
if "symbol" in appliance_config:
|
||||
new_config["symbol"] = self._set_symbol(appliance_config["symbol"])
|
||||
new_config["symbol"] = self._set_symbol(appliance_config["symbol"], controller_symbols)
|
||||
|
||||
if new_config.get("symbol") is None:
|
||||
if appliance_config["category"] == "guest":
|
||||
@@ -157,7 +178,7 @@ class Config:
|
||||
if "docker" in appliance_config:
|
||||
self._add_docker_config(new_config, appliance_config)
|
||||
return
|
||||
raise ConfigException("{} no configuration found for know emulators".format(new_config["name"]))
|
||||
raise ConfigException("{} no configuration found for known emulators".format(new_config["name"]))
|
||||
|
||||
def _add_docker_config(self, new_config, appliance_config):
|
||||
new_config["adapters"] = appliance_config["docker"]["adapters"]
|
||||
@@ -167,6 +188,7 @@ class Config:
|
||||
new_config["console_type"] = appliance_config["docker"].get("console_type", "telnet")
|
||||
new_config["console_http_port"] = appliance_config["docker"].get("console_http_port", 80)
|
||||
new_config["console_http_path"] = appliance_config["docker"].get("console_http_path", "/")
|
||||
new_config["extra_hosts"] = appliance_config["docker"].get("extra_hosts", "")
|
||||
self._config["Docker"]["containers"].append(new_config)
|
||||
|
||||
def _add_dynamips_config(self, new_config, appliance_config):
|
||||
@@ -257,8 +279,8 @@ class Config:
|
||||
else:
|
||||
new_config["qemu_path"] = "qemu-system-{}".format(appliance_config["qemu"]["arch"])
|
||||
|
||||
if "boot_priority" in appliance_config:
|
||||
new_config["boot_priority"] = appliance_config["boot_priority"]
|
||||
if "boot_priority" in appliance_config["qemu"]:
|
||||
new_config["boot_priority"] = appliance_config["qemu"]["boot_priority"]
|
||||
|
||||
if "first_port_name" in appliance_config:
|
||||
new_config["first_port_name"] = appliance_config["first_port_name"]
|
||||
@@ -276,9 +298,9 @@ class Config:
|
||||
self._config["Qemu"].setdefault("vms", [])
|
||||
self._config["Qemu"]["vms"].append(new_config)
|
||||
|
||||
def _set_symbol(self, symbol):
|
||||
def _set_symbol(self, symbol, controller_symbols):
|
||||
"""
|
||||
Download symbol for the web if need
|
||||
Check if exists on controller or download symbol from the web if needed
|
||||
"""
|
||||
|
||||
# GNS3 builtin symbol
|
||||
@@ -289,11 +311,25 @@ class Config:
|
||||
if os.path.exists(path):
|
||||
return os.path.basename(path)
|
||||
|
||||
is_symbol_on_controller = len([s for s in controller_symbols
|
||||
if s['symbol_id'] == symbol]) > 0
|
||||
|
||||
if is_symbol_on_controller:
|
||||
cached = Controller.instance().getStaticCachedPath(symbol)
|
||||
if os.path.exists(cached):
|
||||
try:
|
||||
shutil.copy(cached, path)
|
||||
except IOError as e:
|
||||
log.warning("Cannot copy cached symbol from `{}` to `{}` due `{}`".format(
|
||||
cached, path, str(e)
|
||||
))
|
||||
return symbol
|
||||
|
||||
url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol)
|
||||
try:
|
||||
urllib.request.urlretrieve(url, path)
|
||||
return os.path.basename(path)
|
||||
except OSError:
|
||||
except (OSError, CertificateError):
|
||||
return None
|
||||
|
||||
def _relative_image_path(self, image_dir_type, path):
|
||||
|
||||
@@ -95,20 +95,28 @@ class Image:
|
||||
self._md5sum = from_cache
|
||||
return self._md5sum
|
||||
|
||||
if os.path.exists(self.path + ".md5sum"):
|
||||
with open(self.path + ".md5sum", encoding="utf-8") as f:
|
||||
self._md5sum = f.read()
|
||||
return self._md5sum
|
||||
md5_file = self.path + ".md5sum"
|
||||
if os.path.exists(md5_file):
|
||||
try:
|
||||
with open(md5_file) as f:
|
||||
self._md5sum = f.read().strip()
|
||||
return self._md5sum
|
||||
except (OSError, UnicodeDecodeError) as e:
|
||||
log.debug("Could not read '{}': {}".format(md5_file, e))
|
||||
|
||||
if not os.path.isfile(self.path):
|
||||
try:
|
||||
if not os.path.isfile(self.path):
|
||||
return None
|
||||
m = hashlib.md5()
|
||||
with open(self.path, "rb") as f:
|
||||
while True:
|
||||
buf = f.read(4096)
|
||||
if not buf:
|
||||
break
|
||||
m.update(buf)
|
||||
except (OSError, PermissionError) as e:
|
||||
log.debug("Cannot access '{}': {}".format(self.path, e))
|
||||
return None
|
||||
m = hashlib.md5()
|
||||
with open(self.path, "rb") as f:
|
||||
while True:
|
||||
buf = f.read(4096)
|
||||
if not buf:
|
||||
break
|
||||
m.update(buf)
|
||||
self._md5sum = m.hexdigest()
|
||||
Image._cache[self.path] = self._md5sum
|
||||
return self._md5sum
|
||||
|
||||
@@ -87,11 +87,11 @@ class Registry(QtCore.QObject):
|
||||
|
||||
for directory in self._images_dirs:
|
||||
log.debug("Search images %s (%s) in %s", filename, md5sum, directory)
|
||||
if os.path.exists(directory):
|
||||
for file in os.listdir(directory):
|
||||
if not file.endswith(".md5sum") and not file.startswith("."):
|
||||
path = os.path.join(directory, file)
|
||||
try:
|
||||
try:
|
||||
if os.path.exists(directory):
|
||||
for file in os.listdir(directory):
|
||||
if not file.endswith(".md5sum") and not file.startswith("."):
|
||||
path = os.path.join(directory, file)
|
||||
if os.path.isfile(path):
|
||||
if md5sum is None:
|
||||
if filename == os.path.basename(path):
|
||||
@@ -105,7 +105,6 @@ class Registry(QtCore.QObject):
|
||||
if image.md5sum == md5sum:
|
||||
log.debug("Found images %s (%s) in %s", filename, md5sum, image.path)
|
||||
return image
|
||||
except (OSError, PermissionError) as e:
|
||||
log.error("Can't scan {}: {}".format(path, str(e)))
|
||||
|
||||
except (OSError, PermissionError) as e:
|
||||
log.error("Cannot scan {}: {}".format(path, e))
|
||||
return None
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"enum": [
|
||||
"router",
|
||||
"multilayer_switch",
|
||||
"switch",
|
||||
"firewall",
|
||||
"guest"
|
||||
],
|
||||
@@ -131,6 +132,10 @@
|
||||
"console_http_path": {
|
||||
"description": "Path of the web interface",
|
||||
"type": "string"
|
||||
},
|
||||
"extra_hosts": {
|
||||
"description": "Hosts which will be written to /etc/hosts into container" ,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -283,8 +288,8 @@
|
||||
"title": "Type of console connection for the administration of the appliance"
|
||||
},
|
||||
"boot_priority": {
|
||||
"enum": ["d", "c", "dc", "cd", "n", "nc", "nd", "cn", "dn"],
|
||||
"title": "Optional define the disk boot priory. Refer to -boot option in qemu manual for more details."
|
||||
"enum": ["c", "d", "n", "cn", "cd", "dn", "dc", "nc", "nd"],
|
||||
"title": "Disk boot priority. Refer to -boot option in qemu manual for more details."
|
||||
},
|
||||
"kernel_command_line": {
|
||||
"type": "string",
|
||||
|
||||
@@ -23,6 +23,7 @@ import os
|
||||
import sys
|
||||
import uuid
|
||||
import platform
|
||||
import shutil
|
||||
|
||||
# Default projects directory location
|
||||
DEFAULT_PROJECTS_PATH = os.path.normpath(os.path.expanduser("~/GNS3/projects"))
|
||||
@@ -41,6 +42,7 @@ DEFAULT_APPLIANCES_PATH = os.path.normpath(os.path.expanduser("~/GNS3/appliances
|
||||
|
||||
DEFAULT_LOCAL_SERVER_HOST = "127.0.0.1"
|
||||
DEFAULT_LOCAL_SERVER_PORT = 3080
|
||||
DEFAULT_DELAY_CONSOLE_ALL = 500
|
||||
|
||||
# Pre-configured Telnet console commands on various OSes
|
||||
if sys.platform.startswith("win"):
|
||||
@@ -66,16 +68,27 @@ if sys.platform.startswith("win"):
|
||||
'ZOC 6': r'"{}\ZOC6\zoc.exe" "/TELNET:%h:%p" /TABBED "/TITLE:%d"'.format(program_files_x86)}
|
||||
|
||||
# default on Windows
|
||||
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Putty (included with GNS3)"]
|
||||
if shutil.which("Solar-PuTTY.exe"):
|
||||
# Solar-Putty is the default if it is installed.
|
||||
PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Solar-Putty (included with GNS3)"] = 'Solar-PuTTY.exe --telnet --hostname %h --port %p --name "%d"'
|
||||
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Solar-Putty (included with GNS3)"]
|
||||
DEFAULT_DELAY_CONSOLE_ALL = 1500
|
||||
else:
|
||||
PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Solar-Putty (included with GNS3 downloaded from gns3.com)"] = 'Solar-PuTTY.exe --telnet --hostname %h --port %p --name "%d"'
|
||||
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Putty (included with GNS3)"]
|
||||
|
||||
elif sys.platform.startswith("darwin"):
|
||||
# Mac OS X
|
||||
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {
|
||||
'Terminal': r"""osascript -e 'tell application "Terminal"'"""
|
||||
'Terminal': r"""osascript"""
|
||||
r""" -e 'set posix_path to do shell script "echo \"$PATH\""'"""
|
||||
r""" -e 'tell application "Terminal"'"""
|
||||
r""" -e 'activate'"""
|
||||
r""" -e 'do script "echo -n -e \"\\033]0;%d\\007\"; clear; PATH=\"" & (system attribute "PATH") & "\" telnet %h %p ; exit"'"""
|
||||
r""" -e 'do script "echo -n -e \"\\033]0;%d\\007\"; clear; PATH=" & quoted form of posix_path & " telnet %h %p ; exit"'"""
|
||||
r""" -e 'end tell'""",
|
||||
'Terminal tabbed (experimental)': r"""osascript -e 'tell application "Terminal"'"""
|
||||
'Terminal tabbed (experimental)': r"""osascript"""
|
||||
r""" -e 'set posix_path to do shell script "echo \"$PATH\""'"""
|
||||
r""" -e 'tell application "Terminal"'"""
|
||||
r""" -e 'activate'"""
|
||||
r""" -e 'tell application "System Events" to tell process "Terminal" to keystroke "t" using command down'"""
|
||||
r""" -e 'if (the (count of the window) = 0) then'"""
|
||||
@@ -87,9 +100,11 @@ elif sys.platform.startswith("darwin"):
|
||||
r""" -e 'repeat while the busy of window 1 = true'"""
|
||||
r""" -e 'delay 0.01'"""
|
||||
r""" -e 'end repeat'"""
|
||||
r""" -e 'do script "echo -n -e \"\\033]0;%d\\007\"; clear; PATH=\"" & (system attribute "PATH") & "\" telnet %h %p ; exit" in window 1'"""
|
||||
r""" -e 'do script "echo -n -e \"\\033]0;%d\\007\"; clear; PATH=" & quoted form of posix_path & " telnet %h %p ; exit" in window 1'"""
|
||||
r""" -e 'end tell'""",
|
||||
'iTerm2 2.x': r"""osascript -e 'tell application "iTerm"'"""
|
||||
'iTerm2 2.x': r"""osascript"""
|
||||
r""" -e 'set posix_path to do shell script "echo \"$PATH\""'"""
|
||||
r""" -e 'tell application "iTerm"'"""
|
||||
r""" -e 'activate'"""
|
||||
r""" -e 'if (count of terminals) = 0 then'"""
|
||||
r""" -e ' set t to (make new terminal)'"""
|
||||
@@ -99,12 +114,14 @@ elif sys.platform.startswith("darwin"):
|
||||
r""" -e 'tell t'"""
|
||||
r""" -e ' set s to (make new session at the end of sessions)'"""
|
||||
r""" -e ' tell s'"""
|
||||
r""" -e ' exec command "sh -c \"PATH=\\\"" & (system attribute "PATH") & "\\\" telnet %h %p"'"""
|
||||
|
||||
r""" -e ' exec command "sh"'"""
|
||||
r""" -e ' write text "PATH=" & quoted form of posix_path & " exec telnet %h %p"'"""
|
||||
r""" -e ' end tell'"""
|
||||
r""" -e 'end tell'"""
|
||||
r""" -e 'end tell'""",
|
||||
'iTerm2 3.x': r"""osascript -e 'tell application "iTerm"'"""
|
||||
'iTerm2 3.x': r"""osascript"""
|
||||
r""" -e 'set posix_path to do shell script "echo \"$PATH\""'"""
|
||||
r""" -e 'tell application "iTerm"'"""
|
||||
r""" -e 'activate'"""
|
||||
r""" -e 'if (count of windows) = 0 then'"""
|
||||
r""" -e ' set t to (create window with default profile)'"""
|
||||
@@ -116,7 +133,7 @@ elif sys.platform.startswith("darwin"):
|
||||
r""" -e ' set s to current session'"""
|
||||
r""" -e ' tell s'"""
|
||||
r""" -e ' set name to "%d"'"""
|
||||
r""" -e ' write text "PATH=\"" & (system attribute "PATH") & "\" exec telnet %h %p"'"""
|
||||
r""" -e ' write text "PATH=" & quoted form of posix_path & " exec telnet %h %p"'"""
|
||||
r""" -e ' end tell'"""
|
||||
r""" -e 'end tell'"""
|
||||
r""" -e 'end tell'""",
|
||||
@@ -153,7 +170,7 @@ if sys.platform.startswith("win"):
|
||||
# Windows
|
||||
PRECONFIGURED_VNC_CONSOLE_COMMANDS = {
|
||||
'TightVNC (included with GNS3)': 'tvnviewer.exe %h:%p',
|
||||
'UltraVNC': 'C:\\Program Files\\uvnc bvba\\UltraVNC\\vncviewer.exe %h:%p'
|
||||
'UltraVNC': r'"{}\uvnc bvba\UltraVNC\vncviewer.exe" %h:%p'.format(program_files)
|
||||
}
|
||||
|
||||
# default Windows VNC console command
|
||||
@@ -189,11 +206,11 @@ else:
|
||||
if sys.platform.startswith("win"):
|
||||
# Windows
|
||||
PRECONFIGURED_SPICE_CONSOLE_COMMANDS = {
|
||||
'Remote Viewer (included with GNS3)': '"c:\\Program Files\\VirtViewer v5.0-256\\bin\\remote-viewer.exe" spice://%h:%p',
|
||||
'Remote Viewer': r'"{}\VirtViewer v7.0-256\bin\remote-viewer.exe" spice://%h:%p'.format(program_files),
|
||||
}
|
||||
|
||||
# default Windows SPICE console command
|
||||
DEFAULT_SPICE_CONSOLE_COMMAND = PRECONFIGURED_SPICE_CONSOLE_COMMANDS['Remote Viewer (included with GNS3)']
|
||||
DEFAULT_SPICE_CONSOLE_COMMAND = PRECONFIGURED_SPICE_CONSOLE_COMMANDS['Remote Viewer']
|
||||
|
||||
elif sys.platform.startswith("darwin"):
|
||||
# Mac OS X
|
||||
@@ -217,8 +234,8 @@ WIRESHARK_NORMAL_CAPTURE = "Wireshark Traditional Capture"
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE = "Wireshark Live Traffic Capture"
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: "{}\Wireshark\wireshark.exe %c".format(os.environ["PROGRAMFILES"]),
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail.exe -f -c +0b %c | "{}\Wireshark\wireshark.exe" -o "gui.window_title:%d" -k -i -'.format(os.environ["PROGRAMFILES"])}
|
||||
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: "{}\Wireshark\wireshark.exe %c".format(program_files),
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail.exe -f -c +0b %c | "{}\Wireshark\wireshark.exe" -o "gui.window_title:%d" -k -i -'.format(program_files)}
|
||||
|
||||
elif sys.platform.startswith("darwin"):
|
||||
# Mac OS X
|
||||
@@ -237,9 +254,9 @@ else:
|
||||
DEFAULT_PACKET_CAPTURE_READER_COMMAND = PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS[WIRESHARK_LIVE_TRAFFIC_CAPTURE]
|
||||
|
||||
DEFAULT_PACKET_CAPTURE_ANALYZER_COMMAND = ""
|
||||
if sys.platform.startswith("win") and "PROGRAMFILES(X86)" in os.environ:
|
||||
if sys.platform.startswith("win"):
|
||||
# Windows 64-bit
|
||||
DEFAULT_PACKET_CAPTURE_ANALYZER_COMMAND = r'"{}\SolarWinds\ResponseTimeViewer\ResponseTimeViewer.exe" %c'.format(os.environ["PROGRAMFILES(X86)"])
|
||||
DEFAULT_PACKET_CAPTURE_ANALYZER_COMMAND = r'"{}\SolarWinds\ResponseTimeViewer\ResponseTimeViewer.exe" %c'.format(program_files_x86)
|
||||
|
||||
STYLES = ["Charcoal", "Classic", "Legacy"]
|
||||
|
||||
@@ -259,7 +276,7 @@ GENERAL_SETTINGS = {
|
||||
"telnet_console_command": DEFAULT_TELNET_CONSOLE_COMMAND,
|
||||
"vnc_console_command": DEFAULT_VNC_CONSOLE_COMMAND,
|
||||
"spice_console_command": DEFAULT_SPICE_CONSOLE_COMMAND,
|
||||
"delay_console_all": 500,
|
||||
"delay_console_all": DEFAULT_DELAY_CONSOLE_ALL,
|
||||
"hide_getting_started_dialog": False,
|
||||
"hide_setup_wizard": False,
|
||||
"hide_new_appliance_template_button": False,
|
||||
@@ -281,6 +298,7 @@ NODES_VIEW_SETTINGS = {
|
||||
GRAPHICS_VIEW_SETTINGS = {
|
||||
"scene_width": 2000,
|
||||
"scene_height": 1000,
|
||||
"grid_size": 75,
|
||||
"draw_rectangle_selected_item": False,
|
||||
"draw_link_status_points": True,
|
||||
"default_label_font": "TypeWriter,10,-1,5,75,0,0,0,0,0",
|
||||
@@ -289,7 +307,9 @@ GRAPHICS_VIEW_SETTINGS = {
|
||||
"show_layers": False,
|
||||
"snap_to_grid": False,
|
||||
"show_grid": False,
|
||||
"show_interface_labels": False
|
||||
"show_interface_labels": False,
|
||||
"show_interface_labels_on_new_project": False,
|
||||
"limit_size_node_symbols": True
|
||||
}
|
||||
|
||||
LOCAL_SERVER_SETTINGS = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user