Compare commits

...

84 Commits

Author SHA1 Message Date
grossmj
97b777ceea Release v2.2.43 2023-09-19 20:16:52 +07:00
grossmj
c06e534935 Merge branch 'master' into 2.2 2023-09-19 20:08:11 +07:00
grossmj
025276f8a7 Upgrade sentry-sdk and truststore 2023-09-19 20:07:57 +07:00
grossmj
6777961d29 Add KiTTY to preconfigured telnet consoles. Fixes #3507 2023-09-18 20:23:09 +07:00
grossmj
7f6cace0d5 Fix generic icon in Wayland. Ref #3501 2023-09-15 16:08:45 +07:00
Jeremy Grossmann
1a739c0c37 Merge pull request #3490 from GNS3/appliance-v8-support
Support for appliance version 8 format
2023-08-16 14:18:59 +10:00
grossmj
6d855045ef Show installation instructions when available and fix regression when installing Docker appliance. 2023-08-16 14:11:55 +10:00
grossmj
fef734bbbe Finalize appliance v8 support and add tests. 2023-08-16 00:30:02 +10:00
grossmj
b079443735 Merge remote-tracking branch 'origin/appliance-v8-support' into appliance-v8-support 2023-08-14 12:01:18 +10:00
grossmj
4a32ae9736 Drop "kvm" field. 2023-08-14 12:01:09 +10:00
Jeremy Grossmann
9793d00131 Merge branch '2.2' into appliance-v8-support 2023-08-13 12:21:18 +10:00
grossmj
2b7840279a Downgrade jsonschema 2023-08-12 17:51:24 +10:00
grossmj
9243083321 Upgrade dependencies 2023-08-12 17:47:48 +10:00
Jeremy Grossmann
3a8b3e5c4a Merge pull request #3506 from GNS3/fix/3505
Use importlib instead of pkg_resources
2023-08-12 17:26:51 +10:00
grossmj
e2168a3c81 Use importlib instead of pkg_resources 2023-08-12 17:20:33 +10:00
grossmj
01deb01e6a Upgrade to PyQt 5.15.9 and pywin32 2023-08-11 18:13:41 +10:00
grossmj
1133ee6e1b Development on v2.2.43.dev1 2023-08-09 22:15:15 +10:00
Jeremy Grossmann
7512ffec64 Merge pull request #3503 from GNS3/release-v2.2.42
Release v2.2.42
2023-08-09 22:08:18 +10:00
grossmj
3527e5551c Release v2.2.42 2023-08-09 21:11:57 +10:00
grossmj
72960f8f2b Enable system certificate store later in the code and bump version to 2.2.42.dev4 2023-08-08 17:22:27 +10:00
grossmj
8abb502c72 Use the system's certificate store for SSL connections 2023-08-07 21:33:25 +10:00
grossmj
08c729e83a Upgrade dependencies 2023-08-06 20:39:27 +10:00
grossmj
aac004bd2f Use certifi to get SSL root certificates 2023-08-06 20:37:10 +10:00
grossmj
70677d8f18 Bump version to 2.2.42.dev3 2023-08-06 18:14:22 +10:00
Jeremy Grossmann
fba1ff4208 Merge pull request #3502 from GNS3/use-bundled-cacert
Use bundled cacert file for frozen app
2023-08-05 22:37:08 +10:00
grossmj
e4edbefc23 Use bundled cacert file on Windows and macOS 2023-08-05 22:21:08 +10:00
grossmj
d93f9afe74 Bump version to 2.2.42.dev2 2023-08-05 20:16:30 +10:00
grossmj
162d197e36 Give a node some time to start before opening the console (for console auto start). Fixes #3474 2023-08-02 11:08:58 +10:00
grossmj
5c21dd8a2f Merge branch 'master' into 2.2 2023-08-01 15:50:51 +10:00
Jeremy Grossmann
aa9b9d3b0b Merge pull request #3364 from AbdelbakiBoukerche/feature_rounded_rectangle
Rounded Rectangle
2023-08-01 15:46:32 +10:00
grossmj
eae9eec15b Support for horizontal and vertical corner radius 2023-08-01 15:34:30 +10:00
Jeremy Grossmann
a3bf832721 Merge branch 'master' into feature_rounded_rectangle 2023-07-31 18:34:50 +10:00
Jeremy Grossmann
67890d74d9 Merge pull request #3500 from GNS3/fix/3449
Support for gnome-terminal tabs to be opened in the same window
2023-07-31 18:30:22 +10:00
grossmj
ab4325f951 Remove warning to set open new terminals in tabs for gnome-terminal 2023-07-31 12:32:49 +10:00
grossmj
2a947b9cc5 Add comments for gnome-terminal special case 2023-07-31 01:44:39 +10:00
grossmj
601c082288 Use Mate Terminal by default if installed on Debian, Ubuntu and Linux Mint. 2023-07-31 01:40:20 +10:00
grossmj
7701d57bd0 Check that gnome-terminal is configured to open new terminals in tabs. 2023-07-31 01:34:59 +10:00
grossmj
f0b4148a20 Support for gnome-terminal tabs to be opened in the same window. 2023-07-30 22:15:38 +10:00
grossmj
2fdcbafbc1 Revert "Support for Python 3.12"
This reverts commit 5d82cea935.
2023-07-30 17:50:21 +10:00
grossmj
5d82cea935 Support for Python 3.12 2023-07-30 17:48:30 +10:00
grossmj
b0e3e93c41 Remove import urllib3 and let sentry_sdk import and patch it. Fixes https://github.com/GNS3/gns3-gui/issues/3498 2023-07-30 17:42:52 +10:00
grossmj
535f53737d Merge remote-tracking branch 'origin/master' into 2.2 2023-07-23 12:35:21 +10:00
Jeremy Grossmann
354f3eecec Merge pull request #3497 from kevinchevreuil/master
Add import sys in sudo.py
2023-07-23 12:04:29 +10:00
Kevin Chevreuil - Kaisen
35a6a5c8c7 Add import sys in sudo.py 2023-07-22 23:12:34 +02:00
grossmj
23cba0a28d Development on 2.2.42.dev1 2023-07-12 18:26:26 +10:00
Jeremy Grossmann
9c3d7bc95a Merge pull request #3495 from GNS3/release-v2.2.41
Release v2.2.41
2023-07-12 18:24:05 +10:00
grossmj
cf2802b15a Release v2.2.41 2023-07-12 17:07:39 +10:00
grossmj
b162c55078 Merge remote-tracking branch 'origin/master' into 2.2 2023-07-12 17:03:50 +10:00
grossmj
bb42b0ed0b Change chown in authorize_ubridge.py 2023-07-12 13:28:24 +10:00
grossmj
e108b5194d Bump version to 2.2.41.dev3 2023-07-12 13:26:22 +10:00
grossmj
e9ef8735be Use a small executable to set the correct permissions on uBridge on macOS 2023-07-12 12:40:53 +10:00
grossmj
e8e90bb16a Add debugging for AuthorizationExecuteWithPrivileges 2023-07-11 21:23:15 +10:00
grossmj
49f77930f4 Use alternative method to setuid uBridge on macOS 2023-07-11 18:27:19 +10:00
grossmj
a58451a552 Update to support template_type & template_properties 2023-07-11 17:38:00 +10:00
grossmj
0a43b9e6e9 Fix tests 2023-07-09 20:18:22 +10:00
Jeremy Grossmann
4f32619ed8 Merge pull request #3493 from GNS3/remove-analytics
Remove sending usage stats
2023-07-09 19:04:40 +10:00
grossmj
20740748c1 Remove sending stats to GA 2023-07-09 18:55:31 +10:00
grossmj
8a5ab6b374 Update schema for appliance version 8 2023-07-09 14:10:15 +10:00
grossmj
e11ce27f7b Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483 2023-07-06 17:16:05 +10:00
grossmj
4b7cf4e553 Support legacy "idlepc" field 2023-07-03 19:12:44 +10:00
grossmj
b72358461c Add support for appliance version 8 format 2023-06-25 16:54:12 +09:30
Jeremy Grossmann
2987bcf91a Merge pull request #3489 from GNS3/backport-uefi-boot-mode
Backport UEFI boot mode support for Qemu VMs
2023-06-23 11:31:49 +09:30
grossmj
5e97bc0f86 Backport UEFI boot mode support for Qemu VMs 2023-06-23 11:18:25 +09:30
grossmj
e3a3de5df7 Add debug for dropEvent. Ref https://github.com/GNS3/gns3-server/issues/2242 2023-06-20 14:49:25 +09:30
grossmj
e1693ce113 Developement on v2.2.41.dev2 2023-06-10 21:40:39 +09:30
Jeremy Grossmann
522091d219 Merge pull request #3481 from GNS3/release-v2.2.40.1
Release v2.2.40.1
2023-06-10 21:36:42 +09:30
grossmj
1446748934 Release v2.2.40.1 2023-06-10 20:06:40 +09:30
grossmj
3f0ce380e8 Merge branch 'master' into 2.2 2023-06-10 17:22:50 +09:30
grossmj
bd71383354 Development on v2.2.41.dev1 2023-06-06 12:43:20 +09:30
Jeremy Grossmann
9649895378 Merge pull request #3479 from GNS3/release-v2.2.40
Release v2.2.40
2023-06-06 12:41:46 +09:30
grossmj
8579ffa20a Release v2.2.40 2023-06-06 10:23:42 +09:30
grossmj
3206743329 Merge branch 'master' into 2.2 2023-06-06 10:19:45 +09:30
grossmj
29c87b6e96 Change log messages for Websocket errors 2023-06-03 21:05:26 +09:30
grossmj
93b2721d6a Do not proceed if an appliance symbol cannot be downloaded. Ref #3466 2023-05-23 16:31:51 +08:00
grossmj
1ff369683f Revert "Fix open IPv6 address for HTTP consoles. Fixes #3448"
This reverts commit 7c56a2467c.
2023-05-22 19:39:03 +08:00
grossmj
7c56a2467c Fix open IPv6 address for HTTP consoles. Fixes #3448 2023-05-22 16:46:57 +08:00
grossmj
59ef34c17d Delete a node or link from topology summary view using Delete key. Ref #3445 2023-05-14 21:19:19 +08:00
grossmj
d1fae54049 Fix "Start the capture visualization program" checkbox works only one (first) time for a given link. Fixes #3442 2023-05-14 20:38:34 +08:00
grossmj
49bd61f769 Let the selected link style applied when editing a link. Fixes #3460 2023-05-14 15:33:08 +08:00
grossmj
9a4faddd10 Fix hovered color shown in style editing dialog. Fixes #3460 2023-05-14 14:52:27 +08:00
Jeremy Grossmann
7654681a94 Merge pull request #3468 from GNS3/release-v2.2.39
Release v2.2.39
2023-05-08 20:28:43 +08:00
grossmj
9bfecde957 Development on v2.2.40.dev1 2023-05-08 20:26:40 +08:00
Jeremy Grossmann
7418c190a8 Merge pull request #3447 from GNS3/2.2
Release v2.2.38
2023-02-28 14:22:10 +08:00
Abdelbaki Boukerche
181bf3f360 Rounded Rectangle 2022-08-04 18:20:54 +01:00
67 changed files with 2130 additions and 488 deletions

View File

@@ -1,5 +1,45 @@
# Change Log
## 2.2.43 19/09/2023
* Add KiTTY to preconfigured telnet consoles. Fixes #3507
* Fix generic icon in Wayland. Ref #3501
* Support for appliance format version 8.
* Use importlib instead of pkg_resources
* Upgrade to PyQt 5.15.9 and pywin32
* Add support for appliance version 8 format
## 2.2.42 09/08/2023
* Use the system's certificate store for SSL connections
* Give a node some time to start before opening the console (for console auto start). Fixes #3474
* Use Mate Terminal by default if installed on Debian, Ubuntu and Linux Mint.
* Support for gnome-terminal tabs to be opened in the same window.
* Remove import urllib3 and let sentry_sdk import and patch it. Fixes https://github.com/GNS3/gns3-gui/issues/3498
* Add import sys in sudo.py
* Rounded Rectangle support
## 2.2.41 12/07/2023
* Use alternative method to set the correct permissions for uBridge on macOS
* Remove sending stats to GA
* Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483
* Backport UEFI boot mode support for Qemu VMs
* Add debug for dropEvent. Ref https://github.com/GNS3/gns3-server/issues/2242
## 2.2.40.1 10/06/2023
* No changes
## 2.2.40 06/06/2023
* Change log messages for Websocket errors
* Do not proceed if an appliance symbol cannot be downloaded. Ref #3466
* Delete a node or link from topology summary view using Delete key. Ref #3445
* Fix "Start the capture visualization program" checkbox works only one (first) time for a given link. Fixes #3442
* Let the selected link style applied when editing a link. Fixes #3460
* Fix hovered color shown in style editing dialog. Fixes #3460
## 2.2.39 08/05/2023
* Fix nodes are not snapped to the grid at the moment of creation

View File

@@ -46,6 +46,9 @@ class Application(QtWidgets.QApplication):
super().__init__(argv)
# this is tell Wayland what is the name of the desktop file (gns3.desktop)
self.setDesktopFileName("gns3")
# this info is necessary for QSettings
self.setOrganizationName("GNS3")
self.setOrganizationDomain("gns3.net")

View File

@@ -449,7 +449,7 @@ class Controller(QtCore.QObject):
@qslot
def _websocket_error(self, error):
if self._notification_stream:
log.error("Websocket notification stream error: {}".format(self._notification_stream.errorString()))
log.error("Websocket controller notification stream error: {}".format(self._notification_stream.errorString()))
self._notification_stream = None
self._startListenNotifications()

View File

