From c676a5bf82e61509dea6bd2e96a88b493ca374d4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 16 Dec 2025 16:46:31 +0800 Subject: [PATCH 1/9] Development on v2.2.56.dev1 --- gns3server/version.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gns3server/version.py b/gns3server/version.py index c99f6f848..20d2c4179 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,8 +23,9 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.55" -__version_info__ = (2, 2, 55, 0) +__version__ = "2.2.56.dev1" +__version_info__ = (2, 2, 56, 99) + if "dev" in __version__: try: import os From 8ce690ce782fb020ca694601656ce0f1b957d4e4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 17 Jan 2026 17:54:27 +0800 Subject: [PATCH 2/9] Upgrade pywin32 to v311 --- win-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/win-requirements.txt b/win-requirements.txt index 5d5d84516..09cf5b80f 100644 --- a/win-requirements.txt +++ b/win-requirements.txt @@ -1,4 +1,4 @@ -rrequirements.txt -pywin32==306 +pywin32==311 wmi==1.5.1 From 58d1e8f0c50f56b65013b500960dd9e9114d0d73 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 17 Jan 2026 18:01:42 +0800 Subject: [PATCH 3/9] Set default location of udhcpc in "/etc/network/udhcpc". Fixes #2582 --- .../compute/docker/resources/etc/udhcpc/default.script | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gns3server/compute/docker/resources/etc/udhcpc/default.script b/gns3server/compute/docker/resources/etc/udhcpc/default.script index 404fb190a..873642c47 100755 --- a/gns3server/compute/docker/resources/etc/udhcpc/default.script +++ b/gns3server/compute/docker/resources/etc/udhcpc/default.script @@ -3,7 +3,12 @@ # script for udhcpc # Copyright (c) 2008 Natanael Copa -UDHCPC="/gns3/etc/udhcpc" +if [ -d /etc/network ]; then + UDHCPC="/etc/network/udhcpc" +else + UDHCPC="/gns3/etc/udhcpc" +fi + UDHCPC_CONF="$UDHCPC/udhcpc.conf" RESOLV_CONF="/etc/resolv.conf" From 7b3fd4450e1fe881652cdae92845b27ac8f385a4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 17 Jan 2026 18:37:41 +0800 Subject: [PATCH 4/9] Sync appliances --- .../asterfusion-vAsterNOS-VPP.gns3a | 31 +++ gns3server/appliances/infix.gns3a | 14 ++ gns3server/appliances/kali-linux.gns3a | 229 +----------------- .../appliances/mikrotik-winbox-2026.gns3a | 22 ++ gns3server/appliances/vyos.gns3a | 13 + 5 files changed, 93 insertions(+), 216 deletions(-) create mode 100644 gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a create mode 100644 gns3server/appliances/mikrotik-winbox-2026.gns3a diff --git a/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a b/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a new file mode 100644 index 000000000..3afa29fa5 --- /dev/null +++ b/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a @@ -0,0 +1,31 @@ +{ + "name": "Asterfusion AsterNOS-VPP", + "category": "router", + "description": "AsterNOS-VPP leverages SONiC’s robust routing features and management capability with VPP’s high-performance data forwarding. It supports L3 routing, NAT, PPPoE, and VPN services. Minimum requirements: 4 vCPUs and 4GB RAM.", + "vendor_name": "Asterfusion", + "vendor_url": "https://cloudswit.ch/product/sonic-enterprise-distribution/#vAsterNOS", + "documentation_url": "https://docs.asternos.com/routing", + "product_name": "AsterNOS-VPP", + "registry_version": 6, + "status": "stable", + "maintainer": "Asterfusion Product Team", + "images": [ + { + "filename": "AsterNOS-VPP_V6.1-R0101P02_x86.img.gz", + "version": "V6.1", + "md5sum": "55834c3e8849ab226144d79ef76acf81", + "filesize": 1318201015, + "download_url": "https://asternos.dev/api/file/b2c9fd83-a9d3-46f3-9676-6b49dd481d6c", + "comments": "Registration is required to download the image." + } + ], + "qemu": { + "adapter_type": "virtio-net-pci", + "adapters": 4, + "ram": 4096, + "cpus": 4, + "arch": "x86_64", + "console_type": "telnet", + "kvm": "require" + } +} diff --git a/gns3server/appliances/infix.gns3a b/gns3server/appliances/infix.gns3a index 797266497..d9ab0321f 100644 --- a/gns3server/appliances/infix.gns3a +++ b/gns3server/appliances/infix.gns3a @@ -104,9 +104,23 @@ "md5sum": "781d0fce9fbf789f6ea6ff10dc104afb", "version": "25.10.0", "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.10.0/infix-x86_64-disk-25.10.0.qcow2" + }, + { + "filename": "infix-x86_64-disk-25.11.0.qcow2", + "filesize": 288930816, + "md5sum": "e2eb1a4fff9d56815f37c2e82a6279fb", + "version": "25.11.0", + "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.11.0/infix-x86_64-disk-25.11.0.qcow2" } ], "versions": [ + { + "name": "25.11.0", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-disk-25.11.0.qcow2" + } + }, { "name": "25.10.0", "images": { diff --git a/gns3server/appliances/kali-linux.gns3a b/gns3server/appliances/kali-linux.gns3a index 5452ec058..60f742842 100644 --- a/gns3server/appliances/kali-linux.gns3a +++ b/gns3server/appliances/kali-linux.gns3a @@ -11,237 +11,34 @@ "status": "stable", "maintainer": "GNS3 Team", "maintainer_email": "developers@gns3.net", - "usage": "Default password is toor\nEnable persistence by selecting boot option 'Live USB Persistence'", + "usage": "Default username is kali and the default password is kali", "port_name_format": "eth{0}", "qemu": { "adapter_type": "e1000", "adapters": 8, - "ram": 1024, + "ram": 2048, + "cpus": 2, "hda_disk_interface": "ide", "arch": "x86_64", "console_type": "vnc", - "boot_priority": "d", "kvm": "require" }, "images": [ - { - "filename": "kali-linux-2021.1-live-amd64.iso", - "version": "2021.1", - "md5sum": "3a3716fef866e5c29a1c1ccfc94264b5", - "filesize": 3591385088, - "download_url": "http://cdimage.kali.org/kali-2021.1/", - "direct_download_url": "http://cdimage.kali.org/kali-2021.1/kali-linux-2021.1-live-amd64.iso" - }, - { - "filename": "kali-linux-2019.3-amd64.iso", - "version": "2019.3", - "md5sum": "9c6fb00558f78ed06992d89f745ef975", - "filesize": 3037736960, - "download_url": "http://old.kali.org/kali-images/kali-2019.3", - "direct_download_url": "http://old.kali.org/kali-images/kali-2019.3/kali-linux-2019.3-amd64.iso" - }, - { - "filename": "kali-linux-2019.2-amd64.iso", - "version": "2019.2", - "md5sum": "0f89b6225d7ea9c18682f7cc541c1179", - "filesize": 3353227264, - "download_url": "http://old.kali.org/kali-images/kali-2019.2", - "direct_download_url": "http://old.kali.org/kali-images/kali-2019.2/kali-linux-2019.2-amd64.iso" - }, - { - "filename": "kali-linux-mate-2019.2-amd64.iso", - "version": "2019.2 (MATE)", - "md5sum": "fec8dd7009f932c51a74323df965a709", - "filesize": 3313217536, - "download_url": "http://old.kali.org/kali-images/kali-2019.2", - "direct_download_url": "http://old.kali.org/kali-images/kali-2019.2/kali-linux-mate-2019.2-amd64.iso" - }, - { - "filename": "kali-linux-2019.1a-amd64.iso", - "version": "2019.1a", - "md5sum": "58c6111ed0be1919ea87267e7e65ab0f", - "filesize": 3483873280, - "download_url": "http://old.kali.org/kali-images/kali-2019.1a", - "direct_download_url": "http://old.kali.org/kali-images/kali-2019.1a/kali-linux-2019.1a-amd64.iso" - }, - { - "filename": "kali-linux-2018.4-amd64.iso", - "version": "2018.4", - "md5sum": "1b2d598bb8d2003e6207c119c0ba42fe", - "filesize": 3139436544, - "download_url": "http://old.kali.org/kali-images/kali-2018.4", - "direct_download_url": "http://old.kali.org/kali-images/kali-2018.4/kali-linux-2018.4-amd64.iso" - }, - { - "filename": "kali-linux-2018.3a-amd64.iso", - "version": "2018.3a", - "md5sum": "2da675d016bd690c05e180e33aa98b94", - "filesize": 3192651776, - "download_url": "http://old.kali.org/kali-images/kali-2018.3a", - "direct_download_url": "http://old.kali.org/kali-images/kali-2018.3a/kali-linux-2018.3a-amd64.iso" - }, - { - "filename": "kali-linux-2018.1-amd64.iso", - "version": "2018.1", - "md5sum": "a3feb90df5b71b3c7f4a02bdddf221d7", - "filesize": 3028500480, - "download_url": "http://old.kali.org/kali-images/kali-2018.1", - "direct_download_url": "http://old.kali.org/kali-images/kali-2018.1/kali-linux-2018.1-amd64.iso" - }, - { - "filename": "kali-linux-2017.3-amd64.iso", - "version": "2017.3", - "md5sum": "b465580c897e94675ac1daf031fa66b9", - "filesize": 2886402048, - "download_url": "http://old.kali.org/kali-images/kali-2017.3/", - "direct_download_url": "http://old.kali.org/kali-images/kali-2017.3/kali-linux-2017.3-amd64.iso" - }, - { - "filename": "kali-linux-2017.2-amd64.iso", - "version": "2017.2", - "md5sum": "541654f8f818450dc0db866a0a0f6eec", - "filesize": 3020619776, - "download_url": "http://old.kali.org/kali-images/kali-2017.2/", - "direct_download_url": "http://old.kali.org/kali-images/kali-2017.2/kali-linux-2017.2-amd64.iso" - }, - { - "filename": "kali-linux-2017.1-amd64.iso", - "version": "2017.1", - "md5sum": "c8e742283929d7a12dbe7c58e398ff08", - "filesize": 2794307584, - "download_url": "http://old.kali.org/kali-images/kali-2017.1/", - "direct_download_url": "http://old.kali.org/kali-images/kali-2017.1/kali-linux-2017.1-amd64.iso" - }, - { - "filename": "kali-linux-2016.2-amd64.iso", - "version": "2016.2", - "md5sum": "3d163746bc5148e61ad689d94bc263f9", - "filesize": 3076767744, - "download_url": "http://old.kali.org/kali-images/kali-2016.2/", - "direct_download_url": "http://old.kali.org/kali-images/kali-2016.2/kali-linux-2016.2-amd64.iso" - }, - { - "filename": "kali-linux-2016.1-amd64.iso", - "version": "2016.1", - "md5sum": "2e1230dc14036935b3279dfe3e49ad39", - "filesize": 2945482752, - "download_url": "http://old.kali.org/kali-images/kali-2016.1/", - "direct_download_url": "http://old.kali.org/kali-images/kali-2016.1/kali-linux-2016.1-amd64.iso" - }, - { - "filename": "kali-linux-2.0-amd64.iso", - "version": "2.0", - "md5sum": "ef192433017c5d99a156eaef51fd389d", - "filesize": 3320512512, - "download_url": "https://www.offensive-security.com/kali-linux-vmware-arm-image-download/", - "direct_download_url": "http://images.kali.org/Kali-Linux-2.0.0-vm-amd64.7z" - }, - { - "filename": "kali-linux-persistence-1gb.qcow2", - "version": "1.0", - "md5sum": "14e9c92f3ba5a0bd1128c1ea26a129ea", - "filesize": 34734080, - "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", - "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/kali-linux-persistence-1gb.qcow2/download" + { + "filename": "kali-linux-2025.4-qemu-amd64.qcow2", + "version": "2025.4", + "md5sum": "a4be43351b5deb5b2823cb38a73b2095", + "filesize": 15719268352, + "download_url": "https://cdimage.kali.org/kali-2025.4/", + "direct_download_url": "https://cdimage.kali.org/kali-2025.4/kali-linux-2025.4-qemu-amd64.7z", + "compression": "7z" } ], "versions": [ { - "name": "2021.1", + "name": "2025.4", "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2021.1-live-amd64.iso" - } - }, - { - "name": "2019.3", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2019.3-amd64.iso" - } - }, - { - "name": "2019.2", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2019.2-amd64.iso" - } - }, - { - "name": "2019.2 (MATE)", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-mate-2019.2-amd64.iso" - } - }, - { - "name": "2019.1a", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2019.1a-amd64.iso" - } - }, - { - "name": "2018.4", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2018.4-amd64.iso" - } - }, - { - "name": "2018.3a", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2018.3a-amd64.iso" - } - }, - { - "name": "2018.1", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2018.1-amd64.iso" - } - }, - { - "name": "2017.3", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2017.3-amd64.iso" - } - }, - { - "name": "2017.2", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2017.2-amd64.iso" - } - }, - { - "name": "2017.1", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2017.1-amd64.iso" - } - }, - { - "name": "2016.2", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2016.2-amd64.iso" - } - }, - { - "name": "2016.1", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2016.1-amd64.iso" - } - }, - { - "name": "2.0", - "images": { - "hda_disk_image": "kali-linux-persistence-1gb.qcow2", - "cdrom_image": "kali-linux-2.0-amd64.iso" + "hda_disk_image": "kali-linux-2025.4-qemu-amd64.qcow2" } } ] diff --git a/gns3server/appliances/mikrotik-winbox-2026.gns3a b/gns3server/appliances/mikrotik-winbox-2026.gns3a new file mode 100644 index 000000000..7d0bc91ee --- /dev/null +++ b/gns3server/appliances/mikrotik-winbox-2026.gns3a @@ -0,0 +1,22 @@ +{ + "appliance_id": "1174e46d-4a29-4554-a1e8-71ed90682295", + "name": "MikroTik WinBox 2026", + "category": "guest", + "description": "Mikrotik's WinBox router management software for GNS3 2026", + "vendor_name": "MikroTik", + "vendor_url": "https://mikrotik.com", + "documentation_url": "https://mikrotik.com/winbox", + "product_name": "MikroTik WinBox", + "product_url": "https://mikrotik.com/winbox", + "registry_version": 4, + "status": "stable", + "availability": "free", + "maintainer": "h3ck13r", + "maintainer_email": "pisdushko1886@gmail.com", + "usage": "Connect WinBox to a MikroTik CHR port and start them both. Open WinBox console and wait until MikroTik appear in neighbors tab, provide credentials and connect.", + "docker": { + "adapters": 1, + "image": "snatao808/winbox-mikrotik-2026", + "console_type": "vnc" + } +} diff --git a/gns3server/appliances/vyos.gns3a b/gns3server/appliances/vyos.gns3a index 3255a80e0..10bf9ae4c 100644 --- a/gns3server/appliances/vyos.gns3a +++ b/gns3server/appliances/vyos.gns3a @@ -30,6 +30,13 @@ "on_close": "shutdown_signal" }, "images": [ + { + "filename": "vyos-1.4.4-kvm-amd64.qcow2", + "version": "1.4.4", + "md5sum": "942dae45d63924b58b65d069890765d4", + "filesize": 518782976, + "download_url": "https://support.vyos.io/" + }, { "filename": "vyos-1.4.3-kvm-amd64.qcow2", "version": "1.4.3", @@ -130,6 +137,12 @@ } ], "versions": [ + { + "name": "1.4.4", + "images": { + "hda_disk_image": "vyos-1.4.4-kvm-amd64.qcow2" + } + }, { "name": "1.4.3", "images": { From 27fbe37375bc879b9a5886a6f9d370cebbaa6ed8 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 21 Jan 2026 22:15:12 +0800 Subject: [PATCH 5/9] Release v2.2.56 --- CHANGELOG | 5 +++++ gns3server/crash_report.py | 2 +- gns3server/version.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7e212c27d..15c9573b3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ # Change Log +## 2.2.56 21/01/2026 + +* Set default location of udhcpc in "/etc/network/udhcpc". Fixes #2582 +* Upgrade pywin32 to v311 + ## 2.2.55 19/11/2025 * Docker API version requirements and process to handle older daemons diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 296f92fe9..93f503113 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -57,7 +57,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "https://9721facaf4cb1e0a93e126300ea56901@o19455.ingest.us.sentry.io/38482" + DSN = "https://9b9fce29bf59d9a99c230240e1070600@o19455.ingest.us.sentry.io/38482" _instance = None def __init__(self): diff --git a/gns3server/version.py b/gns3server/version.py index 20d2c4179..a5a0c9036 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,8 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.56.dev1" -__version_info__ = (2, 2, 56, 99) +__version__ = "2.2.56" +__version_info__ = (2, 2, 56, 0) if "dev" in __version__: try: From ef2cf277c166c9c185721b97ac3d45e5f4cdd1b9 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 24 Jan 2026 16:53:43 +0800 Subject: [PATCH 6/9] Development on 2.2.57.dev1 --- gns3server/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/version.py b/gns3server/version.py index a5a0c9036..26e1d9395 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,8 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.56" -__version_info__ = (2, 2, 56, 0) +__version__ = "2.2.57.dev1" +__version_info__ = (2, 2, 56, 99) if "dev" in __version__: try: From 4335fd7ce0cfd0362d0d38dfc8740c1b27fb954f Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 24 Jan 2026 18:51:44 +0800 Subject: [PATCH 7/9] Drop Python 3.8 support --- gns3server/run.py | 6 +++--- gns3server/utils/asyncio/__init__.py | 19 +++++-------------- gns3server/utils/asyncio/pool.py | 5 +---- setup.py | 5 ++--- 4 files changed, 11 insertions(+), 24 deletions(-) diff --git a/gns3server/run.py b/gns3server/run.py index 32467d45a..c58bd6e92 100644 --- a/gns3server/run.py +++ b/gns3server/run.py @@ -235,9 +235,9 @@ def run(): return log.info("HTTP authentication is enabled with username '{}'".format(user)) - # we only support Python 3 version >= 3.8 - if sys.version_info < (3, 8, 0): - raise SystemExit("Python 3.8 or higher is required") + # we only support Python 3 version >= 3.9 + if sys.version_info < (3, 9, 0): + raise SystemExit("Python 3.9 or higher is required") user_log.info("Running with Python {major}.{minor}.{micro} and has PID {pid}".format(major=sys.version_info[0], minor=sys.version_info[1], micro=sys.version_info[2], pid=os.getpid())) diff --git a/gns3server/utils/asyncio/__init__.py b/gns3server/utils/asyncio/__init__.py index efc32d066..7e464373c 100644 --- a/gns3server/utils/asyncio/__init__.py +++ b/gns3server/utils/asyncio/__init__.py @@ -91,25 +91,16 @@ async def wait_for_process_termination(process, timeout=10): In theory this can be implemented by just: await asyncio.wait_for(self._iou_process.wait(), timeout=100) - But it's broken before Python 3.4: - http://bugs.python.org/issue23140 - :param process: An asyncio subprocess :param timeout: Timeout in seconds """ - if sys.version_info >= (3, 5): - try: - await asyncio.wait_for(process.wait(), timeout=timeout) - except ProcessLookupError: + while timeout > 0: + if process.returncode is not None: return - else: - while timeout > 0: - if process.returncode is not None: - return - await asyncio.sleep(0.1) - timeout -= 0.1 - raise asyncio.TimeoutError() + await asyncio.sleep(0.1) + timeout -= 0.1 + raise asyncio.TimeoutError() async def _check_process(process, termination_callback): diff --git a/gns3server/utils/asyncio/pool.py b/gns3server/utils/asyncio/pool.py index 22750eadf..444863295 100644 --- a/gns3server/utils/asyncio/pool.py +++ b/gns3server/utils/asyncio/pool.py @@ -40,10 +40,7 @@ class Pool(): while len(self._tasks) > 0 or len(pending) > 0: while len(self._tasks) > 0 and len(pending) < self._concurrency: task, args, kwargs = self._tasks.pop(0) - if sys.version_info >= (3, 7): - t = asyncio.create_task(task(*args, **kwargs)) - else: - t = asyncio.get_event_loop().create_task(task(*args, **kwargs)) + t = asyncio.create_task(task(*args, **kwargs)) pending.add(t) (done, pending) = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) for task in done: diff --git a/setup.py b/setup.py index 5685ce694..2cc398f69 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand # we only support Python 3 version >= 3.8 -if len(sys.argv) >= 2 and sys.argv[1] == "install" and sys.version_info < (3, 8): - raise SystemExit("Python 3.8 or higher is required") +if len(sys.argv) >= 2 and sys.argv[1] == "install" and sys.version_info < (3, 9): + raise SystemExit("Python 3.9 or higher is required") class PyTest(TestCommand): @@ -78,7 +78,6 @@ setup( "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 193f470eaf33d9f5621c5689ababcaf0bf56d4d4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 24 Jan 2026 19:12:58 +0800 Subject: [PATCH 8/9] Upgrade dependencies --- .github/workflows/testing.yml | 6 +----- dev-requirements.txt | 8 ++++---- requirements.txt | 17 ++++++++--------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 744115355..cb84817ea 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -18,11 +18,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest"] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] - #include: - # only test with Python 3.10 on Windows - # - os: windows-latest - # python-version: "3.10" + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/dev-requirements.txt b/dev-requirements.txt index 4348b0d94..ece129f97 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,6 @@ -rrequirements.txt -pytest==8.3.3 -flake8==7.1.0 -pytest-timeout==2.3.1 -pytest-aiohttp==1.0.5 +pytest==8.4.2 # version 8.4.2 is the last one supporting Python 3.9 +flake8==7.3.0 +pytest-timeout==2.4.0 +pytest-aiohttp==1.1.0 diff --git a/requirements.txt b/requirements.txt index 5f0b26144..9c322ec53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,12 @@ -jsonschema>=4.23,<4.24 -aiohttp>=3.10.11,<3.11 # version 3.10.11 is the last compatible version with Python 3.8 -aiohttp-cors>=0.7.0,<0.8 -aiofiles>=24.1.0,<25.0 +jsonschema>=4.25.1,<4.26 # version 4.25.1 is the last to support Python 3.9 +aiohttp>=3.13.3,<3.14 +aiohttp-cors>=0.8.1,<0.9 +aiofiles>=25.1.0,<26.0 Jinja2>=3.1.6,<3.2 -sentry-sdk>=2.44.0,<2.45 # optional dependency -psutil>=7.1.3 -async-timeout>=5.0.1,<5.1 +sentry-sdk>=2.50.0,<3 # optional dependency +psutil>=7.2.1 +async-timeout>=5.0.1,<5.1 # this library has effectively been upstreamed into Python 3.11+ distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 platformdirs>=2.4.0,<3 # platformdirs >=3 conflicts when building Debian packages -importlib-resources>=1.3; python_version < '3.9' -truststore>=0.10.0; python_version >= '3.10' +truststore>=0.10.4; python_version >= '3.10' From 7b20314edb37aa4331b9452652e70624ef3b986d Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 24 Jan 2026 19:17:55 +0800 Subject: [PATCH 9/9] Delete random_query.py --- scripts/random_query.py | 238 ---------------------------------------- 1 file changed, 238 deletions(-) delete mode 100644 scripts/random_query.py diff --git a/scripts/random_query.py b/scripts/random_query.py deleted file mode 100644 index f5e7de2a9..000000000 --- a/scripts/random_query.py +++ /dev/null @@ -1,238 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (C) 2016 GNS3 Technologies Inc. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -This script connect to the local GNS3 server and will create a random topology -""" - -import sys -import json -import math -import aiohttp -import aiohttp.web -import asyncio -import random - -import coloredlogs -import logging - -coloredlogs.install(fmt=" %(asctime)s %(levelname)s %(message)s") -log = logging.getLogger(__name__) - -PROJECT_ID = "9e26e37d-4962-4921-8c0e-136d3b04ba9c" -HOST = "192.168.84.151:3080" - -# Use for node names uniqueness -node_i = 1 - - -def die(*args): - log.error(*args) - sys.exit(1) - - -class HTTPError(Exception): - - def __init__(self, method, path, response): - self._method = method - self._path = path - self._response = response - - @property - def response(self): - return self._response - - @property - def path(self): - return self._path - - @property - def method(self): - return self._method - - -class HTTPConflict(HTTPError): - pass - - -class HTTPNotFound(HTTPError): - pass - - -async def query(method, path, body=None, **kwargs): - global session - - if body: - kwargs["data"] = json.dumps(body) - - async with session.request(method, "http://" + HOST + "/v2" + path, **kwargs) as response: - if response.status == 409: - raise HTTPConflict(method, path, response) - elif response.status == 404: - raise HTTPNotFound(method, path, response) - elif response.status >= 300: - raise HTTPError(method, path, response) - log.info("%s %s %d", method, path, response.status) - if response.headers["content-type"] == "application/json": - return await response.json() - else: - return "{}" - - -async def post(path, **kwargs): - return await query("POST", path, **kwargs) - - -async def get(path, **kwargs): - return await query("GET", path, **kwargs) - - -async def delete(path, **kwargs): - return await query("DELETE", path, **kwargs) - - -async def create_project(): - # Delete project if already exists - response = await get("/projects") - project_exists = False - for project in response: - if project["name"] == "random" and project["project_id"] != PROJECT_ID: - await delete("/projects/" + project["project_id"]) - elif project["project_id"] == PROJECT_ID: - project_exists = True - tasks = [] - for node in await get("/projects/" + PROJECT_ID + "/nodes"): - tasks.append(delete_node(project, node)) - await asyncio.gather(*tasks) - if project_exists: - response = await post("/projects/" + PROJECT_ID + "/open") - else: - response = await post("/projects", body={"name": "random", "project_id": PROJECT_ID, "auto_close": False}) - return response - - -async def create_node(project): - global node_i - - r = random.randint(0, 1) - - if r == 0: - node_type = "ethernet_switch" - symbol = ":/symbols/ethernet_switch.svg" - elif r == 1: - node_type = "vpcs" - symbol = ":/symbols/vpcs_guest.svg" - response = await post("/projects/{}/nodes".format(project["project_id"]), body={ - "node_type": node_type, - "compute_id": "local", - "symbol": symbol, - "name": "Node{}".format(node_i), - "x": (math.floor((node_i - 1) % 12.0) * 100) - 500, - "y": (math.ceil((node_i) / 12.0) * 100) - 300 - }) - node_i += 1 - return response - - -async def delete_node(project, node): - await delete("/projects/{}/nodes/{}".format(project["project_id"], node["node_id"])) - - -async def create_link(project, nodes): - """ - Create all possible link of a node - """ - node1 = random.choice(list(nodes.values())) - - for port in range(0, 8): - node2 = random.choice(list(nodes.values())) - - if node1 == node2: - continue - - data = {"nodes": - [ - { - "adapter_number": 0, - "node_id": node1["node_id"], - "port_number": port - }, - { - "adapter_number": 0, - "node_id": node2["node_id"], - "port_number": port - } - ] - } - try: - await post("/projects/{}/links".format(project["project_id"]), body=data) - except (HTTPConflict, HTTPNotFound): - pass - - -async def build_topology(): - global node_i - - nodes = {} - project = await create_project() - while True: - rand = random.randint(0, 1000) - if rand < 500: # chance to create a new node - if len(nodes.keys()) < 255: # Limit of VPCS: - node = await create_node(project) - nodes[node["node_id"]] = node - elif rand < 600: # start all nodes - await post("/projects/{}/nodes/start".format(project["project_id"])) - elif rand < 700: # stop all nodes - await post("/projects/{}/nodes/stop".format(project["project_id"])) - elif rand < 950: # create a link - if len(nodes.keys()) >= 2: - await create_link(project, nodes) - elif rand < 999: # chance to delete a node - continue - if len(nodes.keys()) > 0: - node = random.choice(list(nodes.values())) - await delete_node(project, node) - del nodes[node["node_id"]] - elif len(nodes.keys()) > 0: # % chance to delete all nodes - continue - node_i = 1 - tasks = [] - for node in nodes.values(): - tasks.append(delete_node(project, node)) - await asyncio.gather(*tasks) - nodes = {} - await asyncio.sleep(0.2) - -async def main(loop): - global session - async with aiohttp.ClientSession() as session: - try: - await build_topology() - except HTTPError as error: - try: - j = await error.response.json() - die("%s %s invalid status %d:\n%s", error.method, error.path, error.response.status, json.dumps(j, indent=4)) - except (ValueError, aiohttp.ServerDisconnectedError): - die("%s %s invalid status %d", error.method, error.path, error.response.status) - - -loop = asyncio.get_event_loop() -loop.run_until_complete(main(loop)) - -if session: - session.close()