Skip to content

Commit 5c57de7

Browse files
authored
Merge pull request #3775 from GNS3/release/v2.2.56
Release v2.2.56
2 parents 8dc3ef0 + b2b50ce commit 5c57de7

File tree

218 files changed

+3489
-3668
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

218 files changed

+3489
-3668
lines changed

CHANGELOG

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Change Log
22

3+
## 2.2.56 21/01/2026
4+
5+
* Fixing tab name in MobaXterm
6+
* PyQt6 migration
7+
* Add XDG Config Home support
8+
9+
## 2.2.55 19/11/2025
10+
11+
* Fix SyntaxWarning: invalid escape sequence. Fixes #3760
12+
* Support for Python 3.14
13+
* Clicking the "console connect to all nodes" opens all consoles in name order with case-insensitively
14+
315
## 2.2.54 21/04/2025
416

517
* Replace "Docker hub" by "Docker repository" because it is possible to use different repositories

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Please see <https://docs.gns3.com/>
1717
Software dependencies
1818
---------------------
1919

20-
PyQt5 which is either part of the Linux distribution or installable from
20+
PyQt6 which is either part of the Linux distribution or installable from
2121
PyPi. The other Python dependencies are automatically installed during
2222
the GNS3 GUI installation and are listed
2323
[here](https://github.com/GNS3/gns3-gui/blob/master/requirements.txt)

gns3/application.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,11 @@
2727

2828

2929
class Application(QtWidgets.QApplication):
30-
file_open_signal = QtCore.pyqtSignal(str)
30+
file_open_signal = QtCore.Signal(str)
3131

32-
def __init__(self, argv, hdpi=True):
32+
def __init__(self, argv):
3333

3434
self.setStyle(QtWidgets.QStyleFactory.create("Fusion"))
35-
# both Qt and PyQt must be version >= 5.6 in order to enable high DPI scaling
36-
if parse_version(QtCore.QT_VERSION_STR) >= parse_version("5.6") and parse_version(QtCore.PYQT_VERSION_STR) >= parse_version("5.6"):
37-
# only available starting Qt version 5.6
38-
if hdpi:
39-
if sys.platform.startswith("linux"):
40-
log.warning("HDPI mode is enabled. HDPI support on Linux is not fully stable and GNS3 may crash depending of your version of Linux. To disabled HDPI mode please edit ~/.config/GNS3/gns3_gui.conf and set 'hdpi' to 'false'")
41-
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
42-
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
43-
else:
44-
log.info("HDPI mode is disabled")
45-
self.setAttribute(QtCore.Qt.AA_DisableHighDpiScaling)
46-
4735
super().__init__(argv)
4836

4937
# this is tell Wayland what is the name of the desktop file (gns3.desktop)
@@ -60,7 +48,7 @@ def __init__(self, argv, hdpi=True):
6048
self.open_file_at_startup = None
6149

6250
def event(self, event):
63-
# When you double click file you receive an event
51+
# When you double click on a file, you receive an event
6452
# and not the file as command line parameter
6553
if sys.platform.startswith("darwin"):
6654
if isinstance(event, QtGui.QFileOpenEvent):

gns3/compute_summary_view.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def _refreshStatusSlot(self):
8282
self._status = "stopped"
8383
self.setToolTip(0, "{} is stopped or cannot be reached".format(self._compute.name()))
8484
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
85-
self._parent.sortItems(0, QtCore.Qt.AscendingOrder)
85+
self._parent.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder)
8686

8787
# add nodes belonging to this compute
8888
self.takeChildren()
@@ -98,7 +98,7 @@ def _refreshStatusSlot(self):
9898
else:
9999
item.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
100100
self.addChild(item)
101-
self.sortChildren(0, QtCore.Qt.AscendingOrder)
101+
self.sortChildren(0, QtCore.Qt.SortOrder.AscendingOrder)
102102

103103

104104
class ComputeSummaryView(QtWidgets.QTreeWidget):

gns3/console_view.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import datetime
2323
import platform
2424