@@ -15,12 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import platform
import struct
import distro
try:
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
@@ -29,7 +23,12 @@ except ImportError:
# Sentry SDK is not installed with deb package in order to simplify packaging
SENTRY_SDK_AVAILABLE = False
from .utils.get_resource import get_resource
import sys
import os
import platform
import struct
import distro
from .version import __version__, __version_info__
import logging
@@ -51,7 +50,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "https://de4e2c3e542f47149bf89dffb48491ae@o19455.ingest.sentry.io/38506"
DSN = "https://57454675a266a9d705fd505947a81b5c@o19455.ingest.sentry.io/38506"
_instance = None
def __init__(self):
@@ -64,22 +63,16 @@ class CrashReport:
self._sentry_initialized = False
if SENTRY_SDK_AVAILABLE:
cacert = None
if hasattr(sys, "frozen"):
cacert_resource = get_resource("cacert.pem")
if cacert_resource is not None and os.path.isfile(cacert_resource):
cacert = cacert_resource
else:
log.error("The SSL certificate bundle file '{}' could not be found".format(cacert_resource))
# Don't send log records as events.
sentry_logging = LoggingIntegration(level=logging.INFO, event_level=None)
sentry_sdk.init(dsn=CrashReport.DSN,
release=__version__,
ca_certs=cacert,
default_integrations=False,
integrations=[sentry_logging])
try:
sentry_sdk.init(dsn=CrashReport.DSN,
release=__version__,
default_integrations=False,
integrations=[sentry_logging])
except Exception as e:
log.error("Crash report could not be sent: {}".format(e))
return
tags = {
"os:name": platform.system(),

View File

@@ -94,9 +94,11 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.setWindowTitle("Install {} appliance".format(self._appliance["name"]))
# add a custom button to show appliance information
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Appliance info")
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
self.customButtonClicked.connect(self._showApplianceInfoSlot)
if self._appliance["registry_version"] < 8:
# FIXME: show appliance info for v8
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Appliance info")
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
self.customButtonClicked.connect(self._showApplianceInfoSlot)
# customize the server selection
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
@@ -144,18 +146,9 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if self.page(page_id) == self.uiServerWizardPage:
Controller.instance().getSymbols(self._getSymbolsCallback)
if "qemu" in self._appliance:
emulator_type = "qemu"
elif "iou" in self._appliance:
emulator_type = "iou"
elif "docker" in self._appliance:
emulator_type = "docker"
elif "dynamips" in self._appliance:
emulator_type = "dynamips"
else:
QtWidgets.QMessageBox.warning(self, "Appliance", "Could not determine the emulator type")
template_type = self._appliance.template_type()
if not template_type:
raise ApplianceError("No template type found for appliance {}".format(self._appliance["name"]))
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
is_win = ComputeManager.instance().localPlatform().startswith("win")
@@ -173,11 +166,11 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if ComputeManager.instance().localPlatform() is None:
self.uiLocalRadioButton.setEnabled(False)
elif is_mac or is_win:
if emulator_type == "qemu":
if template_type == "qemu":
# disallow usage of the local server because Qemu has issues on OSX and Windows
if not LocalConfig.instance().experimental():
self.uiLocalRadioButton.setEnabled(False)
elif emulator_type != "dynamips":
elif template_type != "dynamips":
self.uiLocalRadioButton.setEnabled(False)
if ComputeManager.instance().vmCompute():
@@ -195,27 +188,55 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
elif self.page(page_id) == self.uiFilesWizardPage:
if Controller.instance().isRemote() or self._compute_id != "local":
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
self._registry.getRemoteImageList(self._appliance.template_type(), self._compute_id)
else:
self.images_changed_signal.emit()
elif self.page(page_id) == self.uiQemuWizardPage:
if self._appliance['qemu'].get('kvm', 'require') == 'require':
if self._appliance.template_properties().get('kvm', 'require') == 'require':
self._server_check = False
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
else:
self._server_check = True
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
if self._appliance["registry_version"] >= 8:
qemu_platform = self._appliance.template_properties()["platform"]
else:
qemu_platform = self._appliance.template_properties()["arch"]
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [qemu_platform])
elif self.page(page_id) == self.uiInstructionsPage:
installation_instructions = self._appliance.get("installation_instructions", "No installation instructions available")
self.uiInstructionsTextEdit.setText(installation_instructions.strip())
elif self.page(page_id) == self.uiUsageWizardPage:
self.uiUsageTextEdit.setText("The template will be available in the {} category.\n\n{}".format(self._appliance["category"].replace("_", " "), self._appliance.get("usage", "")))
# TODO: allow taking these info fields at the version level in v8
category = self._appliance["category"].replace("_", " ")
usage = self._appliance.get("usage", "No usage information available")
if self._appliance["registry_version"] >= 8:
default_username = self._appliance.get("default_username")
default_password = self._appliance.get("default_password")
if default_username and default_password:
usage += "\n\nDefault username: {}\nDefault password: {}".format(default_username, default_password)
usage_info = """
The template will be available in the {} category.
Usage: {}
""".format(category, usage)
self.uiUsageTextEdit.setText(usage_info.strip())
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
"""
Check if the server supports KVM or not
"""
if error is None and "kvm" in result and self._appliance["qemu"]["arch"] in result["kvm"]:
if self._appliance["registry_version"] >= 8:
qemu_platform = self._appliance.template_properties()["platform"]
else:
qemu_platform = self._appliance.template_properties()["arch"]
if error is None and "kvm" in result and qemu_platform in result["kvm"]:
self._server_check = True
else:
if error:
@@ -236,7 +257,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
else:
log.info("Image '{}' has been successfully uploaded".format(image_path))
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
self._registry.getRemoteImageList(self._appliance.template_type(), self._compute_id)
def _showApplianceInfoSlot(self):
"""
@@ -407,7 +428,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
for version in self._appliance["versions"]:
for image in version["images"].values():
img = self._registry.search_image_file(self._appliance.emulator(),
img = self._registry.search_image_file(self._appliance.template_type(),
image["filename"],
image.get("md5sum"),
image.get("filesize"),
@@ -519,7 +540,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if len(path) == 0:
return
image = Image(self._appliance.emulator(), path, filename=disk["filename"])
image = Image(self._appliance.template_type(), path, filename=disk["filename"])
try:
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
reply = QtWidgets.QMessageBox.question(self, "Add appliance",
@@ -554,7 +575,11 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if self.uiQemuListComboBox.count() == 1:
self.next()
else:
i = self.uiQemuListComboBox.findData(self._appliance["qemu"]["arch"], flags=QtCore.Qt.MatchEndsWith)
if self._appliance["registry_version"] >= 8:
qemu_platform = self._appliance.template_properties()["platform"]
else:
qemu_platform = self._appliance.template_properties()["arch"]
i = self.uiQemuListComboBox.findData(qemu_platform, flags=QtCore.Qt.MatchEndsWith)
if i != -1:
self.uiQemuListComboBox.setCurrentIndex(i)
@@ -567,8 +592,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if version is None:
appliance_configuration = self._appliance.copy()
if "docker" not in appliance_configuration:
# only Docker do not have version
if self._appliance.template_type() != "docker":
# only Docker do not have versions
return False
else:
try:
@@ -585,10 +610,15 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
return False
appliance_configuration["name"] = appliance_configuration["name"].strip()
if "qemu" in appliance_configuration:
if self._appliance["registry_version"] >= 8:
if "settings" in appliance_configuration:
for settings in appliance_configuration["settings"]:
if settings["template_type"] == "qemu":
settings["template_properties"]["path"] = self.uiQemuListComboBox.currentData()
elif "qemu" in appliance_configuration:
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, self._symbols, parent=self)
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, version, self._symbols, parent=self)
TemplateManager.instance().createTemplate(Template(new_template), callback=self._templateCreatedCallback)
return False
@@ -632,7 +662,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if not Controller.instance().isRemote() and self._compute_id == "local" and image["path"].startswith(ImageManager.instance().getDirectory()):
log.debug("{} is already on the local server".format(image["path"]))
return
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
image = Image(self._appliance.template_type(), image["path"], filename=image["filename"])
image_upload_manager = ImageUploadManager(image, Controller.instance(), self._compute_id, self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manager.upload()
self._image_uploading_count += 1
@@ -649,12 +679,16 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
def nextId(self):
if self.currentPage() == self.uiServerWizardPage:
if "docker" in self._appliance:
if self._appliance.template_type() == "docker":
# skip Qemu binary selection and files pages if this is a Docker appliance
return super().nextId() + 2
elif "qemu" not in self._appliance:
return super().nextId() + 3
elif self._appliance.template_type() != "qemu":
# skip the Qemu binary selection page if not a Qemu appliance
return super().nextId() + 1
if self.currentPage() == self.uiQemuWizardPage:
if not self._appliance.get("installation_instructions"):
# skip the installation instructions page if there are no instructions
return super().nextId() + 1
return super().nextId()
def validateCurrentPage(self):
@@ -722,7 +756,6 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
elif self.currentPage() == self.uiQemuWizardPage:
# validate the Qemu
if self._server_check is False:
QtWidgets.QMessageBox.critical(self, "Checking for KVM support", "Please wait for the server to reply...")
return False

View File

@@ -22,6 +22,7 @@ Style editor to edit Shape items.
from ..qt import QtCore, QtWidgets, QtGui
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
from ..items.shape_item import ShapeItem
from ..items.rectangle_item import RectangleItem
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
@@ -70,6 +71,13 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
self._border_color.green(),
self._border_color.blue(),
self._border_color.alpha()))
if isinstance(first_item, RectangleItem):
# use the horizontal corner radius first and then the vertical one if it's not set
# maybe we allow configuring them separately in the future
corner_radius = first_item.horizontalCornerRadius()
if not corner_radius:
corner_radius = first_item.verticalCornerRadius()
self.uiCornerRadiusSpinBox.setValue(corner_radius)
self.uiRotationSpinBox.setValue(int(first_item.rotation()))
self.uiBorderWidthSpinBox.setValue(pen.width())
index = self.uiBorderStyleComboBox.findData(pen.style())
@@ -116,10 +124,16 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
for item in self._items:
item.setPen(pen)
# on multiselection it's possible to select many type of items
# on multi-selection it's possible to select many type of items
# but brush can be applied only on ShapeItem,
if brush and isinstance(item, ShapeItem):
item.setBrush(brush)
if isinstance(item, RectangleItem):
corner_radius = self.uiCornerRadiusSpinBox.value()
# use the corner radius for both horizontal (rx) and vertical (ry)
# maybe we support setting them separately in the future
item.setHorizontalCornerRadius(corner_radius)
item.setVerticalCornerRadius(corner_radius)
item.setRotation(self.uiRotationSpinBox.value())
def done(self, result):

View File

@@ -54,11 +54,15 @@ class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
self.uiColorLabel.hide()
self.uiColorPushButton.hide()
self._color = None
self.uiCornerRadiusLabel.hide()
self.uiCornerRadiusSpinBox.hide()
self.uiRotationLabel.hide()
self.uiRotationSpinBox.hide()
link.setHovered(False) # make sure we use the right style
pen = link.pen()
link.setHovered(True)
self._border_color = pen.color()
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
@@ -102,6 +106,7 @@ class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
# Store values
self._link.setLinkStyle(new_link_style)
self._link.setHovered(False) # allow to see the new style
def done(self, result):
"""

View File

@@ -713,6 +713,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
:param event: QDropEvent instance
"""
log.debug("Drop event received with mime data: {}".format(event.mimeData().formats()))
# check if what has been dropped is handled by this view
if event.mimeData().hasFormat("application/x-gns3-template"):
template_id = event.mimeData().data("application/x-gns3-template").data().decode()

View File

@@ -320,13 +320,13 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
if not sip_is_deleted(self):
# create the contextual menu
self.setHovered(True)
self.setAcceptHoverEvents(False)
menu = QtWidgets.QMenu()
self.populateLinkContextualMenu(menu)
menu.exec_(QtGui.QCursor.pos())
self.setAcceptHoverEvents(True)
self._hovered = False
self.adjust()
self.setHovered(False)
def keyPressEvent(self, event):
"""

View File

@@ -32,8 +32,22 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
"""
def __init__(self, width=200, height=100, **kws):
self._rx = 0
self._ry = 0
super().__init__(width=width, height=height, **kws)
def setHorizontalCornerRadius(self, radius: int):
self._rx = radius
def horizontalCornerRadius(self):
return self._rx
def setVerticalCornerRadius(self, radius: int):
self._ry = radius
def verticalCornerRadius(self):
return self._ry
def paint(self, painter, option, widget=None):
"""
Paints the contents of an item in local coordinates.
@@ -43,7 +57,9 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
:param widget: QWidget instance
"""
super().paint(painter, option, widget)
painter.setPen(self.pen())
painter.setBrush(self.brush())
painter.drawRoundedRect(self.rect(), self._rx, self._ry)
self.drawLayerInfo(painter)
def toSvg(self):
@@ -57,7 +73,27 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
rect = ET.SubElement(svg, "rect")
rect.set("width", str(int(self.rect().width())))
rect.set("height", str(int(self.rect().height())))
if self._rx:
rect.set("rx", str(self._rx))
if self._ry:
rect.set("ry", str(self._ry))
rect = self._styleSvg(rect)
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
def fromSvg(self, svg):
svg_elem = ET.fromstring(svg)
if len(svg_elem):
# handle horizontal corner radius and vertical corner radius (specific to rectangles)
rx = svg_elem[0].get("rx")
ry = svg_elem[0].get("ry")
if rx:
self._rx = int(rx)
elif ry:
self._rx = int(ry) # defaults to ry if it is specified
if ry:
self._ry = int(ry)
elif rx:
self._ry = int(rx) # defaults to rx if it is specified
super().fromSvg(svg)

View File

@@ -173,7 +173,7 @@ class ShapeItem(DrawingItem):
def fromSvg(self, svg):
"""
Import element informations from an SVG
Import element information from SVG
"""
svg = ET.fromstring(svg)
width = float(svg.get("width", self.rect().width()))

View File

@@ -193,7 +193,11 @@ class LocalServer(QtCore.QObject):
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
from gns3.utils.macos_ubridge_setuid import macos_ubridge_setuid
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
macos_ubridge_setuid()
else:
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set root permissions to uBridge {}: {}".format(path, str(e)))
return False

View File

@@ -30,16 +30,6 @@ try:
except Exception as e:
print("Fail update installation: {}".format(str(e)))
# WARNING
# Due to buggy user machines we choose to put this as the first loading modules
# otherwise the egg cache is initialized in his standard location and
# if is not writetable the application crash. It's the user fault
# because one day the user as used sudo to run an egg and break his
# filesystem permissions, but it's a common mistake.
from gns3.utils.get_resource import get_resource
import datetime
import traceback
import time
@@ -60,12 +50,12 @@ from gns3.local_config import LocalConfig
from gns3.application import Application
from gns3.utils import parse_version
from gns3.dialogs.profile_select import ProfileSelectDialog
from gns3.version import __version__
import logging
log = logging.getLogger(__name__)
from gns3.version import __version__
def locale_check():
"""
@@ -135,6 +125,13 @@ def main():
if options.project:
options.project = os.path.abspath(options.project)
try:
import truststore
truststore.inject_into_ssl()
log.info("Using system certificate store for SSL connections")
except ImportError:
pass
if hasattr(sys, "frozen"):
# We add to the path where the OS search executable our binary location starting by GNS3
# packaged binary

View File

@@ -49,7 +49,6 @@ from .topology import Topology
from .http_client import HTTPClient
from .progress import Progress
from .update_manager import UpdateManager
from .utils.analytics import AnalyticsClient
from .dialogs.appliance_wizard import ApplianceWizard
from .dialogs.new_template_wizard import NewTemplateWizard
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
@@ -133,7 +132,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._local_config_timer = QtCore.QTimer(self)
self._local_config_timer.timeout.connect(local_config.checkConfigChanged)
self._local_config_timer.start(1000) # milliseconds
self._analytics_client = AnalyticsClient()
self._template_manager = TemplateManager().instance()
self._appliance_manager = ApplianceManager().instance()
@@ -378,13 +376,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiConsoleDockWidget.setFloating(False)
self.uiNodesDockWidget.setFloating(False)
def analyticsClient(self):
"""
Return the analytics client
"""
return self._analytics_client
def _newProjectActionSlot(self):
"""
Slot called to create a new project.
@@ -1144,8 +1135,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
progress.setCancelButtonText("Force quit")
log.debug("Close the Main Window")
self._analytics_client.sendScreenView("Main Window", session_start=False)
self._finish_application_closing(close_windows=False)
event.accept()
self.uiConsoleTextEdit.closeIO()
@@ -1234,7 +1223,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Controller.instance().connected_signal.connect(self._controllerConnectedSlot)
Controller.instance().project_list_updated_signal.connect(self.updateRecentProjectActions)
self._analytics_client.sendScreenView("Main Window")
self.uiGraphicsView.setEnabled(False)
# show the setup wizard

View File

@@ -57,6 +57,7 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
self.uiPrivateConfigToolButton.hide()
# location of the base config templates
# FIXME: this does not work
self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_startup-config.txt"))
self._base_iou_l3_config_template = get_resource(os.path.join("configs", "iou_l3_base_startup-config.txt"))
self._default_configs_dir = LocalServer.instance().localServerSettings()["configs_path"]

View File

@@ -579,6 +579,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
self.uiQemuOptionsLineEdit.setText(settings["options"])
self.uiUsageTextEdit.setPlainText(settings["usage"])
self.uiTPMCheckBox.setChecked(settings["tpm"])
self.uiUEFICheckBox.setChecked(settings["uefi"])
def saveSettings(self, settings, node=None, group=False):
"""
@@ -694,4 +695,5 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
settings["options"] = self.uiQemuOptionsLineEdit.text()
settings["usage"] = self.uiUsageTextEdit.toPlainText()
settings["tpm"] = self.uiTPMCheckBox.isChecked()
settings["uefi"] = self.uiUEFICheckBox.isChecked()
return settings

View File

@@ -75,6 +75,7 @@ class QemuVM(Node):
"legacy_networking": QEMU_VM_SETTINGS["legacy_networking"],
"replicate_network_connection_state": QEMU_VM_SETTINGS["replicate_network_connection_state"],
"tpm": QEMU_VM_SETTINGS["tpm"],
"uefi": QEMU_VM_SETTINGS["uefi"],
"create_config_disk": QEMU_VM_SETTINGS["create_config_disk"],
"platform": QEMU_VM_SETTINGS["platform"],
"on_close": QEMU_VM_SETTINGS["on_close"],

View File

@@ -58,6 +58,7 @@ QEMU_VM_SETTINGS = {
"legacy_networking": False,
"replicate_network_connection_state": True,
"tpm": False,
"uefi": False,
"create_config_disk": False,
"on_close": "power_off",
"platform": "",

View File

@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>478</width>
<height>550</height>
<height>579</height>
</rect>
</property>
<property name="windowTitle">
@@ -867,13 +867,6 @@
<string>Additional settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="uiQemuOptionsLabel">
<property name="text">
<string>Options:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="uiQemuOptionsLineEdit">
<property name="toolTip">
@@ -890,7 +883,7 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="uiBaseVMCheckBox">
<property name="enabled">
<bool>true</bool>
@@ -907,11 +900,26 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="uiQemuOptionsLabel">
<property name="text">
<string>Options:</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="uiUEFICheckBox">
<property name="text">
<string>Enable UEFI boot mode</string>
</property>
</widget>
</item>
</layout>
<zorder>uiQemuOptionsLineEdit</zorder>
<zorder>uiQemuOptionsLabel</zorder>
<zorder>uiBaseVMCheckBox</zorder>
<zorder>uiTPMCheckBox</zorder>
<zorder>uiUEFICheckBox</zorder>
</widget>
</item>
<item>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
#
# Created by: PyQt5 UI code generator 5.15.6
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_QemuVMConfigPageWidget(object):
def setupUi(self, QemuVMConfigPageWidget):
QemuVMConfigPageWidget.setObjectName("QemuVMConfigPageWidget")
QemuVMConfigPageWidget.resize(478, 550)
QemuVMConfigPageWidget.resize(478, 579)
self.verticalLayout = QtWidgets.QVBoxLayout(QemuVMConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiQemutabWidget = QtWidgets.QTabWidget(QemuVMConfigPageWidget)
@@ -428,23 +428,27 @@ class Ui_QemuVMConfigPageWidget(object):
self.groupBox.setObjectName("groupBox")
self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBox)
self.gridLayout_3.setObjectName("gridLayout_3")
self.uiQemuOptionsLabel = QtWidgets.QLabel(self.groupBox)
self.uiQemuOptionsLabel.setObjectName("uiQemuOptionsLabel")
self.gridLayout_3.addWidget(self.uiQemuOptionsLabel, 0, 0, 1, 1)
self.uiQemuOptionsLineEdit = QtWidgets.QLineEdit(self.groupBox)
self.uiQemuOptionsLineEdit.setObjectName("uiQemuOptionsLineEdit")
self.gridLayout_3.addWidget(self.uiQemuOptionsLineEdit, 0, 1, 1, 1)
self.uiBaseVMCheckBox = QtWidgets.QCheckBox(self.groupBox)
self.uiBaseVMCheckBox.setEnabled(True)
self.uiBaseVMCheckBox.setObjectName("uiBaseVMCheckBox")
self.gridLayout_3.addWidget(self.uiBaseVMCheckBox, 2, 0, 1, 2)
self.gridLayout_3.addWidget(self.uiBaseVMCheckBox, 3, 0, 1, 2)
self.uiTPMCheckBox = QtWidgets.QCheckBox(self.groupBox)
self.uiTPMCheckBox.setObjectName("uiTPMCheckBox")
self.gridLayout_3.addWidget(self.uiTPMCheckBox, 1, 0, 1, 2)
self.uiQemuOptionsLabel = QtWidgets.QLabel(self.groupBox)
self.uiQemuOptionsLabel.setObjectName("uiQemuOptionsLabel")
self.gridLayout_3.addWidget(self.uiQemuOptionsLabel, 0, 0, 1, 1)
self.uiUEFICheckBox = QtWidgets.QCheckBox(self.groupBox)
self.uiUEFICheckBox.setObjectName("uiUEFICheckBox")
self.gridLayout_3.addWidget(self.uiUEFICheckBox, 2, 0, 1, 2)
self.uiQemuOptionsLineEdit.raise_()
self.uiQemuOptionsLabel.raise_()
self.uiBaseVMCheckBox.raise_()
self.uiTPMCheckBox.raise_()
self.uiUEFICheckBox.raise_()
self.verticalLayout_2.addWidget(self.groupBox)
spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_2.addItem(spacerItem4)
@@ -549,7 +553,6 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiProcessPriorityComboBox.setItemText(4, _translate("QemuVMConfigPageWidget", "Low"))
self.uiProcessPriorityComboBox.setItemText(5, _translate("QemuVMConfigPageWidget", "Very low"))
self.groupBox.setTitle(_translate("QemuVMConfigPageWidget", "Additional settings"))
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:"))
self.uiQemuOptionsLineEdit.setToolTip(_translate("QemuVMConfigPageWidget", "<html><head/><body><p>Variable replacements:</p>\n"
"<ul>\n"
"<li>%vm-name% =VM name</li>\n"
@@ -562,5 +565,7 @@ class Ui_QemuVMConfigPageWidget(object):
"</body></html>"))
self.uiBaseVMCheckBox.setText(_translate("QemuVMConfigPageWidget", "Use as a linked base VM"))
self.uiTPMCheckBox.setText(_translate("QemuVMConfigPageWidget", "Enable Trusted Platform Module (TPM)"))
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:"))
self.uiUEFICheckBox.setText(_translate("QemuVMConfigPageWidget", "Enable UEFI boot mode"))
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiAdvancedSettingsTab), _translate("QemuVMConfigPageWidget", "Advanced"))
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiUsageTab), _translate("QemuVMConfigPageWidget", "Usage"))

View File

@@ -689,7 +689,8 @@ class Node(BaseNode):
return
super().setStatus(status)
if status == self.started and "console_auto_start" in self.settings() and self.settings()["console_auto_start"]:
self.openConsole()
# give the node some time to start before opening the console
QtCore.QTimer.singleShot(1000, self.openConsole)
def openConsole(self, command=None, aux=False):
"""

View File

@@ -112,15 +112,20 @@ class PacketCapture:
"""
Starts the packet capture reader.
"""
self._startPacketCommand(link, self.settings()["packet_capture_reader_command"])
def stopPacketCaptureReader(self, link):
"""
Stop the packet capture reader
"""
if link in self._tail_process and self._tail_process[link].poll() is None:
if link in self._tail_process:
log.debug("Stopping packet capture reader for link {}".format(link.link_id()))
self._tail_process[link].kill()
try:
self._tail_process[link].kill()
except (PermissionError, OSError):
pass
del self._tail_process[link]
def startPacketCaptureAnalyzer(self, link):
@@ -183,14 +188,14 @@ class PacketCapture:
QtWidgets.QMessageBox.critical(self.parent(), "Packet capture", "Can't create packet capture file {}: {}".format(capture_file_path, str(e)))
return
if link in self._tail_process and self._tail_process[link].poll() is None:
if link in self._tail_process:
try:
self._tail_process[link].kill()
except (PermissionError, OSError):
# Sometimes we have condition on windows where the process is in the process to quit
pass
del self._tail_process[link]
if link in self._capture_reader_process and self._capture_reader_process[link].poll() is None:
if link in self._capture_reader_process:
try:
self._capture_reader_process[link].kill()
except (PermissionError, OSError):

View File

@@ -301,7 +301,6 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
self.uiImagesPathLineEdit.setText(local_server["images_path"])
self.uiConfigsPathLineEdit.setText(local_server["configs_path"])
self.uiAppliancesPathLineEdit.setText(local_server["appliances_path"])
self.uiStatsCheckBox.setChecked(settings["send_stats"])
self.uiOverlayNotificationsCheckBox.setChecked(settings["overlay_notifications"])
self.uiCrashReportCheckBox.setChecked(local_server["report_errors"])
self.uiCheckForUpdateCheckBox.setChecked(settings["check_for_update"])
@@ -411,7 +410,6 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
"vnc_console_command": self.uiVNCConsoleCommandLineEdit.text(),
"spice_console_command": self.uiSPICEConsoleCommandLineEdit.text(),
"delay_console_all": self.uiDelayConsoleAllSpinBox.value(),
"send_stats": self.uiStatsCheckBox.isChecked(),
"multi_profiles": self.uiMultiProfilesCheckBox.isChecked(),
"direct_file_upload": self.uiDirectFileUpload.isChecked()
}

View File

@@ -654,7 +654,7 @@ class Project(QtCore.QObject):
@qslot
def _websocket_error(self, error):
if self._notification_stream:
log.error(self._notification_stream.errorString())
log.error("Websocket project notification stream error: {}".format(self._notification_stream.errorString()))
self._notification_stream = None
self._startListenNotifications()

View File

@@ -195,54 +195,6 @@ if hasattr(sys, '_called_from_test'):
QtCore.pyqtSignal = FakeQtSignal
class StatsQtWidgetsQWizard(QtWidgets.QWizard):
"""
Send stats from all the QWizard
"""
def __init__(self, *args):
super().__init__(*args)
from ..utils.analytics import AnalyticsClient
name = self.__class__.__name__
name = re.sub(r"([A-Z])", r" \1", name).strip()
AnalyticsClient.instance().sendScreenView(name)
QtWidgets.QWizard = StatsQtWidgetsQWizard
class StatsQtWidgetsQMainWindow(QtWidgets.QMainWindow):
"""
Send stats from all the QMainWindow
"""
def __init__(self, *args):
super().__init__(*args)
from ..utils.analytics import AnalyticsClient
name = self.__class__.__name__
name = re.sub(r"([A-Z])", r" \1", name).strip()
AnalyticsClient.instance().sendScreenView(name)
QtWidgets.QMainWindow = StatsQtWidgetsQMainWindow
class StatsQtWidgetsQDialog(QtWidgets.QDialog):
"""
Send stats from all the QWizard
"""
def __init__(self, *args):
super().__init__(*args)
from ..utils.analytics import AnalyticsClient
name = self.__class__.__name__
name = re.sub(r"([A-Z])", r" \1", name).strip()
AnalyticsClient.instance().sendScreenView(name)
QtWidgets.QDialog = StatsQtWidgetsQDialog
def qpartial(func, *args, **kwargs):
"""
A functools partial that you can use on qobject. If the targeted qobject is

View File