25-
from .qt import QtCore, QtWidgets
25+
from .qt import QtCore, QtGui
2626
from .topology import Topology
2727
from .version import __version__
2828
from .console_cmd import ConsoleCmd
@@ -117,10 +117,10 @@ def contextMenuEvent(self, event):
117117
"""
118118

119119
menu = self.createStandardContextMenu()
120-
delete_all_action = QtWidgets.QAction("Delete All", menu)
120+
delete_all_action = QtGui.QAction("Delete All", menu)
121121
delete_all_action.triggered.connect(self._deleteAllActionSlot)
122122
menu.addAction(delete_all_action)
123-
menu.exec_(event.globalPos());
123+
menu.exec(event.globalPos())
124124

125125
def _deleteAllActionSlot(self):
126126
"""

gns3/controller.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,13 @@ def _versionGetSlot(self, result, error=False, **kwargs):
153153
self.connection_failed_signal.emit()
154154
if self._display_error:
155155
self._error_dialog = QtWidgets.QMessageBox(self.parent())
156-
self._error_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
156+
self._error_dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
157157
self._error_dialog.setWindowTitle("Connection to server")
158158
if result and "message" in result:
159159
self._error_dialog.setText("Error when connecting to the GNS3 server:\n{}".format(result["message"]))
160160
else:
161161
self._error_dialog.setText("Cannot connect to the GNS3 server")
162-
self._error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
162+
self._error_dialog.setIcon(QtWidgets.QMessageBox.Icon.Critical)
163163
self._error_dialog.show()
164164
# Try to connect again in 5 seconds
165165
QtCore.QTimer.singleShot(5000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))