@@ -21,11 +21,13 @@ import copy
import os
import collections.abc
import jsonschema
from gns3.utils.get_resource import get_resource
import logging
log = logging.getLogger(__name__)
class ApplianceError(Exception):
pass
@@ -38,6 +40,7 @@ class Appliance(collections.abc.Mapping):
:params path: Path of the appliance file on disk or file content
"""
self._registry = registry
self._registry_version = None
if os.path.isabs(path):
try:
@@ -58,16 +61,25 @@ class Appliance(collections.abc.Mapping):
:param appliance: Sanity check on the appliance configuration
"""
if "registry_version" not in self._appliance:
raise ApplianceError("Invalid appliance configuration please report the issue on https://github.com/GNS3/gns3-registry")
if self._appliance["registry_version"] > 7:
raise ApplianceError("Please update GNS3 in order to install this appliance")
raise ApplianceError("Invalid appliance configuration please report the issue on https://github.com/GNS3/gns3-registry/issues")
with open(get_resource(os.path.join("schemas", "appliance.json"))) as f:
self._registry_version = self._appliance["registry_version"]
if self._registry_version > 8:
# we only support registry version 8 and below
raise ApplianceError("Registry version {} is not supported in this version of GNS3".format(self._registry_version))
if self._registry_version == 8:
# registry version 8 has a different schema with support for multiple settings sets
appliance_file = "appliance_v8.json"
else:
appliance_file = "appliance.json"
with open(get_resource("schemas/{}".format(appliance_file))) as f:
schema = json.load(f)
v = jsonschema.Draft4Validator(schema)
try:
v.validate(self._appliance)
except jsonschema.ValidationError as e:
except jsonschema.ValidationError:
error = jsonschema.exceptions.best_match(v.iter_errors(self._appliance)).message
raise ApplianceError("Invalid appliance file: {}".format(error))
@@ -82,10 +94,11 @@ class Appliance(collections.abc.Mapping):
def _resolve_version(self):
"""
Replace image field in versions by their the complete information from images
Replace image field in versions by the complete information from images
"""
if "versions" not in self._appliance:
log.debug("No versions found in appliance")
return
for version in self._appliance["versions"]:
@@ -96,7 +109,7 @@ class Appliance(collections.abc.Mapping):
for file in self._appliance["images"]:
file = copy.copy(file)
if "idlepc" in version:
if self._registry_version < 8 and "idlepc" in version:
file["idlepc"] = version["idlepc"]
if "/" in filename:
@@ -127,7 +140,7 @@ class Appliance(collections.abc.Mapping):
def search_images_for_version(self, version_name):
"""
Search on disk the images required by this version.
And keep only the require images in the images fields. Add to the images
And keep only the required images in the images fields. Add to the images
their disk type and path.
:param version_name: Version name
@@ -142,10 +155,18 @@ class Appliance(collections.abc.Mapping):
for image_type, image in version["images"].items():
image["type"] = image_type
img = self._registry.search_image_file(self.emulator(), image["filename"], image.get("md5sum"), image.get("filesize"))
checksum = image.get("md5sum")
if checksum is None and self._registry_version >= 8:
# registry version >= 8 has the checksum and checksum_type fields
checksum_type = image.get("checksum_type", "md5") # md5 is the default and only supported type
if checksum_type != "md5":
raise ApplianceError("Checksum type {} is not supported".format(checksum_type))
checksum = image.pop("checksum")
img = self._registry.search_image_file(self.template_type(), image["filename"], checksum, image.get("filesize"))
if img is None:
if "md5sum" in image:
raise ApplianceError("File {} with checksum {} not found for {}".format(image["filename"], image["md5sum"], appliance["name"]))
if checksum:
raise ApplianceError("File {} with checksum {} not found for {}".format(image["filename"], checksum, appliance["name"]))
else:
raise ApplianceError("File {} not found for {}".format(image["filename"], appliance["name"]))
@@ -186,9 +207,51 @@ class Appliance(collections.abc.Mapping):
except ApplianceError:
return False
def emulator(self):
if "qemu" in self._appliance:
return "qemu"
if "iou" in self._appliance:
return "iou"
return "dynamips"
def template_type(self):
if self._registry_version >= 8:
template_type = None
for settings in self._appliance["settings"]:
if settings["template_type"] and not template_type:
template_type = settings["template_type"]
elif settings["template_type"] and template_type != settings["template_type"]:
# we are currently not supporting multiple different template types in the same appliance
raise ApplianceError("Multiple different template types found in appliance")
if not template_type:
raise ApplianceError("No template type found in appliance {}".format(self._appliance["name"]))
return template_type
else:
if "qemu" in self._appliance:
return "qemu"
if "iou" in self._appliance:
return "iou"
if "dynamips" in self._appliance:
return "dynamips"
if "docker" in self._appliance:
return "docker"
return None
def template_properties(self):
"""
Get template properties
"""
if self._registry_version >= 8:
# find the default settings if any
for settings in self._appliance["settings"]:
if settings.get("default", False):
return settings["template_properties"]
# otherwise take the first settings we find
for settings in self._appliance["settings"]:
if settings["template_type"]:
return settings["template_properties"]
else:
if "qemu" in self._appliance:
return self._appliance["qemu"]
if "iou" in self._appliance:
return self._appliance["iou"]
if "dynamips" in self._appliance:
return self._appliance["dynamips"]
if "docker" in self._appliance:
return self._appliance["docker"]
return None

View File

@@ -24,6 +24,7 @@ from ..qt import QtCore, QtWidgets, QtNetwork
from ..controller import Controller
from .config import Config, ConfigException
import logging
log = logging.getLogger(__name__)
@@ -33,7 +34,7 @@ class ApplianceToTemplate:
Appliance installation.
"""
def new_template(self, appliance_config, server, controller_symbols=None, parent=None):
def new_template(self, appliance_config, server, appliance_version=None, controller_symbols=None, parent=None):
"""
Creates a new template from an appliance.
@@ -42,6 +43,7 @@ class ApplianceToTemplate:
"""
self._parent = parent
self._registry_version = appliance_config["registry_version"]
new_template = {
"compute_id": server,
"name": appliance_config["name"]
@@ -73,45 +75,101 @@ class ApplianceToTemplate:
elif appliance_config["category"] == "firewall":
new_template["symbol"] = ":/symbols/firewall.svg"
if "qemu" in appliance_config:
new_template["template_type"] = "qemu"
self._add_qemu_config(new_template, appliance_config)
elif "iou" in appliance_config:
new_template["template_type"] = "iou"
self._add_iou_config(new_template, appliance_config)
elif "dynamips" in appliance_config:
new_template["template_type"] = "dynamips"
self._add_dynamips_config(new_template, appliance_config)
elif "docker" in appliance_config:
new_template["template_type"] = "docker"
self._add_docker_config(new_template, appliance_config)
if self._registry_version >= 8:
if appliance_version:
for version in appliance_config["versions"]:
if appliance_version and version["name"] == appliance_version:
# inject "usage", "category" and "symbol" specified at the version
# level into the template properties
usage = version.get("usage")
if usage:
new_template["usage"] = usage
new_template["symbol"] = version.get("symbol", new_template["symbol"])
new_template["category"] = version.get("category", new_template["category"])
settings = self._get_settings(appliance_config, version.get("settings"))
template_type = settings["template_type"]
if template_type == "qemu":
self._add_qemu_config(new_template, settings["template_properties"], appliance_config)
elif template_type == "iou":
self._add_iou_config(new_template, settings["template_properties"], appliance_config)
elif template_type == "dynamips":
self._add_dynamips_config(new_template, settings["template_properties"], appliance_config)
else:
# docker appliances have no version
settings = self._get_settings(appliance_config)
if settings["template_type"] == "docker":
self._add_docker_config(new_template, settings["template_properties"], appliance_config)
else:
raise ConfigException("{} no configuration found for known emulators".format(new_template["name"]))
if "qemu" in appliance_config:
self._add_qemu_config(new_template, appliance_config["qemu"], appliance_config)
elif "iou" in appliance_config:
self._add_iou_config(new_template, appliance_config["iou"], appliance_config)
elif "dynamips" in appliance_config:
self._add_dynamips_config(new_template, appliance_config["dynamips"], appliance_config)
elif "docker" in appliance_config:
self._add_docker_config(new_template, appliance_config["docker"], appliance_config)
else:
raise ConfigException("{} no configuration found for known emulators".format(new_template["name"]))
return new_template
def _add_qemu_config(self, new_config, appliance_config):
def _get_settings(self, appliance_config, settings_name=None):
new_config.update(appliance_config["qemu"])
default_settings = None
# first look for default settings, if any ('default' = true, first set that has it)
for settings in appliance_config["settings"]:
if settings.get("default", False):
default_settings = settings
break
# then look for specific settings set if a name is provided
if settings_name:
for settings in appliance_config["settings"]:
if settings.get("name") == settings_name:
if settings.get("inherit_default_properties", True) and \
default_settings and default_settings["template_type"] == settings["template_type"]:
default_settings["template_properties"].update(settings["template_properties"])
return default_settings
return settings
raise ConfigException("Settings '{}' cannot be found in the appliance file", settings_name)
elif default_settings:
return default_settings
if not appliance_config.get("settings"):
raise ConfigException("No settings found in the appliance file")
# if no default settings are specified, use the first available settings set
return appliance_config["settings"][0]
def _add_qemu_config(self, new_config, template_properties, appliance_config):
new_config["template_type"] = "qemu"
new_config.update(template_properties)
# the following properties are not valid for a template
new_config.pop("kvm", None)
new_config.pop("path", None)
new_config.pop("arch", None)
new_config.pop("kvm", None) # To check KVM setting against the server capabilities
new_config.pop("path", None) # Qemu binary selected in previous step
new_config.pop("arch", None) # Used for selecting the Qemu binary
options = appliance_config["qemu"].get("options", "")
if appliance_config["qemu"].get("kvm", "allow") == "disable" and "-machine accel=tcg" not in options:
options = template_properties.get("options", "")
if template_properties.get("kvm", "allow") == "disable" and "-machine accel=tcg" not in options:
options += " -machine accel=tcg"
new_config["options"] = options.strip()
options = options.strip()
if options:
new_config["options"] = options
for image in appliance_config["images"]:
if image.get("path"):
new_config[image["type"]] = self._relative_image_path("QEMU", image["path"])
if "path" in appliance_config["qemu"]:
new_config["qemu_path"] = appliance_config["qemu"]["path"]
if "path" in template_properties:
new_config["qemu_path"] = template_properties["path"]
else:
new_config["qemu_path"] = "qemu-system-{}".format(appliance_config["qemu"]["arch"])
if self._registry_version >= 8:
# the "arch" field was replaced by the "platform" field in registry version 8
new_config["qemu_path"] = "qemu-system-{}".format(template_properties["platform"])
else:
new_config["qemu_path"] = "qemu-system-{}".format(template_properties["arch"])
if "first_port_name" in appliance_config:
new_config["first_port_name"] = appliance_config["first_port_name"]
@@ -128,24 +186,28 @@ class ApplianceToTemplate:
if "linked_clone" in appliance_config:
new_config["linked_clone"] = appliance_config["linked_clone"]
def _add_docker_config(self, new_config, appliance_config):
def _add_docker_config(self, new_config, template_properties, appliance_config):
new_config.update(appliance_config["docker"])
new_config["template_type"] = "docker"
new_config.update(template_properties)
if "custom_adapters" in appliance_config:
new_config["custom_adapters"] = appliance_config["custom_adapters"]
def _add_dynamips_config(self, new_config, appliance_config):
def _add_dynamips_config(self, new_config, template_properties, appliance_config):
new_config.update(appliance_config["dynamips"])
new_config["template_type"] = "dynamips"
new_config.update(template_properties)
for image in appliance_config["images"]:
new_config[image["type"]] = self._relative_image_path("IOS", image["path"])
new_config["idlepc"] = image.get("idlepc", "")
if self._registry_version < 8:
new_config["idlepc"] = image.get("idlepc", "")
def _add_iou_config(self, new_config, appliance_config):
def _add_iou_config(self, new_config, template_properties, appliance_config):
new_config.update(appliance_config["iou"])
new_config["template_type"] = "iou"
new_config.update(template_properties)
for image in appliance_config["images"]:
if "path" not in image:
raise ConfigException("Disk image is missing")
@@ -197,7 +259,8 @@ class ApplianceToTemplate:
url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol_id)
try:
self._downloadApplianceSymbol(url, path)
if not self._downloadApplianceSymbol(url, path):
return None
controller.clearStaticCache()
if controller.isRemote():
controller.uploadSymbol(symbol_id, path)
@@ -230,5 +293,7 @@ class ApplianceToTemplate:
log.debug("Error while saving appliance symbol to '{}': {}".format(path, e))
raise
log.debug("Appliance symbol downloaded and saved to '{}'".format(path))
return True
else:
log.warning("Error when downloading appliance symbol from '{}': {}".format(url, reply.errorString()))
log.error("Error when downloading appliance symbol from '{}': {}".format(url, reply.errorString()))
return False

View File

@@ -86,7 +86,7 @@
"title": "An optional product url on vendor website"
},
"registry_version": {
"enum": [1, 2, 3, 4, 5, 6],
"enum": [1, 2, 3, 4, 5, 6, 7],
"title": "Version of the registry compatible with this appliance"
},
"status": {
@@ -354,6 +354,10 @@
"type": "boolean",
"title": "Enable the Trusted Platform Module (TPM)"
},
"uefi": {
"type": "boolean",
"title": "Enable the UEFI boot mode"
},
"on_close": {
"title": "Action to execute on the VM is closed",
"enum": ["power_off", "shutdown_signal", "save_vm_state"]
@@ -396,7 +400,6 @@
"md5sum": {
"type": "string",
"title": "md5sum of the file",
"type": "string",
"pattern": "^[a-f0-9]{32}$"
},
"filesize": {

View File

@@ -0,0 +1,898 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "JSON schema validating a GNS3 appliance",
"definitions": {
"categories": {
"enum": [
"router",
"multilayer_switch",
"firewall",
"guest"
]
},
"docker_properties": {
"type": "object",
"title": "Docker template properties",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"title": "Name of the template"
},
"category": {
"$ref": "#/definitions/categories",
"title": "Category of the template"
},
"default_name_format": {
"type": "string",
"title": "Default name format"
},
"usage": {
"type": "string",
"title": "How to use the template"
},
"symbol": {
"type": "string",
"title": "Symbol of the template"
},
"image": {
"type": "string",
"title": "Docker image"
},
"adapters": {
"type": "integer",
"title": "Number of ethernet adapters"
},
"start_command": {
"type": "string",
"title": "Command executed when the container start. Empty will use the default"
},
"environment": {
"type": "string",
"title": "One KEY=VAR environment by line"
},
"console_type": {
"enum": [
"telnet",
"vnc",
"http",
"https"
],
"title": "Type of console"
},
"console_http_port": {
"title": "Internal port in the container of the HTTP server",
"type": "integer"
},
"console_http_path": {
"title": "Path of the web interface",
"type": "string"
},
"console_resolution": {
"title": "Console resolution for VNC, for example 1024x768",
"type": "string",
"pattern": "^[0-9]+x[0-9]+$"
},
"extra_hosts": {
"title": "Docker extra hosts (added to /etc/hosts)",
"type": "string"
},
"extra_volumes": {
"title": "Additional directories to make persistent",
"type": "array"
}
},
"required": [
"image"
]
},
"iou_properties": {
"type": "object",
"title": "IOU template properties",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"title": "Name of the template"
},
"category": {
"$ref": "#/definitions/categories",
"title": "Category of the template"
},
"default_name_format": {
"type": "string",
"title": "Default name format"
},
"usage": {
"type": "string",
"title": "How to use the template"
},
"symbol": {
"type": "string",
"title": "Symbol of the template"
},
"ethernet_adapters": {
"type": "integer",
"title": "Number of ethernet adapters"
},
"serial_adapters": {
"type": "integer",
"title": "Number of serial adapters"
},
"ram": {
"type": "integer",
"title": "Host RAM"
},
"nvram": {
"type": "integer",
"title": "Host NVRAM"
},
"startup_config": {
"type": "string",
"title": "Config loaded at startup"
}
}
},
"dynamips_slot": {
"enum": [
"C2600-MB-2FE",
"C2600-MB-1E",
"PA-A1",
"PA-8E",
"C1700-MB-1FE",
"PA-8T",
"PA-2FE-TX",
"PA-FE-TX",
"PA-GE",
"C2600-MB-2E",
"C7200-IO-FE",
"NM-4T",
"C2600-MB-1FE",
"C7200-IO-2FE",
"PA-POS-OC3",
"PA-4T+",
"C1700-MB-WIC1",
"NM-16ESW",
"C7200-IO-GE-E",
"NM-4E",
"GT96100-FE",
"NM-1FE-TX",
"Leopard-2FE",
"NM-1E",
"PA-4E",
""
]
},
"dynamips_wic": {
"enum": [
"WIC-1ENET",
"WIC-1T",
"WIC-2T",
""
]
},
"dynamips_properties": {
"type": "object",
"title": "Dynamips template properties",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"title": "Name of the template"
},
"category": {
"$ref": "#/definitions/categories",
"title": "Category of the template"
},
"default_name_format": {
"type": "string",
"title": "Default name format"
},
"usage": {
"type": "string",
"title": "How to use the template"
},
"symbol": {
"type": "string",
"title": "Symbol of the template"
},
"chassis": {
"title": "Chassis type",
"enum": [
"1720",
"1721",
"1750",
"1751",
"1760",
"2610",
"2620",
"2610XM",
"2620XM",
"2650XM",
"2621",
"2611XM",
"2621XM",
"2651XM",
"3620",
"3640",
"3660",
""
]
},
"platform": {
"title": "Platform type",
"enum": [
"c1700",
"c2600",
"c2691",
"c3725",
"c3745",
"c3600",
"c7200"
]
},
"ram": {
"title": "Amount of ram",
"type": "integer",
"minimum": 1
},
"nvram": {
"title": "Amount of nvram",
"type": "integer",
"minimum": 1
},
"idlepc": {
"type": "string",
"pattern": "^0x[0-9a-f]{8}"
},
"startup_config": {
"type": "string",
"title": "Config loaded at startup"
},
"wic0": {
"$ref": "#/definitions/dynamips_wic"
},
"wic1": {
"$ref": "#/definitions/dynamips_wic"
},
"wic2": {
"$ref": "#/definitions/dynamips_wic"
},
"slot0": {
"$ref": "#/definitions/dynamips_slot"
},
"slot1": {
"$ref": "#/definitions/dynamips_slot"
},
"slot2": {
"$ref": "#/definitions/dynamips_slot"
},
"slot3": {
"$ref": "#/definitions/dynamips_slot"
},
"slot4": {
"$ref": "#/definitions/dynamips_slot"
},
"slot5": {
"$ref": "#/definitions/dynamips_slot"
},
"slot6": {
"$ref": "#/definitions/dynamips_slot"
},
"midplane": {
"enum": [
"std",
"vxr"
]
},
"npe": {
"enum": [
"npe-100",
"npe-150",
"npe-175",
"npe-200",
"npe-225",
"npe-300",
"npe-400",
"npe-g2"
]
}
},
"required": [
"platform"
]
},
"qemu_disk_interfaces": {
"enum": [
"ide",
"scsi",
"sd",
"mtd",
"floppy",
"pflash",
"virtio",
"sata"
]
},
"qemu_properties": {
"type": "object",
"title": "Qemu template properties",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"title": "Name of the template"
},
"category": {
"$ref": "#/definitions/categories",
"title": "Category of the template"
},
"default_name_format": {
"type": "string",
"title": "Default name format"
},
"usage": {
"type": "string",
"title": "How to use the template"
},
"symbol": {
"type": "string",
"title": "Symbol of the template"
},
"adapter_type": {
"enum": [
"e1000",
"i82550",
"i82551",
"i82557a",
"i82557b",
"i82557c",
"i82558a",
"i82558b",
"i82559a",
"i82559b",
"i82559c",
"i82559er",
"i82562",
"i82801",
"ne2k_pci",
"pcnet",
"rtl8139",
"virtio",
"virtio-net-pci",
"vmxnet3"
],
"title": "Type of network adapter"
},
"adapters": {
"type": "integer",
"title": "Number of adapters"
},
"custom_adapters": {
"type": "array",
"title": "Custom adapters",
"items": {
"type": "object",
"properties": {
"adapter_number": {
"title": "Adapter number",
"type": "integer"
},
"port_name": {
"title": "Custom port name",
"type": "string",
"minimum": 1
},
"adapter_type": {
"title": "Custom adapter type",
"type": "string",
"enum": [
"e1000",
"i82550",
"i82551",
"i82557a",
"i82557b",
"i82557c",
"i82558a",
"i82558b",
"i82559a",
"i82559b",
"i82559c",
"i82559er",
"i82562",
"i82801",
"ne2k_pci",
"pcnet",
"rtl8139",
"virtio",
"virtio-net-pci",
"vmxnet3"
]
},
"mac_address": {
"title": "Custom MAC address",
"type": "string",
"minimum": 1,
"pattern": "^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$"
}
},
"required": [
"adapter_number"
]
}
},
"first_port_name": {
"type": "string",
"title": "Optional name of the first networking port example: eth0"
},
"port_name_format": {
"type": "string",
"title": "Optional formating of the networking port example: eth{0}"
},
"port_segment_size": {
"type": "integer",
"title": "Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2"
},
"linked_clone": {
"type": "boolean",
"title": "False if you don't want to use a single image for all nodes"
},
"ram": {
"type": "integer",
"title": "Ram allocated to the appliance (MB)"
},
"cpus": {
"type": "integer",
"title": "Number of Virtual CPU"
},
"hda_disk_interface": {
"$ref": "#/definitions/qemu_disk_interfaces",
"title": "Disk interface for the installed hda_disk_image"
},
"hdb_disk_interface": {
"$ref": "#/definitions/qemu_disk_interfaces",
"title": "Disk interface for the installed hdb_disk_image"
},
"hdc_disk_interface": {
"$ref": "#/definitions/qemu_disk_interfaces",
"title": "Disk interface for the installed hdc_disk_image"
},
"hdd_disk_interface": {
"$ref": "#/definitions/qemu_disk_interfaces",
"title": "Disk interface for the installed hdd_disk_image"
},
"platform": {
"enum": [
"aarch64",
"alpha",
"arm",
"cris",
"i386",
"lm32",
"m68k",
"microblaze",
"microblazeel",
"mips",
"mips64",
"mips64el",
"mipsel",
"moxie",
"or32",
"ppc",
"ppc64",
"ppcemb",
"s390x",
"sh4",
"sh4eb",
"sparc",
"sparc64",
"tricore",
"unicore32",
"x86_64",
"xtensa",
"xtensaeb"
],
"title": "Platform to emulate"
},
"console_type": {
"enum": [
"telnet",
"vnc",
"spice",
"spice+agent"
],
"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."
},
"kernel_command_line": {
"type": "string",
"title": "Command line parameters send to the kernel"
},
"options": {
"type": "string",
"title": "Optional additional qemu command line options"
},
"cpu_throttling": {
"type": "number",
"minimum": 0,
"maximum": 100,
"title": "Throttle the CPU"
},
"tpm": {
"type": "boolean",
"title": "Enable the Trusted Platform Module (TPM)"
},
"uefi": {
"type": "boolean",
"title": "Enable the UEFI boot mode"
},
"on_close": {
"title": "Action to execute on the VM is closed",
"enum": [
"power_off",
"shutdown_signal",
"save_vm_state"
]
},
"process_priority": {
"title": "Process priority for QEMU",
"enum": [
"realtime",
"very high",
"high",
"normal",
"low",
"very low",
"null"
]
}
}
}
},
"properties": {
"appliance_id": {
"title": "Appliance ID",
"type": "string",
"minLength": 36,
"maxLength": 36,
"pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
},
"name": {
"type": "string",
"title": "Appliance name"
},
"category": {
"$ref": "#/definitions/categories",
"title": "Category of the appliance"
},
"description": {
"type": "string",
"title": "Description of the appliance. Could be a marketing description"
},
"vendor_name": {
"type": "string",
"title": "Name of the vendor"
},
"vendor_url": {
"type": "string",
"format": "uri",
"title": "Website of the vendor"
},
"documentation_url": {
"type": "string",
"format": "uri",
"title": "An optional documentation for using the appliance on vendor website"
},
"product_name": {
"type": "string",
"title": "Product name"
},
"product_url": {
"type": "string",
"format": "uri",
"title": "An optional product url on vendor website"
},
"registry_version": {
"enum": [
8
],
"title": "Version of the registry compatible with this appliance (version >=8 introduced breaking changes)"
},
"status": {
"enum": [
"stable",
"experimental",
"broken"
],
"title": "Document if the appliance is working or not"
},
"availability": {
"enum": [
"free",
"with-registration",
"free-to-try",
"service-contract"
],
"title": "About image availability: can be downloaded directly; download requires a free registration; paid but a trial version (time or feature limited) is available; not available publicly"
},
"maintainer": {
"type": "string",
"title": "Maintainer name"
},
"maintainer_email": {
"type": "string",
"format": "email",
"title": "Maintainer email"
},
"installation_instructions": {
"type": "string",
"title": "Optional installation instructions"
},
"usage": {
"type": "string",
"title": "How to use the appliance"
},
"default_username": {
"type": "string",
"title": "Default username for the appliance"
},
"default_password": {
"type": "string",
"title": "Default password for the appliance"
},
"symbol": {
"type": "string",
"title": "An optional symbol for the appliance"
},
"settings": {
"type": "array",
"title": "Settings for running the appliance",
"items": {
"type": "object",
"title": "Emulator settings",
"properties": {
"name": {
"type": "string",
"title": "Name of the settings set"
},
"default": {
"type": "boolean",
"title": "Whether these are the default settings"
},
"inherit_default_properties": {
"type": "boolean",
"title": "Whether the default properties should be used",
"default": "true"
},
"template_type": {
"enum": [
"docker",
"iou",
"dynamips",
"qemu"
],
"title": "Type of emulator properties"
},
"template_properties": {
"type": "object",
"title": "Properties for the template",
"oneOf": [
{
"$ref": "#/definitions/qemu_properties"
},
{
"$ref": "#/definitions/dynamips_properties"
},
{
"$ref": "#/definitions/iou_properties"
},
{
"$ref": "#/definitions/docker_properties"
}
]
}
},
"required": [
"template_type",
"template_properties"
]
}
}
},
"images": {
"type": "array",
"title": "Images for this appliance",
"items": {
"type": "object",
"title": "An image file",
"properties": {
"filename": {
"type": "string",
"title": "Filename"
},
"version": {
"type": "string",
"title": "Version of the image file"
},
"md5sum": {
"type": "string",
"title": "MD5 cheksum of the image file",
"pattern": "^[a-f0-9]{32}$"
},
"checksum": {
"type": "string",
"title": "checksum of the image file"
},
"checksum_type": {
"title": "checksum type of the image file",
"enum": [
"md5"
]
},
"filesize": {
"type": "integer",
"title": "File size in bytes of the image file"
},
"download_url": {
"type": "string",
"format": "uri",
"title": "Download URL where you can download the image file from a browser"
},
"direct_download_url": {
"type": "string",
"format": "uri",
"title": "Optional. Non authenticated URL to the image file where you can download the image directly"
},
"compression": {
"enum": [
"bzip2",
"gzip",
"lzma",
"xz",
"rar",
"zip",
"7z"
],
"title": "Optional, compression type of direct download URL image."
},
"compression_target": {
"type": "string",
"title": "Optional, file name of the image file inside the compressed file."
}
},
"anyOf": [
{
"required": [
"filename",
"version",
"md5sum",
"filesize"
]
},
{
"required": [
"filename",
"version",
"checksum",
"filesize"
]
}
]
}
},
"versions": {
"type": "array",
"title": "Versions of the appliance",
"items": {
"type": "object",
"title": "A version of the appliance",
"properties": {
"name": {
"type": "string",
"title": "Name of the version"
},
"settings": {
"type": "string",
"title": "Template settings to use to run the version"
},
"category": {
"$ref": "#/definitions/categories",
"title": "Category of the version"
},
"installation_instructions": {
"type": "string",
"title": "Optional installation instructions for the version"
},
"usage": {
"type": "string",
"title": "Optional instructions about using the version"
},
"default_username": {
"type": "string",
"title": "Default username for the version"
},
"default_password": {
"type": "string",
"title": "Default password for the version"
},
"symbol": {
"type": "string",
"title": "An optional symbol for the version"
},
"images": {
"type": "object",
"title": "Images used for this version",
"properties": {
"kernel_image": {
"type": "string",
"title": "Kernel image (Qemu only)"
},
"initrd": {
"type": "string",
"title": "Initrd disk image (Qemu only)"
},
"image": {
"type": "string",
"title": "OS image (IOU and Dynamips only)"
},
"bios_image": {
"type": "string",
"title": "Bios image (Qemu only)"
},
"hda_disk_image": {
"type": "string",
"title": "Hda disk image (Qemu only)"
},
"hdb_disk_image": {
"type": "string",
"title": "Hdc disk image (Qemu only)"
},
"hdc_disk_image": {
"type": "string",
"title": "Hdd disk image (Qemu only)"
},
"hdd_disk_image": {
"type": "string",
"title": "Hdd disk image (Qemu only)"
},
"cdrom_image": {
"type": "string",
"title": "cdrom image (Qemu only)"
}
}
}
},
"required": [
"name"
]
}
},
"required": [
"appliance_id",
"name",
"category",
"description",
"vendor_name",
"vendor_url",
"product_name",
"registry_version",
"status",
"maintainer",
"maintainer_email",
"settings"
]
}

View File