gns3/crash_report.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class CrashReport:
5050
Report crash to a third party service
5151
"""
5252

53-
DSN = "https://62d45083e8fee6a3f5c28d4710ef2cb6@o19455.ingest.us.sentry.io/38506"
53+
DSN = "https://f09d966530e45cc56fbe33adc6baeeb8@o19455.ingest.us.sentry.io/38506"
5454
_instance = None
5555

5656
def __init__(self):

gns3/dialogs/appliance_wizard.py

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def __init__(self, parent, path):
8181
images_directories.append(emulator_images_dir)
8282

8383
images_directories.append(os.path.dirname(self._path))
84-
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
84+
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DownloadLocation)
8585
if download_directory != "" and download_directory != os.path.dirname(self._path):
8686
images_directories.append(download_directory)
8787

@@ -96,8 +96,8 @@ def __init__(self, parent, path):
9696
# add a custom button to show appliance information
9797
if self._appliance["registry_version"] < 8:
9898
# FIXME: show appliance info for v8
99-
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Appliance info")
100-
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
99+
self.setButtonText(QtWidgets.QWizard.WizardButton.CustomButton1, "&Appliance info")
100+
self.setOption(QtWidgets.QWizard.WizardOption.HaveCustomButton1, True)
101101
self.customButtonClicked.connect(self._showApplianceInfoSlot)
102102

103103
# customize the server selection
@@ -141,7 +141,7 @@ def initializePage(self, page_id):
141141
symbol = ":/symbols/computer.svg"
142142
else:
143143
symbol = ":/symbols/{}.svg".format(self._appliance["category"])
144-
self.page(page_id).setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(symbol))
144+
self.page(page_id).setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(symbol))
145145

146146
if self.page(page_id) == self.uiServerWizardPage:
147147

@@ -327,10 +327,10 @@ def _showApplianceInfoSlot(self):
327327
msgbox = QtWidgets.QMessageBox(self)
328328
msgbox.setWindowTitle("Appliance information")
329329
msgbox.setStyleSheet("QLabel{min-width: 600px;}") # TODO: resize details box QTextEdit{min-height: 500px;}
330-
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
330+
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
331331
msgbox.setText(text_info)
332332
msgbox.setDetailedText(self._appliance["description"])
333-
msgbox.exec_()
333+
msgbox.exec()
334334

335335
@qslot
336336
def _refreshVersions(self, *args):
@@ -377,9 +377,9 @@ def _versionRefreshedSlot(self, *args):
377377
image_widget.setToolTip(2, image["path"])
378378

379379
# Associated data stored are col 0: version, col 1: image
380-
image_widget.setData(0, QtCore.Qt.UserRole, version)
381-
image_widget.setData(1, QtCore.Qt.UserRole, image)
382-
image_widget.setData(2, QtCore.Qt.UserRole, self._appliance)
380+
image_widget.setData(0, QtCore.Qt.ItemDataRole.UserRole, version)
381+
image_widget.setData(1, QtCore.Qt.ItemDataRole.UserRole, image)
382+
image_widget.setData(2, QtCore.Qt.ItemDataRole.UserRole, self._appliance)
383383
top.addChild(image_widget)
384384

385385
font = top.font(0)
@@ -393,10 +393,10 @@ def _versionRefreshedSlot(self, *args):
393393
expand = False
394394
top.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
395395

396-
top.setData(1, QtCore.Qt.DisplayRole, human_filesize(size))
397-
top.setData(2, QtCore.Qt.DisplayRole, status)
398-
top.setData(0, QtCore.Qt.UserRole, version)
399-
top.setData(2, QtCore.Qt.UserRole, self._appliance)
396+
top.setData(1, QtCore.Qt.ItemDataRole.DisplayRole, human_filesize(size))
397+
top.setData(2, QtCore.Qt.ItemDataRole.DisplayRole, status)
398+
top.setData(0, QtCore.Qt.ItemDataRole.UserRole, version)
399+
top.setData(2, QtCore.Qt.ItemDataRole.UserRole, self._appliance)
400400
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
401401
if expand:
402402
top.setExpanded(True)
@@ -459,7 +459,7 @@ def _applianceVersionCurrentItemChangedSlot(self, current, previous):
459459
if current is None or sip.isdeleted(current):
460460
return
461461

462-
image = current.data(1, QtCore.Qt.UserRole)
462+
image = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
463463
if image is not None:
464464
if "direct_download_url" in image or "download_url" in image:
465465
self.uiDownloadPushButton.show()
@@ -480,7 +480,7 @@ def _downloadPushButtonClickedSlot(self, *args):
480480
if current is None or sip.isdeleted(current):
481481
return
482482

483-
data = current.data(1, QtCore.Qt.UserRole)
483+
data = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
484484
if data is not None:
485485
if "direct_download_url" in data:
486486
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["direct_download_url"]))
@@ -500,16 +500,16 @@ def _createVersionPushButtonClickedSlot(self, *args):
500500
if current is None:
501501
QtWidgets.QMessageBox.critical(self.parent(), "Base version", "Please select a base version")
502502
return
503-
base_version = current.data(0, QtCore.Qt.UserRole)
503+
base_version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
504504

505-
new_version_name, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Create a new version for this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.Normal, base_version.get("name"))
505+
new_version_name, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Create a new version for this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.EchoMode.Normal, base_version.get("name"))
506506
if ok:
507507
new_version = {"name": new_version_name}
508508
new_version["images"] = {}
509509

510510
for disk_type in base_version["images"]:
511511
base_filename = base_version["images"][disk_type]["filename"]
512-
filename, ok = QtWidgets.QInputDialog.getText(self, "Image", "Disk image filename for {}".format(disk_type), QtWidgets.QLineEdit.Normal, base_filename)
512+
filename, ok = QtWidgets.QInputDialog.getText(self, "Image", "Disk image filename for {}".format(disk_type), QtWidgets.QLineEdit.EchoMode.Normal, base_filename)
513513
if not ok:
514514
filename = base_filename
515515
new_version["images"][disk_type] = {"filename": filename, "version": new_version_name}
@@ -534,7 +534,7 @@ def _importPushButtonClickedSlot(self, *args):
534534
current = self.uiApplianceVersionTreeWidget.currentItem()
535535
if not current:
536536
return
537-
disk = current.data(1, QtCore.Qt.UserRole)
537+
disk = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
538538

539539
path, _ = QtWidgets.QFileDialog.getOpenFileName()
540540
if len(path) == 0:
@@ -554,9 +554,9 @@ def _importPushButtonClickedSlot(self, *args):
554554
f"actual:\t{image.filesize} bytes\n"
555555
f"expected:\t{disk['filesize']} bytes\n\n"
556556
"Do you want to accept it at your own risks?",
557-
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
557+
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
558558
)
559-
if reply == QtWidgets.QMessageBox.No:
559+
if reply == QtWidgets.QMessageBox.StandardButton.No:
560560
return
561561
except OSError as e:
562562
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
@@ -589,7 +589,7 @@ def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
589589
qemu_platform = self._appliance.template_properties()["platform"]
590590
else:
591591
qemu_platform = self._appliance.template_properties()["arch"]
592-
i = self.uiQemuListComboBox.findData(qemu_platform, flags=QtCore.Qt.MatchEndsWith)
592+
i = self.uiQemuListComboBox.findData(qemu_platform, flags=QtCore.Qt.MatchFlag.MatchEndsWith)
593593
if i != -1:
594594
self.uiQemuListComboBox.setCurrentIndex(i)
595595

@@ -615,7 +615,7 @@ def _install(self, version):
615615
template_manager = TemplateManager().instance()
616616
while len(appliance_configuration["name"]) == 0 or not template_manager.is_name_available(appliance_configuration["name"]):
617617
QtWidgets.QMessageBox.warning(self.parent(), "Add template", "The name \"{}\" is already used by another template".format(appliance_configuration["name"]))
618-
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add template", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
618+
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add template", "New name:", QtWidgets.QLineEdit.EchoMode.Normal, appliance_configuration["name"])
619619
if not ok:
620620
return False
621621
appliance_configuration["name"] = appliance_configuration["name"].strip()
@@ -635,15 +635,15 @@ def _install(self, version):
635635
#worker = WaitForLambdaWorker(lambda: self._create_template(appliance_configuration, self._compute_id), allowed_exceptions=[ConfigException, OSError])
636636
#progress_dialog = ProgressDialog(worker, "Add template", "Installing a new template...", None, busy=True, parent=self)
637637
#progress_dialog.show()
638-
#if progress_dialog.exec_():
638+
#if progress_dialog.exec():
639639
# QtWidgets.QMessageBox.information(self.parent(), "Add template", "{} template has been installed!".format(appliance_configuration["name"]))
640640
# return True
641641
#return False
642642

643643
# worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
644644
# progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
645645
# progress_dialog.show()
646-
# if progress_dialog.exec_():
646+
# if progress_dialog.exec():
647647
# QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
648648
# return True
649649

@@ -714,18 +714,18 @@ def validateCurrentPage(self):
714714
current = self.uiApplianceVersionTreeWidget.currentItem()
715715
if current is None or sip.isdeleted(current):
716716
return False
717-
version = current.data(0, QtCore.Qt.UserRole)
717+
version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
718718
if version is None:
719719
return False
720-
appliance = current.data(2, QtCore.Qt.UserRole)
720+
appliance = current.data(2, QtCore.Qt.ItemDataRole.UserRole)
721721
try:
722722
self._appliance.search_images_for_version(version["name"])
723723
except ApplianceError as e:
724724
QtWidgets.QMessageBox.critical(self, "Appliance", "Cannot install {} version {}: {}".format(appliance["name"], version["name"], e))
725725
return False
726726
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {} version {}?".format(appliance["name"], version["name"]),
727-
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
728-
if reply == QtWidgets.QMessageBox.No:
727+
QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
728+
if reply == QtWidgets.QMessageBox.StandardButton.No:
729729
return False
730730

731731
self._uploadImages(appliance["name"], version["name"])
@@ -740,7 +740,7 @@ def validateCurrentPage(self):
740740
return False
741741
current = self.uiApplianceVersionTreeWidget.currentItem()
742742
if current:
743-
version = current.data(0, QtCore.Qt.UserRole)
743+
version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
744744
return self._install(version["name"])
745745
else:
746746
return self._install(None)
@@ -759,8 +759,8 @@ def validateCurrentPage(self):
759759
if ComputeManager.instance().localPlatform():
760760
if (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
761761
if "qemu" in self._appliance:
762-
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and macOS is not supported by the GNS3 team. Do you want to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
763-
if reply == QtWidgets.QMessageBox.No:
762+
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and macOS is not supported by the GNS3 team. Do you want to continue?", QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
763+
if reply == QtWidgets.QMessageBox.StandardButton.No:
764764
return False
765765
self._compute_id = "local"
766766

@@ -821,8 +821,8 @@ def _allowCustomFilesChangedSlot(self, checked):
821821
reply = QtWidgets.QMessageBox.question(self, "Custom files",
822822
"This option allows files with different MD5 checksums. This feature is only for advanced users and can lead "
823823
"to unexpected problems. Do you want to proceed?",
824-
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
824+
QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
825825

826-
if reply == QtWidgets.QMessageBox.No:
826+
if reply == QtWidgets.QMessageBox.StandardButton.No:
827827
self.allowCustomFiles.setChecked(False)
828828
return False

0 commit comments

Comments
 (0)