@@ -56,6 +56,7 @@ if sys.platform.startswith("win"):
program_files_x86 = program_files = os.environ["PROGRAMFILES"]
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {'Putty (normal standalone version)': 'putty_standalone.exe -telnet %h %p -loghost "%d"',
'KiTTY': r'kitty -title "%d" telnet://%h %p',
'MobaXterm': r'"{}\Mobatek\MobaXterm Personal Edition\MobaXterm.exe" -newtab "telnet %h %p"'.format(program_files_x86),
'Royal TS V3': r'{}\code4ward.net\Royal TS V3\RTS3App.exe /connectadhoc:%h /adhoctype:terminal /p:IsTelnetConnection="true" /p:ConnectionType="telnet;Telnet Connection" /p:Port="%p" /p:Name="%d"'.format(program_files),
'Royal TS V5': r'"{}\Royal TS V5\RoyalTS.exe" /protocol:terminal /using:adhoc /uri:"%h" /property:Port="%p" /property:IsTelnetConnection="true" /property:Name="%d"'.format(program_files_x86),
@@ -152,12 +153,12 @@ elif sys.platform.startswith("darwin"):
else:
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {'Xterm': 'xterm -T "%d" -e "telnet %h %p"',
'Putty': 'putty -telnet %h %p -title "%d" -sl 2500 -fg SALMON1 -bg BLACK',
'Gnome Terminal': 'gnome-terminal -t "%d" -e "telnet %h %p"',
'Gnome Terminal': 'gnome-terminal --tab -t "%d" -- telnet %h %p',
'Xfce4 Terminal': 'xfce4-terminal --tab -T "%d" -e "telnet %h %p"',
'ROXTerm': 'roxterm -n "%d" --tab -e "telnet %h %p"',
'KDE Konsole': 'konsole --new-tab -p tabtitle="%d" -e "telnet %h %p"',
'SecureCRT': 'SecureCRT /T /N "%d" /TELNET %h %p',
'Mate Terminal': 'mate-terminal --tab -e "telnet %h %p" -t "%d"',
'Mate Terminal': 'mate-terminal --tab -e "telnet %h %p" -t "%d"',
'terminator': 'terminator -e "telnet %h %p" -T "%d"',
'urxvt': 'urxvt -title %d -e telnet %h %p',
'kitty': 'kitty -T %d telnet %h %p'}
@@ -168,7 +169,10 @@ else:
if sys.platform.startswith("linux"):
distro_name = distro.name()
if distro_name == "Debian" or distro_name == "Ubuntu" or distro_name == "LinuxMint":
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Gnome Terminal"]
if shutil.which("mate-terminal"):
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Mate Terminal"]
else:
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Gnome Terminal"]
# Pre-configured VNC console commands on various OSes
if sys.platform.startswith("win"):
@@ -284,7 +288,6 @@ GENERAL_SETTINGS = {
"check_for_update": True,
"overlay_notifications": True,
"experimental_features": False,
"send_stats": True,
"stats_visitor_id": str(uuid.uuid4()), # An anonymous id for stats
"last_check_for_update": 0,
"telnet_console_command": DEFAULT_TELNET_CONSOLE_COMMAND,

View File

@@ -25,6 +25,8 @@ import os
import sys
import shlex
import subprocess
import psutil
from .main_window import MainWindow
from .controller import Controller
@@ -34,6 +36,39 @@ log = logging.getLogger(__name__)
console_mutex = QtCore.QMutex()
def gnome_terminal_env():
uid = os.getuid()
# get list of processes of current user
procs = [p.info for p in psutil.process_iter(
attrs=['name', 'pid', 'ppid', 'create_time', 'uids']
) if p.info['uids'].real == uid]
# get pid of gnome-terminal-server process
gnome_terminal_server_pid = [p['pid'] for p in procs if p['name'] == "gnome-terminal-server"]
if not gnome_terminal_server_pid:
return {}
gnome_terminal_server_pid = gnome_terminal_server_pid[0]
# get subprocesses of gnome-terminal-server
gnome_terminal_server_children = [p for p in procs if p['ppid'] == gnome_terminal_server_pid]
gnome_terminal_server_children.sort(key=lambda p: p['create_time'], reverse=True)
# return the gnome-terminal environment variables of the first subprocess named telnet
for proc in gnome_terminal_server_children:
if proc['name'] == "telnet":
try:
env = psutil.Process(proc['pid']).environ()
if 'GNOME_TERMINAL_SERVICE' in env and \
'GNOME_TERMINAL_SCREEN' in env:
return {'GNOME_TERMINAL_SERVICE': env['GNOME_TERMINAL_SERVICE'],
'GNOME_TERMINAL_SCREEN': env['GNOME_TERMINAL_SCREEN']}
except psutil.Error:
pass
return {}
class ConsoleThread(QtCore.QThread):
consoleError = QtCore.pyqtSignal(str)
@@ -60,7 +95,14 @@ class ConsoleThread(QtCore.QThread):
except ValueError:
self.consoleError.emit("Syntax error in command: '{}'".format(command))
return
subprocess.call(args, env=os.environ)
env = os.environ.copy()
# special case to force gnome-terminal to correctly use tabs on Linux
if sys.platform.startswith("linux") and "gnome-terminal" in args[0] and "--tab" in command:
# inject gnome-terminal environment variables
if "GNOME_TERMINAL_SERVICE" not in env or "GNOME_TERMINAL_SCREEN" not in env:
env.update(gnome_terminal_env())
subprocess.call(args, env=env)
def run(self):

View File

@@ -415,3 +415,22 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
for link in self._topology.links():
if link.suspended():
link.toggleSuspend()
def keyPressEvent(self, event):
"""
Handles key press events
"""
from .main_window import MainWindow
view = MainWindow.instance().uiGraphicsView
# only deleting a link or node is supported for now
if event.key() == QtCore.Qt.Key_Delete:
current_item = self.currentItem()
if isinstance(current_item, TopologyNodeItem):
current_item.node().delete()
else:
link = current_item.data(0, QtCore.Qt.UserRole)
for item in view.scene().items():
if isinstance(item, LinkItem) and item.link() == link:
item.delete()
super().keyPressEvent(event)

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>601</height>
<width>726</width>
<height>428</height>
</rect>
</property>
<property name="sizePolicy">
@@ -219,6 +219,27 @@
</item>
</layout>
</widget>
<widget class="QWizardPage" name="uiInstructionsPage">
<property name="title">
<string>Installation instructions</string>
</property>
<property name="subTitle">
<string>Please read the following instructions in order to install your new appliance.</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextEdit" name="uiInstructionsTextEdit">
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWizardPage" name="uiFilesWizardPage">
<property name="title">
<string>Required files</string>
@@ -232,12 +253,12 @@
<property name="indentation">
<number>20</number>
</property>
<attribute name="headerDefaultSectionSize">
<number>120</number>
</attribute>
<attribute name="headerMinimumSectionSize">
<number>20</number>
</attribute>
<attribute name="headerDefaultSectionSize">
<number>120</number>
</attribute>
<column>
<property name="text">
<string>Appliance version and files</string>

View File

@@ -2,16 +2,19 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/appliance_wizard.ui'
#
# Created by: PyQt5 UI code generator 5.9
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_ApplianceWizard(object):
def setupUi(self, ApplianceWizard):
ApplianceWizard.setObjectName("ApplianceWizard")
ApplianceWizard.resize(900, 601)
ApplianceWizard.resize(726, 428)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -112,6 +115,14 @@ class Ui_ApplianceWizard(object):
self.uiQemuListComboBox.setObjectName("uiQemuListComboBox")
self.horizontalLayout_2.addWidget(self.uiQemuListComboBox)
ApplianceWizard.addPage(self.uiQemuWizardPage)
self.uiInstructionsPage = QtWidgets.QWizardPage()
self.uiInstructionsPage.setObjectName("uiInstructionsPage")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.uiInstructionsPage)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.uiInstructionsTextEdit = QtWidgets.QTextEdit(self.uiInstructionsPage)
self.uiInstructionsTextEdit.setObjectName("uiInstructionsTextEdit")
self.verticalLayout_3.addWidget(self.uiInstructionsTextEdit)
ApplianceWizard.addPage(self.uiInstructionsPage)
self.uiFilesWizardPage = QtWidgets.QWizardPage()
self.uiFilesWizardPage.setObjectName("uiFilesWizardPage")
self.verticalLayout = QtWidgets.QVBoxLayout(self.uiFilesWizardPage)
@@ -174,6 +185,13 @@ class Ui_ApplianceWizard(object):
self.uiQemuWizardPage.setTitle(_translate("ApplianceWizard", "Qemu settings"))
self.uiQemuWizardPage.setSubTitle(_translate("ApplianceWizard", "Please choose the qemu binary that will be used to run this appliance."))
self.uiQemuListLabel.setText(_translate("ApplianceWizard", "Qemu binary:"))
self.uiInstructionsPage.setTitle(_translate("ApplianceWizard", "Installation instructions"))
self.uiInstructionsPage.setSubTitle(_translate("ApplianceWizard", "Please read the following instructions in order to install your new appliance."))
self.uiInstructionsTextEdit.setHtml(_translate("ApplianceWizard", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Ubuntu\'; font-size:11pt; font-weight:400; font-style:normal;\">\n"
"<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><br /></p></body></html>"))
self.uiFilesWizardPage.setTitle(_translate("ApplianceWizard", "Required files"))
self.uiFilesWizardPage.setSubTitle(_translate("ApplianceWizard", "The following files are required to install the appliance"))
self.uiApplianceVersionTreeWidget.headerItem().setText(0, _translate("ApplianceWizard", "Appliance version and files"))
@@ -191,5 +209,4 @@ class Ui_ApplianceWizard(object):
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Ubuntu\'; font-size:11pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">The default username/password is admin/admin. A default configuration is present.</p></body></html>"))
from . import resources_rc

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>941</width>
<height>910</height>
<width>512</width>
<height>652</height>
</rect>
</property>
<property name="windowTitle">
@@ -223,7 +223,16 @@
<string>Binary images</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -373,7 +382,16 @@
<string>Console applications</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -474,7 +492,16 @@
<string>VNC</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -943,7 +970,16 @@
<string>Miscellaneous</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -966,16 +1002,6 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="uiStatsCheckBox">
<property name="text">
<string>Send anonymous usage statistics</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="uiOverlayNotificationsCheckBox">
<property name="text">
@@ -1098,7 +1124,6 @@
<tabstop>uiDefaultNoteColorPushButton</tabstop>
<tabstop>uiCheckForUpdateCheckBox</tabstop>
<tabstop>uiCrashReportCheckBox</tabstop>
<tabstop>uiStatsCheckBox</tabstop>
<tabstop>uiOverlayNotificationsCheckBox</tabstop>
<tabstop>uiExperimentalFeaturesCheckBox</tabstop>
<tabstop>uiHdpiCheckBox</tabstop>

View File

@@ -2,9 +2,10 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/general_preferences_page.ui'
#
# Created by: PyQt5 UI code generator 5.13.2
# Created by: PyQt5 UI code generator 5.15.6
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
@@ -13,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_GeneralPreferencesPageWidget(object):
def setupUi(self, GeneralPreferencesPageWidget):
GeneralPreferencesPageWidget.setObjectName("GeneralPreferencesPageWidget")
GeneralPreferencesPageWidget.resize(941, 910)
GeneralPreferencesPageWidget.resize(512, 652)
self.verticalLayout = QtWidgets.QVBoxLayout(GeneralPreferencesPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiMiscTabWidget = QtWidgets.QTabWidget(GeneralPreferencesPageWidget)
@@ -446,10 +447,6 @@ class Ui_GeneralPreferencesPageWidget(object):
self.uiCrashReportCheckBox.setChecked(True)
self.uiCrashReportCheckBox.setObjectName("uiCrashReportCheckBox")
self.verticalLayout_2.addWidget(self.uiCrashReportCheckBox)
self.uiStatsCheckBox = QtWidgets.QCheckBox(self.uiMiscTab)
self.uiStatsCheckBox.setChecked(True)
self.uiStatsCheckBox.setObjectName("uiStatsCheckBox")
self.verticalLayout_2.addWidget(self.uiStatsCheckBox)
self.uiOverlayNotificationsCheckBox = QtWidgets.QCheckBox(self.uiMiscTab)
self.uiOverlayNotificationsCheckBox.setObjectName("uiOverlayNotificationsCheckBox")
self.verticalLayout_2.addWidget(self.uiOverlayNotificationsCheckBox)
@@ -520,8 +517,7 @@ class Ui_GeneralPreferencesPageWidget(object):
GeneralPreferencesPageWidget.setTabOrder(self.uiDefaultNoteFontPushButton, self.uiDefaultNoteColorPushButton)
GeneralPreferencesPageWidget.setTabOrder(self.uiDefaultNoteColorPushButton, self.uiCheckForUpdateCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiCheckForUpdateCheckBox, self.uiCrashReportCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiCrashReportCheckBox, self.uiStatsCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiStatsCheckBox, self.uiOverlayNotificationsCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiCrashReportCheckBox, self.uiOverlayNotificationsCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiOverlayNotificationsCheckBox, self.uiExperimentalFeaturesCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiExperimentalFeaturesCheckBox, self.uiHdpiCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiHdpiCheckBox, self.uiMultiProfilesCheckBox)
@@ -601,7 +597,6 @@ class Ui_GeneralPreferencesPageWidget(object):
self.uiMiscTabWidget.setTabText(self.uiMiscTabWidget.indexOf(self.uiSceneTab), _translate("GeneralPreferencesPageWidget", "Topology view"))
self.uiCheckForUpdateCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Automatically check for update"))
self.uiCrashReportCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Send anonymous crash reports"))
self.uiStatsCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Send anonymous usage statistics"))
self.uiOverlayNotificationsCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Display error, warning and info in an overlay popup"))
self.uiExperimentalFeaturesCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Enable experimental features (dangerous, restart required)"))
self.uiHdpiCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Enable HDPI mode (this may crash on Linux, restart required)"))

View File

@@ -2,6 +2,14 @@
<ui version="4.0">
<class>StyleEditorDialog</class>
<widget class="QDialog" name="StyleEditorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>270</width>
<height>294</height>
</rect>
</property>
<property name="windowTitle">
<string>Style editor</string>
</property>
@@ -76,14 +84,14 @@
<item row="3" column="1">
<widget class="QComboBox" name="uiBorderStyleComboBox"/>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QLabel" name="uiRotationLabel">
<property name="text">
<string>Rotation:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="5" column="1">
<widget class="QSpinBox" name="uiRotationSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@@ -106,6 +114,23 @@ editing (notes only) with ALT and '+' (or P) / ALT and '-' (or M)</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="uiCornerRadiusLabel">
<property name="text">
<string>Corner radius:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="uiCornerRadiusSpinBox">
<property name="suffix">
<string>°</string>
</property>
<property name="maximum">
<number>100</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/style_editor_dialog.ui'
#
# Created by: PyQt5 UI code generator 5.15.2
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -14,6 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_StyleEditorDialog(object):
def setupUi(self, StyleEditorDialog):
StyleEditorDialog.setObjectName("StyleEditorDialog")
StyleEditorDialog.resize(270, 294)
StyleEditorDialog.setModal(True)
self.verticalLayout = QtWidgets.QVBoxLayout(StyleEditorDialog)
self.verticalLayout.setObjectName("verticalLayout")
@@ -52,7 +53,7 @@ class Ui_StyleEditorDialog(object):
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.uiBorderStyleComboBox)
self.uiRotationLabel = QtWidgets.QLabel(self.uiStyleSettingsGroupBox)
self.uiRotationLabel.setObjectName("uiRotationLabel")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.uiRotationLabel)
self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.uiRotationLabel)
self.uiRotationSpinBox = QtWidgets.QSpinBox(self.uiStyleSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -62,7 +63,14 @@ class Ui_StyleEditorDialog(object):
self.uiRotationSpinBox.setMinimum(-360)
self.uiRotationSpinBox.setMaximum(360)
self.uiRotationSpinBox.setObjectName("uiRotationSpinBox")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.uiRotationSpinBox)
self.formLayout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.uiRotationSpinBox)
self.uiCornerRadiusLabel = QtWidgets.QLabel(self.uiStyleSettingsGroupBox)
self.uiCornerRadiusLabel.setObjectName("uiCornerRadiusLabel")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.uiCornerRadiusLabel)
self.uiCornerRadiusSpinBox = QtWidgets.QSpinBox(self.uiStyleSettingsGroupBox)
self.uiCornerRadiusSpinBox.setMaximum(100)
self.uiCornerRadiusSpinBox.setObjectName("uiCornerRadiusSpinBox")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.uiCornerRadiusSpinBox)
self.verticalLayout.addWidget(self.uiStyleSettingsGroupBox)
self.uiButtonBox = QtWidgets.QDialogButtonBox(StyleEditorDialog)
self.uiButtonBox.setOrientation(QtCore.Qt.Horizontal)
@@ -73,8 +81,8 @@ class Ui_StyleEditorDialog(object):
self.verticalLayout.addItem(spacerItem)
self.retranslateUi(StyleEditorDialog)
self.uiButtonBox.accepted.connect(StyleEditorDialog.accept)
self.uiButtonBox.rejected.connect(StyleEditorDialog.reject)
self.uiButtonBox.accepted.connect(StyleEditorDialog.accept) # type: ignore
self.uiButtonBox.rejected.connect(StyleEditorDialog.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(StyleEditorDialog)
def retranslateUi(self, StyleEditorDialog):
@@ -90,4 +98,6 @@ class Ui_StyleEditorDialog(object):
self.uiRotationSpinBox.setToolTip(_translate("StyleEditorDialog", "Rotation can be ajusted on the scene for a selected item while\n"
"editing (notes only) with ALT and \'+\' (or P) / ALT and \'-\' (or M)"))
self.uiRotationSpinBox.setSuffix(_translate("StyleEditorDialog", "°"))
self.uiCornerRadiusLabel.setText(_translate("StyleEditorDialog", "Corner radius:"))
self.uiCornerRadiusSpinBox.setSuffix(_translate("StyleEditorDialog", "°"))
from . import resources_rc

View File

@@ -24,7 +24,6 @@ import re
from gns3.utils import parse_version
from gns3 import version
from gns3.qt import QtNetwork, QtCore, QtWidgets, QtGui, qslot
from gns3.local_config import LocalConfig

View File

@@ -1,134 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import platform
import sys
from datetime import datetime
from urllib.parse import quote
from ..version import __version__
from ..qt import QtCore, QtNetwork, QtWidgets
from ..local_config import LocalConfig
from ..settings import GENERAL_SETTINGS
import logging
log = logging.getLogger(__name__)
class AnalyticsClient(QtCore.QObject):
"""
Google analytics client to send events.
"""
_property_id = "UA-55817127-3"
def __init__(self):
super().__init__()
self._visitor_id = None
self._manager = QtNetwork.QNetworkAccessManager(self)
def finished(network_reply):
try:
error = network_reply.error()
except TypeError:
# For unknow reason sometimes error is transform to a signal
# we receive few crash report about that, but we are not able
# to reproduce. We suspect the problem happen when the
# application is closing.
#
# https://github.com/GNS3/gns3-gui/issues/2011
return
if error != QtNetwork.QNetworkReply.NoError:
log.debug("Error when pushing to Google Analytics %s", network_reply.errorString())
self._manager.finished.connect(finished)
#
# We need to build a user agent for Universal Analytics in order to
# let analytics guess the OS
# this could break by analytics at anytime :(
if sys.platform.startswith("darwin"):
self._user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X {release}) AppleWebKit/537.36 (KHTML, like Gecko) GNS3/{version}".format(release=platform.mac_ver()[0].replace(".", "_"), version=__version__)
elif sys.platform.startswith("win"):
self._user_agent = "Mozilla/5.0 (Windows NT {release}) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 GNS3/{version}".format(release=platform.release(), version=__version__)
else:
self._user_agent = "Mozilla/5.0 (X11; Linux {arch}) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 GNS3/{version}".format(arch=platform.machine(), version=__version__)
self._rate_limit = {}
def sendScreenView(self, screen, session_start=None):
"""
:params session_start: True session start, None during session, False session stop
"""
if session_start is not False and screen in self._rate_limit:
if self._rate_limit[screen] + 60 * 1 > datetime.utcnow().timestamp():
log.debug("Ignore call %s to Google Analytics because of rate limiting", screen)
return
self._rate_limit[screen] = datetime.utcnow().timestamp()
settings = LocalConfig.instance().loadSectionSettings("MainWindow", GENERAL_SETTINGS)
if settings["send_stats"] is False:
log.debug("Stats is turn off ignore call %s", screen)
return
body = "v=1" # Version
body += "&tid={}".format(self._property_id) # Tracking ID / Property ID
body += "&cid={}".format(settings["stats_visitor_id"]) # Anonymous Client ID
body += "&aip=1" # Anonymize IP
body += "&t=screenview" # Screenview hit type
body += "&an=GNS3" # App name
body += "&av={}".format(quote(__version__)) # App version.
body += "&ua={}".format(quote(self._user_agent)) # User agent
body += "&cd={}".format(quote(screen)) # Category
body += "&ds=gns3-gui" # Data source
if session_start is True:
body += "&sc=start" # Session start
elif session_start is False:
body += "&sc=end" # Session end
screen = QtWidgets.QApplication.desktop().screenGeometry()
body += "&sr={}x{}".format(screen.width(), screen.height()) # Screen resolution
locale = QtCore.QLocale.system().name().lower()
if locale:
body += "&ul={}".format(locale) # User language
# TODO: HTTPS when possible because it's broken for the moment with Qt on OSX:
# https://bugreports.qt.io/browse/QTBUG-45487
if sys.platform.startswith("darwin"):
url = QtCore.QUrl('http://www.google-analytics.com/collect')
else:
url = QtCore.QUrl('https://www.google-analytics.com/collect')
request_qt = QtNetwork.QNetworkRequest(url)
request_qt.setRawHeader(b"Content-Type", b"application/x-www-form-urlencoded")
request_qt.setRawHeader(b"User-Agent", self._user_agent.encode())
self._manager.post(request_qt, body.encode())
log.debug("Send stats to Google Analytics: %s", body)
@staticmethod
def instance():
"""
Singleton to return only on instance of AnalyticsClient.
:returns: instance of AnalyticsClient
"""
if not hasattr(AnalyticsClient, '_instance') or AnalyticsClient._instance is None:
AnalyticsClient._instance = AnalyticsClient()
return AnalyticsClient._instance

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python
#
# Copyright (C) 2023 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/>.
# This script is intended to be built as a small executable for macOS to set the correct permissions on uBridge
import os
import shutil
import sys
def authorize_ubridge():
path = shutil.which("ubridge", path=os.path.dirname(sys.executable))
if path is None:
raise SystemExit("Could not find ubridge executable at {}".format(path))
try:
shutil.chown(path, "root", "admin")
os.chmod(path, 0o4750)
except OSError as e:
raise SystemExit("Could not authorize {}: {}".format(path, str(e)))
if __name__ == '__main__':
authorize_ubridge()

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
# Copyright (C) 2023 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -15,50 +15,35 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import tempfile
import pkg_resources
import atexit
import logging
import os
import sys
try:
import importlib_resources
except ImportError:
from importlib import resources as importlib_resources
from contextlib import ExitStack
resource_manager = ExitStack()
atexit.register(resource_manager.close)
log = logging.getLogger(__name__)
try:
egg_cache_dir = tempfile.mkdtemp()
pkg_resources.set_extraction_path(egg_cache_dir)
except ValueError:
# If the path is already set the module throw an error
pass
@atexit.register
def clean_egg_cache():
try:
import shutil
shutil.rmtree(egg_cache_dir, ignore_errors=True)
except Exception:
# We don't care if we can not cleanup
pass
def get_resource(resource_name):
"""
Return a resource in current directory or in frozen package
"""
resource_path = None
if hasattr(sys, "frozen"):
resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), resource_name))
if sys.platform.startswith("darwin") and not os.path.exists(resource_path):
resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), "lib", resource_name))
elif not hasattr(sys, "frozen"):
if pkg_resources.resource_exists("gns3", resource_name):
try:
resource_path = pkg_resources.resource_filename("gns3", resource_name)
except pkg_resources.ExtractionError as e:
log.fatal(e)
sys.stderr.write(e)
sys.exit(1)
resource_path = os.path.normpath(resource_path)
else:
resource_path = os.path.dirname(os.path.realpath(__file__))
resource_path = os.path.join(resource_path, "..", "..", "resources", resource_name)
else:
ref = importlib_resources.files("gns3") / resource_name
path = resource_manager.enter_context(importlib_resources.as_file(ref))
if os.path.exists(path):
resource_path = os.path.normpath(path)
return resource_path

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python
#
# Copyright (C) 2023 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import shutil
import logging
log = logging.getLogger(__name__)
def macos_ubridge_setuid():
# AuthorizationExecuteWithPrivileges() has been deprecated since macOS 10.7 but it still works
# and much simpler than using SMJobBless() which requires a separate helper tool
import ctypes
import ctypes.util
from ctypes import byref
authorize_ubridge = shutil.which("authorize_ubridge", path=os.path.dirname(sys.executable))
if authorize_ubridge is None:
raise OSError("Could not find the authorize_ubridge executable")
# https://developer.apple.com/documentation/security
sec = ctypes.cdll.LoadLibrary(ctypes.util.find_library("Security"))
try:
sec.AuthorizationCreate
except AttributeError:
raise OSError("macOS security library does not support AuthorizationCreate")
kAuthorizationFlagDefaults = 0
auth = ctypes.c_void_p()
r_auth = byref(auth)
err = sec.AuthorizationCreate(None, None, kAuthorizationFlagDefaults, r_auth)
if err:
raise OSError("Could not create authorization: {}".format(err))
exe = [authorize_ubridge]
log.info("Executing '{}' with privileges".format(exe))
args = (ctypes.c_char_p * len(exe))()
for i, arg in enumerate(exe[1:]):
args[i] = arg.encode('utf8')
io = ctypes.c_void_p()
err = sec.AuthorizationExecuteWithPrivileges(auth, exe[0].encode('utf8'), 0, args, byref(io))
if err:
raise OSError("Could not authorize uBridge: {}".format(err))
else:
log.info("Successfully authorized uBridge")

View File

@@ -15,9 +15,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import shlex
import subprocess
import sys
from gns3.qt import QtWidgets
from gns3.utils.progress_dialog import ProgressDialog

View File

@@ -23,8 +23,9 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "2.2.39"
__version_info__ = (2, 2, 39, 0)
__version__ = "2.2.43"
__version_info__ = (2, 2, 43, 0)
if "dev" in __version__:
try:

View File

@@ -1,3 +1,3 @@
-rrequirements.txt
PyQt5==5.15.7
PyQt5==5.15.9

View File

@@ -1,7 +1,9 @@
jsonschema>=4.17.3,<4.18; python_version >= '3.7'
jsonschema>=4.17.3,<4.18; python_version >= '3.7' # v4.17.3 is the last version to support Python 3.7
jsonschema==3.2.0; python_version < '3.7' # v3.2.0 is the last version to support Python 3.6
sentry-sdk==1.17.0,<1.18
psutil==5.9.4
sentry-sdk==1.31.0,<1.32
psutil==5.9.5
distro>=1.8.0
truststore>=0.8.0; python_version >= '3.10'
importlib-resources>=1.3; python_version <= '3.9'
setuptools>=60.8.1; python_version >= '3.7'
setuptools==59.6.0; python_version < '3.7' # v59.6.0 is the last version to support Python 3.6

View File

@@ -172,7 +172,7 @@ def local_server_config():
return LocalServerConfig.instance()
@pytest.yield_fixture(autouse=True)
@pytest.fixture(autouse=True)
def run_around_tests(local_config, main_window):
"""
This setup a temporay environnement around tests

View File

@@ -0,0 +1,50 @@
{
"appliance_id": "1c784362-8aaf-4312-b0f5-4b138cf2e25b",
"name": "Arista vEOS",
"category": "router",
"description": "Arista EOS® is the core of Arista cloud networking solutions for next-generation data centers and cloud networks. Cloud architectures built with Arista EOS scale to tens of thousands of compute and storage nodes with management and provisioning capabilities that work at scale. Through its programmability, EOS enables a set of software applications that deliver workflow automation, high availability, unprecedented network visibility and analytics and rapid integration with a wide range of third-party applications for virtualization, management, automation and orchestration services.\n\nArista Extensible Operating System (EOS) is a fully programmable and highly modular, Linux-based network operation system, using familiar industry standard CLI and runs a single binary software image across the Arista switching family. Architected for resiliency and programmability, EOS has a unique multi-process state sharing architecture that separates state information and packet forwarding from protocol processing and application logic.",
"vendor_name": "Arista",
"vendor_url": "http://www.arista.com/",
"documentation_url": "http://www.arista.com/docs/Manuals/ConfigGuide.pdf",
"product_name": "Arista vEOS",
"product_url": "https://eos.arista.com/",
"registry_version": 8,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"settings": [
{
"template_type": "qemu",
"template_properties": {
"adapter_type": "e1000",
"adapters": 8,
"ram": 2048,
"platform": "x86_64",
"console_type": "telnet"
}
}
],
"images": [
{
"filename": "Aboot-veos-serial-2.1.0.iso",
"version": "2.1.0",
"md5sum": "2687534f2ff11b998dec0511066457c0",
"download_url": "https://www.arista.com/en/support/software-download"
},
{
"filename": "vEOS-lab-4.13.8M.vmdk",
"version": "4.13.8M",
"md5sum": "a47145b9e6e7a24171c0850f8755535e",
"download_url": "https://www.arista.com/en/support/software-download"
}
],
"versions": [
{
"name": "4.13.8M",
"images": {
"hda_disk_image": "Aboot-veos-serial-2.1.0.iso",
"hdb_disk_image": "vEOS-lab-4.13.8M.vmdk"
}
}
]
}

View File

@@ -1,4 +1,5 @@
{
"appliance_id": "1c784362-8aaf-4312-b0f5-4b138cf2e25b",
"name": "Arista vEOS",
"category": "router",
"description": "Arista EOS® is the core of Arista cloud networking solutions for next-generation data centers and cloud networks. Cloud architectures built with Arista EOS scale to tens of thousands of compute and storage nodes with management and provisioning capabilities that work at scale. Through its programmability, EOS enables a set of software applications that deliver workflow automation, high availability, unprecedented network visibility and analytics and rapid integration with a wide range of third-party applications for virtualization, management, automation and orchestration services.\n\nArista Extensible Operating System (EOS) is a fully programmable and highly modular, Linux-based network operation system, using familiar industry standard CLI and runs a single binary software image across the Arista switching family. Architected for resiliency and programmability, EOS has a unique multi-process state sharing architecture that separates state information and packet forwarding from protocol processing and application logic.",
@@ -11,7 +12,6 @@
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"qemu": {
"adapter_type": "e1000",
"adapters": 8,
@@ -19,7 +19,6 @@
"arch": "x86_64",
"console_type": "telnet"
},
"images": [
{
"filename": "Aboot-veos-serial-2.1.0.iso",
@@ -34,7 +33,6 @@
"download_url": "https://www.arista.com/en/support/software-download"
}
],
"versions": [
{
"name": "4.13.8M",

View File

@@ -11,7 +11,6 @@
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"qemu": {
"adapter_type": "e1000",
"adapters": 1,
@@ -19,7 +18,6 @@
"arch": "i386",
"console_type": "telnet"
},
"images": [
{
"filename": "linux-microcore-3.4.1.img",
@@ -30,7 +28,6 @@
"direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/linux-microcore-3.4.1.img"
}
],
"versions": [
{
"name": "3.4.1",

View File

@@ -0,0 +1,51 @@
{
"appliance_id": "96dac9ff-581c-4262-a9f0-5c68890d049e",
"category": "router",
"status": "experimental",
"maintainer": "GNS3 Team",
"name": "Cisco 3745",
"vendor_name": "Cisco",
"product_name": "3745",
"vendor_url": "http://www.cisco.com",
"description": "Cisco 3745 Multiservice Access Router",
"registry_version": 8,
"maintainer_email": "developers@gns3.net",
"documentation_url": "http://www.cisco.com/c/en/us/support/routers/3745-multiservice-access-router/model.html",
"settings": [
{
"template_type": "dynamips",
"template_properties": {
"chassis": "",
"platform": "c3745",
"ram": 256,
"nvram": 256,
"startup_config": "ios_base_startup-config.txt",
"slot0": "GT96100-FE",
"slot1": "NM-1FE-TX",
"slot2": "NM-4T",
"slot3": "",
"slot4": "",
"wic0": "WIC-1T",
"wic1": "WIC-1T",
"wic2": "WIC-1T",
"idlepc": "0x60aa1da0"
}
}
],
"versions": [
{
"name": "124-25d",
"images": {
"image": "c3745-adventerprisek9-mz.124-25d.image"
}
}
],
"images": [
{
"filesize": 82053028,
"md5sum": "ddbaf74274822b50fa9670e10c75b08f",
"version": "124-25d",
"filename": "c3745-adventerprisek9-mz.124-25d.image"
}
]
}

View File

@@ -1,4 +1,5 @@
{
"appliance_id": "96dac9ff-581c-4262-a9f0-5c68890d049e",
"category": "router",
"status": "experimental",
"maintainer": "GNS3 Team",
@@ -10,7 +11,6 @@
"registry_version": 2,
"maintainer_email": "developers@gns3.net",
"documentation_url": "http://www.cisco.com/c/en/us/support/routers/3745-multiservice-access-router/model.html",
"dynamips": {
"chassis": "",
"platform": "c3745",
@@ -26,7 +26,6 @@
"wic1": "WIC-1T",
"wic2": "WIC-1T"
},
"versions": [
{
"images": {

View File

@@ -0,0 +1,41 @@
{
"appliance_id": "a8f5935d-7229-4b32-8a01-24ef41758f2f",
"category": "router",
"status": "experimental",
"maintainer": "GNS3 Team",
"name": "Cisco IOU L3",
"vendor_name": "Cisco",
"product_name": "Cisco IOU L3",
"vendor_url": "http://www.cisco.com",
"description": "Cisco IOS on UNIX Layer 3 image.",
"registry_version": 8,
"maintainer_email": "developers@gns3.net",
"settings": [
{
"template_type": "iou",
"template_properties": {
"ethernet_adapters": 2,
"serial_adapters": 2,
"nvram": 128,
"ram": 256,
"startup_config": "iou_l3_base_startup-config.txt"
}
}
],
"versions": [
{
"images": {
"image": "i86bi-linux-l3-adventerprisek9-15.4.1T.bin"
},
"name": "15.4.1T"
}
],
"images": [
{
"filesize": 152677848,
"md5sum": "5d41402abc4b2a76b9719d911017c592",
"version": "15.4.1T",
"filename": "i86bi-linux-l3-adventerprisek9-15.4.1T.bin"
}
]
}

View File

@@ -1,4 +1,5 @@
{
"appliance_id": "a8f5935d-7229-4b32-8a01-24ef41758f2f",
"category": "router",
"status": "experimental",
"maintainer": "GNS3 Team",
@@ -9,7 +10,6 @@
"description": "Cisco IOS on UNIX Layer 3 image.",
"registry_version": 2,
"maintainer_email": "developers@gns3.net",
"iou": {
"ethernet_adapters": 2,
"serial_adapters": 2,
@@ -17,7 +17,6 @@
"ram": 256,
"startup_config": "iou_l3_base_startup-config.txt"
},
"versions": [
{
"images": {

View File

@@ -0,0 +1,115 @@
{
"appliance_id": "1cfdf900-7c30-4cb7-8f03-3f61d2581633",
"name": "Empty VM",
"category": "guest",
"description": "A empty VM with empty hard disks 8G, 30G, 100G & 200G.",
"vendor_name": "GNS3",
"vendor_url": "https://gns3.com",
"documentation_url": "",
"product_name": "QEMU",
"product_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"registry_version": 8,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "Default at first boot the VM will start from the cdrom.",
"settings": [
{
"default": true,
"template_type": "qemu",
"template_properties": {
"adapter_type": "e1000",
"adapters": 1,
"ram": 1024,
"hda_disk_interface": "sata",
"platform": "x86_64",
"console_type": "vnc",
"boot_priority": "d"
}
},
{
"name": "i386 settings",
"template_type": "qemu",
"template_properties": {
"platform": "i386",
"adapters": 8
}
},
{
"name": "ARM settings",
"inherit_default_properties": false,
"template_type": "qemu",
"template_properties": {
"platform": "arm",
"ram": 512
}
}
],
"images": [
{
"filename": "empty8G.qcow2",
"version": "8G",
"md5sum": "f1d2c25b6990f99bd05b433ab603bdb4",
"filesize": 197120,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty8G.qcow2/download"
},
{
"filename": "empty30G.qcow2",
"version": "30G",
"checksum": "3411a599e822f2ac6be560a26405821a",
"filesize": 197120,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty30G.qcow2/download"
},
{
"filename": "empty100G.qcow2",
"version": "100G",
"checksum": "d08fdec95fffbda3f04e9a00db49295df73ae4a507396e442ba9e4ad5c14ce5a",
"checksum_type": "sha256",
"filesize": 198656,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty100G.qcow2/download"
},
{
"filename": "empty200G.qcow2",
"version": "200G",
"md5sum": "d1686d2f25695dee32eab9a6f4652c7c",
"filesize": 200192,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty200G.qcow2/download"
}
],
"versions": [
{
"name": "8G",
"images": {
"hda_disk_image": "empty8G.qcow2"
}
},
{
"name": "30G",
"settings": "i386 settings",
"images": {
"hda_disk_image": "empty30G.qcow2"
}
},
{
"name": "100G",
"settings": "ARM settings",
"images": {
"hda_disk_image": "empty100G.qcow2"
}
},
{
"name": "200G",
"settings": "ARM settings",
"usage": "This is how to use this version",
"symbol": "ethernet_switch",
"category": "switch",
"images": {
"hda_disk_image": "empty200G.qcow2"
}
}
]
}

View File

@@ -3,8 +3,6 @@
"vendor_name": "Juniper",
"product_name": "vSRX",
"name": "vSRX",
"description": "The vSRX delivers core firewall, networking, advanced security, and automated lifecycle management capabilities for enterprises and service providers. The industry\u2019s fastest virtual security platform, the vSRX offers firewall speeds up to 17 Gbps using only two virtual CPUs, providing scalable, secure protection across private, public, and hybrid clouds.",
"maintainer_email": "developers@gns3.net",
"documentation_url": "http://www.juniper.net/techpubs/",
@@ -13,7 +11,6 @@
"status": "experimental",
"vendor_url": "https://www.juniper.net",
"registry_version": 1,
"qemu": {
"console_type": "telnet",
"ram": 2000,
@@ -23,8 +20,6 @@
"options": "-smp 2",
"kvm": "require"
},
"images": [
{
"version": "12.1X47.4-domestic",
@@ -34,7 +29,6 @@
"download_url": "https://www.juniper.net/us/en/dm/free-vsrx-trial/"
}
],
"versions": [
{
"name": "12.1X47.4-domestic",
@@ -43,4 +37,4 @@
}
}
]
}
}

View File

@@ -12,7 +12,6 @@
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "Just start the appliance",
"qemu": {
"adapter_type": "e1000",
"adapters": 1,
@@ -21,7 +20,6 @@
"console_type": "telnet",
"kvm": "allow"
},
"images": [
{
"filename": "linux-microcore-3.4.1.img",
@@ -40,7 +38,6 @@
"direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/linux-microcore-4.0.2-clean.img"
}
],
"versions": [
{
"name": "3.4.1",

View File

@@ -0,0 +1,24 @@
{
"appliance_id": "40c84186-752f-4a71-b6fb-961ba36e8cf7",
"name": "Open vSwitch",
"category": "multilayer_switch",
"description": "Open vSwitch is a production quality, multilayer virtual switch licensed under the open source Apache 2.0 license. It is designed to enable massive network automation through programmatic extension, while still supporting standard management interfaces and protocols (e.g. NetFlow, sFlow, IPFIX, RSPAN, CLI, LACP, 802.1ag). In addition, it is designed to support distribution across multiple physical servers similar to VMware's vNetwork distributed vswitch or Cisco's Nexus 1000V.",
"vendor_name": "Open vSwitch",
"vendor_url": "http://openvswitch.org/",
"documentation_url": "http://openvswitch.org/support/",
"product_name": "Open vSwitch",
"registry_version": 8,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "By default all interfaces are connected to the br0",
"settings": [
{
"template_type": "docker",
"template_properties": {
"adapters": 16,
"image": "gns3/openvswitch:latest"
}
}
]
}

View File

@@ -1,4 +1,5 @@
{
"appliance_id": "40c84186-752f-4a71-b6fb-961ba36e8cf7",
"name": "Open vSwitch",
"category": "multilayer_switch",
"description": "Open vSwitch is a production quality, multilayer virtual switch licensed under the open source Apache 2.0 license. It is designed to enable massive network automation through programmatic extension, while still supporting standard management interfaces and protocols (e.g. NetFlow, sFlow, IPFIX, RSPAN, CLI, LACP, 802.1ag). In addition, it is designed to support distribution across multiple physical servers similar to VMware's vNetwork distributed vswitch or Cisco's Nexus 1000V.",

View File

@@ -202,6 +202,62 @@ def test_create_new_version():
os.remove(wrong_appliance_file)
def test_emulator():
assert Appliance(registry, os.path.abspath("tests/registry/appliances/microcore-linux.gns3a")).emulator() == "qemu"
assert Appliance(registry, os.path.abspath("tests/registry/appliances/cisco-iou-l3.gns3a")).emulator() == "iou"
def test_template_type():
assert Appliance(registry, os.path.abspath("tests/registry/appliances/microcore-linux.gns3a")).template_type() == "qemu"
assert Appliance(registry, os.path.abspath("tests/registry/appliances/cisco-iou-l3.gns3a")).template_type() == "iou"
def test_checksum_in_appliance_format_v8(registry, images_dir):
path_empty_8g = os.path.join(images_dir, "QEMU", "empty8G.qcow2")
with open(path_empty_8g, 'w+') as f:
f.write("hello")
appliance = Appliance(registry, os.path.abspath("tests/registry/appliances/empty-vm-v8.gns3a"))
appliance._appliance['versions'][0]['images']['hda_disk_image']['filesize'] = 5
appliance._appliance['versions'][0]['images']['hda_disk_image']['md5sum'] = "5d41402abc4b2a76b9719d911017c592"
detected = appliance.search_images_for_version("8G")
assert detected["images"][0]["md5sum"] == "5d41402abc4b2a76b9719d911017c592"
assert detected["images"][0]["filesize"] == 5
path_empty_30g = os.path.join(images_dir, "QEMU", "empty30G.qcow2")
os.rename(path_empty_8g, path_empty_30g)
appliance._appliance['versions'][1]['images']['hda_disk_image']['filesize'] = 5
appliance._appliance['versions'][1]['images']['hda_disk_image']['checksum'] = "5d41402abc4b2a76b9719d911017c592"
detected = appliance.search_images_for_version("30G")
assert detected["images"][0]["md5sum"] == "5d41402abc4b2a76b9719d911017c592"
assert detected["images"][0]["filesize"] == 5
with pytest.raises(ApplianceError):
appliance.search_images_for_version("100G")
def test_multiple_different_template_types_found():
appliance_path = "tests/registry/appliances/empty-vm-v8.gns3a"
wrong_appliance_fp, wrong_appliance_file = tempfile.mkstemp()
with open(appliance_path, encoding='utf-8') as f:
appliance = json.loads(f.read())
appliance["settings"][0]["template_type"] = "dynamips"
os.write(wrong_appliance_fp, json.dumps(appliance).encode())
os.close(wrong_appliance_fp)
with pytest.raises(ApplianceError):
Appliance(registry, wrong_appliance_file).template_type()
def test_appliance_without_settings():
appliance_path = "tests/registry/appliances/empty-vm-v8.gns3a"
wrong_appliance_fp, wrong_appliance_file = tempfile.mkstemp()
with open(appliance_path, encoding='utf-8') as f:
appliance = json.loads(f.read())
del appliance["settings"]
os.write(wrong_appliance_fp, json.dumps(appliance).encode())
os.close(wrong_appliance_fp)
with pytest.raises(ApplianceError):
Appliance(registry, wrong_appliance_file).template_type()

View File

@@ -64,8 +64,15 @@ def empty_config(tmpdir, images_dir, symbols_dir, local_server_config):
return Config(path)
def test_add_appliance_iou(iou_l3):
with open("tests/registry/appliances/cisco-iou-l3.gns3a", encoding="utf-8") as f:
@pytest.mark.parametrize(
"appliance_file",
[
"cisco-iou-l3.gns3a",
"cisco-iou-l3-v8.gns3a"
]
)
def test_add_appliance_iou(iou_l3, appliance_file):
with open("tests/registry/appliances/{}".format(appliance_file), encoding="utf-8") as f:
config = json.load(f)
config["images"] = [
{
@@ -74,7 +81,7 @@ def test_add_appliance_iou(iou_l3):
"path": iou_l3
}
]
new_template = ApplianceToTemplate().new_template(config, "local")
new_template = ApplianceToTemplate().new_template(config, "local", "15.4.1T")
assert new_template == {
"category": "router",
"template_type": "iou",
@@ -91,8 +98,15 @@ def test_add_appliance_iou(iou_l3):
}
def test_add_appliance_docker():
with open("tests/registry/appliances/openvswitch.gns3a", encoding="utf-8") as f:
@pytest.mark.parametrize(
"appliance_file",
[
"openvswitch.gns3a",
"openvswitch-v8.gns3a"
]
)
def test_add_appliance_docker(appliance_file):
with open("tests/registry/appliances/{}".format(appliance_file), encoding="utf-8") as f:
config = json.load(f)
new_template = ApplianceToTemplate().new_template(config, "local")
@@ -108,8 +122,15 @@ def test_add_appliance_docker():
}
def test_add_appliance_dynamips(cisco_3745):
with open("tests/registry/appliances/cisco-3745.gns3a", encoding="utf-8") as f:
@pytest.mark.parametrize(
"appliance_file",
[
"cisco-3745.gns3a",
"cisco-3745-v8.gns3a"
]
)
def test_add_appliance_dynamips(cisco_3745, appliance_file):
with open("tests/registry/appliances/{}".format(appliance_file), encoding="utf-8") as f:
config = json.load(f)
config["images"] = [
{
@@ -120,7 +141,7 @@ def test_add_appliance_dynamips(cisco_3745):
}
]
new_template = ApplianceToTemplate().new_template(config, "local")
new_template = ApplianceToTemplate().new_template(config, "local", "124-25d")
assert new_template == {
"template_type": "dynamips",
"category": "router",
@@ -166,7 +187,6 @@ def test_add_appliance_guest(linux_microcore_img):
"symbol": ":/symbols/qemu_guest.svg",
"hda_disk_image": "linux-microcore-3.4.1.img",
"name": "Micro Core Linux",
"options": "",
"qemu_path": "qemu-system-i386",
"usage": "Just start the appliance",
"ram": 32,
@@ -238,8 +258,15 @@ def test_add_appliance_with_boot_priority(linux_microcore_img):
assert new_template["boot_priority"] == "dc"
def test_add_appliance_router_two_disk(images_dir):
with open("tests/registry/appliances/arista-veos.gns3a", encoding="utf-8") as f:
@pytest.mark.parametrize(
"appliance_file",
[
"arista-veos.gns3a",
"arista-veos-v8.gns3a"
]
)
def test_add_appliance_router_two_disk(images_dir, appliance_file):
with open("tests/registry/appliances/{}".format(appliance_file), encoding="utf-8") as f:
config = json.load(f)
config["images"] = [
@@ -255,8 +282,8 @@ def test_add_appliance_router_two_disk(images_dir):
}
]
new_template = ApplianceToTemplate().new_template(config, "local")
assert new_template == {
new_template = ApplianceToTemplate().new_template(config, "local", "4.13.8M")
expected_result = {
"template_type": "qemu",
"adapter_type": "e1000",
"adapters": 8,
@@ -265,12 +292,76 @@ def test_add_appliance_router_two_disk(images_dir):
"hda_disk_image": "a",
"hdb_disk_image": "b",
"name": "Arista vEOS",
"options": "",
"qemu_path": "qemu-system-x86_64",
"ram": 2048,
"console_type": "telnet",
"compute_id": "local"
}
if "v8" in appliance_file:
expected_result["platform"] = "x86_64" # platform was added in v8
assert new_template == expected_result
def test_add_appliance_v8_default_properties_inheritance(images_dir):
with open("tests/registry/appliances/empty-vm-v8.gns3a", encoding="utf-8") as f:
config = json.load(f)
# check that default properties are used
new_template = ApplianceToTemplate().new_template(config, "local", "8G")
expected_result = {
"name": "Empty VM",
"template_type": "qemu",
"symbol": ":/symbols/qemu_guest.svg",
"category": "guest",
"adapter_type": "e1000",
"adapters": 1,
"ram": 1024,
"qemu_path": "qemu-system-x86_64",
"hda_disk_interface": "sata",
"platform": "x86_64",
"console_type": "vnc",
"boot_priority": "d",
"compute_id": "local",
"usage": "Default at first boot the VM will start from the cdrom."
}
assert new_template == expected_result
# check that specific properties are used along with default properties
new_template = ApplianceToTemplate().new_template(config, "local", "30G")
expected_result.update(
{
"adapters": 8,
"qemu_path": "qemu-system-i386",
"platform": "i386",
}
)
assert new_template == expected_result
# check that specific properties are used along without default properties
new_template = ApplianceToTemplate().new_template(config, "local", "100G")
expected_result = {
"name": "Empty VM",
"template_type": "qemu",
"symbol": ":/symbols/qemu_guest.svg",
"category": "guest",
"ram": 512,
"qemu_path": "qemu-system-arm",
"platform": "arm",
"compute_id": "local",
"usage": "Default at first boot the VM will start from the cdrom."
}
assert new_template == expected_result
# check that specific properties are used with "usage", "symbol" and "category" defined at the version level
new_template = ApplianceToTemplate().new_template(config, "local", "200G")
expected_result.update(
{
"usage": "This is how to use this version",
"symbol": "ethernet_switch",
"category": "switch"
}
)
assert new_template == expected_result
def test_add_appliance_path_relative_to_images_dir(tmpdir, linux_microcore_img):

View File

@@ -47,7 +47,7 @@ def http_client(http_request, network_manager):
return HTTPClient({"protocol": "http", "host": "127.0.0.1", "port": "3080"}, network_manager=network_manager)
@pytest.yield_fixture(autouse=True)
@pytest.fixture(autouse=True)
def http_request():
mock = unittest.mock.Mock()

View File

@@ -34,7 +34,7 @@ def images_dir(tmpdir):
return path
@pytest.yield_fixture
@pytest.fixture
def image_manager(tmpdir, images_dir):
ImageManager._instance = None
settings = LOCAL_SERVER_SETTINGS

View File

@@ -32,7 +32,7 @@ def local_server_path(tmpdir):
return str(tmpdir / "gns3server")
@pytest.yield_fixture
@pytest.fixture
def local_server(local_server_path, tmpdir):
with open(str(tmpdir / "test.cfg"), "w+") as f:
f.write("""

View File

@@ -26,14 +26,14 @@ from gns3.update_manager import UpdateManager
from gns3 import version
@pytest.yield_fixture
@pytest.fixture
def frozen():
sys.frozen = True
yield
delattr(sys, 'frozen')
@pytest.yield_fixture
@pytest.fixture
def devVersion():
old_version_info = version.__version_info__
old_version = version.__version__
@@ -44,7 +44,7 @@ def devVersion():
version.__version__ = old_version
@pytest.yield_fixture
@pytest.fixture
def stableVersion():
old_version_info = version.__version_info__
old_version = version.__version__

View File

@@ -1,4 +1,4 @@
-rrequirements.txt
PyQt5==5.15.7 # pyup: ignore
pywin32==305 # pyup: ignore
PyQt5==5.15.9 # pyup: ignore
pywin32==306 # pyup: ignore