diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 000000000..fec449ba2 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,40 @@ +name: Build and Push Docker Image + +on: + workflow_dispatch: + push: + tags: + - v2.* + - v3.* + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push to GitHub Container Registry + run: | + docker build -t ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]'):latest . + docker push ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]'):latest + + - name: Build and push to Docker Hub + run: | + docker build -t gns3/${{ github.event.repository.name }}:latest . + docker push gns3/${{ github.event.repository.name }}:latest diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 23acd4375..3476159a7 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -18,11 +18,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest"] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] - #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/CHANGELOG b/CHANGELOG index b3cbbd9c0..227773671 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,34 @@ # Change Log +## 3.0.6 28/01/2026 + +* Sync appliances +* Fix creating Qemu disk image. Fixes #2542 +* Disable checking for available disk space. Ref #2548 +* Set default location of udhcpc in "/etc/network/udhcpc". Fixes #2582 +* Support for Python 3.14 +* Fix non-ASCII characters in project names +* Increase DB engine pool size and max overflow +* Enable ip cef in IOU L2 and IOU L3 base configs + +## 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 +* Use docker_name property when creating a container +* Set name of container in Docker +* Support for Python 3.14 +* Enable ip cef in IOU L2 and IOU L3 base configs +* Only build and push Docker image when releasing a new version +* Fix pushing Docker image to Docker hub +* Update security issues in README.md. Fixes #2535 +* Add missing 'nat' template type in schema. Fixes #2529 +* Resolve deprecation warnings of regex library + ## 3.0.5 14/05/2025 * Bundle web-ui v3.0.5 diff --git a/dev-requirements.txt b/dev-requirements.txt index 04ca562bd..df3f6b494 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ -pytest==8.3.4 -flake8==7.1.1 -pytest-timeout==2.3.1 -pytest-asyncio==0.25.2 -requests==2.32.3 +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-asyncio==0.26.0 # upgrading leads to stuck / failed tests +requests==2.32.5 httpx==0.28.1 -httpx_ws==0.7.1 +httpx_ws==0.7.1 # upgrading leads to failures in tests \ No newline at end of file diff --git a/gns3server/api/routes/controller/images.py b/gns3server/api/routes/controller/images.py index 736c44c3b..9a43152ed 100644 --- a/gns3server/api/routes/controller/images.py +++ b/gns3server/api/routes/controller/images.py @@ -96,7 +96,13 @@ async def create_qemu_image( await Qemu.instance().create_disk_image(disk_image_path, options) image_info = await read_image_info(disk_image_path, "qemu") - return await images_repo.add_image(**image_info) + + image = await images_repo.get_image(disk_image_path) + if image: + # the image has already been added to the database + return image + else: + return await images_repo.add_image(**image_info) @router.get( "", diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index caa2d3879..37c7e35b4 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -330,6 +330,9 @@ async def export_project( Required privilege: Project.Audit """ + if project.is_running(): + raise ControllerError("Project must be stopped in order to export it") + compression_query = compression.lower() if compression_query == "zip": compression = zipfile.ZIP_DEFLATED @@ -380,7 +383,13 @@ async def export_project( except (ValueError, OSError, RuntimeError) as e: raise ConnectionError(f"Cannot export project: {e}") - headers = {"CONTENT-DISPOSITION": f'attachment; filename="{project.name}.gns3project"'} + fallback = project.name.encode("ascii", "ignore").decode() or "project" + encoded = urllib.parse.quote(project.name, safe="") + headers = { + "Content-Disposition": ( + f'attachment; filename="{fallback}.gns3project"; filename*=UTF-8\'\'{encoded}.gns3project' + ) + } return StreamingResponse(streamer(), media_type="application/gns3project", headers=headers) diff --git a/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a b/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a new file mode 100644 index 000000000..5c467f5dc --- /dev/null +++ b/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a @@ -0,0 +1,33 @@ +{ + "appliance_id": "4aa78f74-c769-43e0-9ef1-5dd2cf1909c6", + "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", + "maintainer_email": "bd@cloudswit.ch", + "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/asterfusion-vAsterNOS.gns3a b/gns3server/appliances/asterfusion-vAsterNOS.gns3a index e6e392c32..991c36069 100644 --- a/gns3server/appliances/asterfusion-vAsterNOS.gns3a +++ b/gns3server/appliances/asterfusion-vAsterNOS.gns3a @@ -6,7 +6,7 @@ "vendor_name": "Asterfusion", "vendor_url": "https://cloudswit.ch/", "vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/asterfusion.png", - "documentation_url": "https://help.cloudswit.ch/portal/en/kb/articles/vasternos", + "documentation_url": "https://asternos.com/community/space/vasternos/post/vasternos-for-data-center-network", "product_name": "vAsterNOS", "product_url": "https://cloudswit.ch/product/sonic-enterprise-distribution", "registry_version": 4, @@ -30,18 +30,18 @@ }, "images": [ { - "filename": "vAsterNOS-V3.1.img", + "filename": "AsterNOS_V3.1_R0408P01-VS.img.gz", "version": "V3.1", - "md5sum": "c323c9c3f60e1a93eca2acdc5034b85c", - "filesize": 2724659200, - "download_url": "https://drive.cloudswitch.io/external/8ae2e3932ad8bb2ec30dd25be415d288ff3e4a949c557c6bd48ac6e6265bcfc1" + "md5sum": "5c182306af67be2c79a5642551412e48", + "filesize": 1425945924, + "download_url": "http://pub.asternos.com/AsterNOS_V3.1_R0408P01-VS.img.gz" } ], "versions": [ { "name": "V3.1", "images": { - "hda_disk_image": "vAsterNOS-V3.1.img" + "hda_disk_image": "AsterNOS_V3.1_R0408P01-VS.img.gz" } } ] diff --git a/gns3server/appliances/cisco-7200.gns3a b/gns3server/appliances/cisco-7200.gns3a index 3e723e50b..78d0be865 100644 --- a/gns3server/appliances/cisco-7200.gns3a +++ b/gns3server/appliances/cisco-7200.gns3a @@ -50,6 +50,12 @@ "version": "124-25G", "md5sum": "9c7cc9b3f3b3571411a7f62faaa2c036", "filesize": 71528984 + }, + { + "filename": "c7200-itpk9-mz.124-15.SW.image", + "version": "124-15.SW", + "md5sum": "3334a0facdbd26164e83d3c201fff5b5", + "filesize": 46423952 } ], "versions": [ @@ -87,6 +93,13 @@ "images": { "image": "c7200-a3jk9s-mz.124-25g.image" } + }, + { + "name": "124-15.SW", + "idlepc": "0x60b2bc68", + "images": { + "image": "c7200-itpk9-mz.124-15.SW.image" + } } ] } diff --git a/gns3server/appliances/extreme-networks-voss.gns3a b/gns3server/appliances/extreme-networks-voss.gns3a index a5ef0a64c..08d6c4509 100644 --- a/gns3server/appliances/extreme-networks-voss.gns3a +++ b/gns3server/appliances/extreme-networks-voss.gns3a @@ -28,6 +28,13 @@ "options": "-nographic" }, "images": [ + { + "filename": "FEGNS3.9.3.0.0.qcow2", + "version": "v9.3.0.0", + "md5sum": "2abfa8d64219aa75522238f87b782726", + "filesize": 490209280, + "direct_download_url": "https://akamai-ep.extremenetworks.com/Extreme_P/github-en/Virtual_VOSS/FEGNS3.9.3.0.0.qcow2" + }, { "filename": "VOSSGNS3.8.10.1.0.qcow2", "version": "v8.10.1.0", @@ -93,6 +100,13 @@ } ], "versions": [ + { + "name": "v9.3.0.0", + "images": + { + "hda_disk_image": "FEGNS3.9.3.0.0.qcow2" + } + }, { "name": "v8.10.1.0", "images": diff --git a/gns3server/appliances/freeRouter.gns3a b/gns3server/appliances/freeRouter.gns3a index ca53df882..087ec49fb 100644 --- a/gns3server/appliances/freeRouter.gns3a +++ b/gns3server/appliances/freeRouter.gns3a @@ -25,19 +25,19 @@ }, "images": [ { - "filename": "rtr.qcow2", - "version": "20.7.1", - "md5sum": "4707415d15d20ced92423d90e52eedcd", - "filesize": 254767616, - "download_url": "http://freerouter.nop.hu/", - "direct_download_url": "http://dl.nop.hu/rtr.qcow2" + "filename": "rtr-x86_64.qcow2", + "version": "25.8.29", + "md5sum": "13d72fdfd772258808a9d4b75fa67083", + "filesize": 73269248, + "download_url": "http://www.freertr.org/", + "direct_download_url": "http://dl.nop.hu/rtr-x86_64.qcow2" } ], "versions": [ { - "name": "20.7.1", + "name": "25.8.29", "images": { - "hda_disk_image": "rtr.qcow2" + "hda_disk_image": "rtr-x86_64.qcow2" } } ] diff --git a/gns3server/appliances/infix.gns3a b/gns3server/appliances/infix.gns3a index 2f19f77da..d9ab0321f 100644 --- a/gns3server/appliances/infix.gns3a +++ b/gns3server/appliances/infix.gns3a @@ -2,13 +2,13 @@ "appliance_id": "4d351078-c6f5-444c-ab30-0ef20e3d8c53", "name": "Infix", "category": "router", - "description": "Infix is a Network Operating System based on Linux. It can be set up both as a switch, with offloading using switchdev, and a router with firewalling.", + "description": "Infix OS is a free Linux-based operating system with made-easy management using NETCONF, RESTCONF, or the built-in command line interface (CLI). It is suitable for both end-devices with a single Ethernet port and switches/routers with multiple ports. Immutable by design, extensions are possible using Docker containers.", "vendor_name": "KernelKit", "vendor_url": "https://github.com/kernelkit", "vendor_logo_url": "https://kernelkit.org/assets/img/jack.png", "product_name": "Infix", "registry_version": 4, - "documentation_url": "https://github.com/kernelkit/infix/tree/main/doc", + "documentation_url": "https://kernelkit.org/infix/latest/", "status": "stable", "availability": "free", "maintainer": "KernelKit", @@ -76,9 +76,79 @@ "md5sum": "84bd999513325d0007d0e6587abc6140", "version": "25.04.0", "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.04.0/infix-x86_64-disk-25.04.0.qcow2" + }, + { + "filename": "infix-x86_64-disk-25.05.0.qcow2", + "filesize": 259958784, + "md5sum": "a6b1b3a6bccc60bb5214f2b864602fde", + "version": "25.05.0", + "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.05.0/infix-x86_64-disk-25.05.0.qcow2" + }, + { + "filename": "infix-x86_64-disk-25.08.0.qcow2", + "filesize": 284453888, + "md5sum": "5fbf146d2ac73d1bd8fa1c509d6169b1", + "version": "25.08.0", + "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.08.0/infix-x86_64-disk-25.08.0.qcow2" + }, + { + "filename": "infix-x86_64-disk-25.09.0.qcow2", + "filesize": 285120000, + "md5sum": "9761968406780a5b28311b8527f8629f", + "version": "25.09.0", + "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.09.0/infix-x86_64-disk-25.09.0.qcow2" + }, + { + "filename": "infix-x86_64-disk-25.10.0.qcow2", + "filesize": 288818688, + "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": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-disk-25.10.0.qcow2" + } + }, + { + "name": "25.09.0", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-disk-25.09.0.qcow2" + } + }, + { + "name": "25.08.0", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-disk-25.08.0.qcow2" + } + }, + { + "name": "25.05.0", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-disk-25.05.0.qcow2" + } + }, { "name": "25.04.0", "images": { @@ -115,4 +185,4 @@ } } ] -} +} \ No newline at end of file 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/openwrt.gns3a b/gns3server/appliances/openwrt.gns3a index add7eb588..ac3711d3a 100644 --- a/gns3server/appliances/openwrt.gns3a +++ b/gns3server/appliances/openwrt.gns3a @@ -23,6 +23,15 @@ "kvm": "allow" }, "images": [ + { + "filename": "openwrt-24.10.2-x86-64-generic-ext4-combined.img", + "version": "24.10.2", + "md5sum": "0769916238524161b0a9a639ae261381", + "filesize": 126353408, + "download_url": "https://downloads.openwrt.org/releases/24.10.2/targets/x86/64/", + "direct_download_url": "https://downloads.openwrt.org/releases/24.10.2/targets/x86/64/openwrt-24.10.2-x86-64-generic-ext4-combined.img.gz", + "compression": "gzip" + }, { "filename": "openwrt-23.05.0-x86-64-generic-ext4-combined.img", "version": "23.05.0", @@ -223,7 +232,13 @@ } ], "versions": [ - { + { + "name": "24.10.2", + "images": { + "hda_disk_image": "openwrt-24.10.2-x86-64-generic-ext4-combined.img" + } + }, + { "name": "23.05.0", "images": { "hda_disk_image": "openwrt-23.05.0-x86-64-generic-ext4-combined.img" diff --git a/gns3server/appliances/opnsense.gns3a b/gns3server/appliances/opnsense.gns3a index 759701763..433b4f2de 100644 --- a/gns3server/appliances/opnsense.gns3a +++ b/gns3server/appliances/opnsense.gns3a @@ -25,6 +25,20 @@ "kvm": "require" }, "images": [ + { + "filename": "OPNsense-25.7-nano-amd64.img", + "version": "25.7", + "md5sum": "07aa8b44ebc57fa9ca0145f777901a59", + "filesize": 3221225472, + "download_url": "https://opnsense.c0urier.net/releases/25.7/" + }, + { + "filename": "OPNsense-25.1-nano-amd64.img", + "version": "25.1", + "md5sum": "9c119e4fed7dd7c1c162d8be4088d983", + "filesize": 3221225472, + "download_url": "https://opnsense.c0urier.net/releases/25.1/" + }, { "filename": "OPNsense-24.7-nano-amd64.img", "version": "24.7", diff --git a/gns3server/appliances/vyos.gns3a b/gns3server/appliances/vyos.gns3a index cace44ed3..10bf9ae4c 100644 --- a/gns3server/appliances/vyos.gns3a +++ b/gns3server/appliances/vyos.gns3a @@ -30,6 +30,27 @@ "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", + "md5sum": "4eec0ba2ec367bbbdf2d2f212f947746", + "filesize": 515047424, + "download_url": "https://support.vyos.io/" + }, + { + "filename": "vyos-1.4.2-kvm-amd64.qcow2", + "version": "1.4.2", + "md5sum": "78c5935e05a7bbd94a639fcee18f5799", + "filesize": 514064384, + "download_url": "https://support.vyos.io/" + }, { "filename": "vyos-1.4.1-kvm-amd64.qcow2", "version": "1.4.1", @@ -116,6 +137,24 @@ } ], "versions": [ + { + "name": "1.4.4", + "images": { + "hda_disk_image": "vyos-1.4.4-kvm-amd64.qcow2" + } + }, + { + "name": "1.4.3", + "images": { + "hda_disk_image": "vyos-1.4.3-kvm-amd64.qcow2" + } + }, + { + "name": "1.4.2", + "images": { + "hda_disk_image": "vyos-1.4.2-kvm-amd64.qcow2" + } + }, { "name": "1.4.1", "images": { diff --git a/gns3server/compute/base_manager.py b/gns3server/compute/base_manager.py index 48082e579..17a41fe7e 100644 --- a/gns3server/compute/base_manager.py +++ b/gns3server/compute/base_manager.py @@ -24,6 +24,7 @@ import socket import shutil import re import logging +import inspect log = logging.getLogger(__name__) @@ -196,7 +197,7 @@ class BaseManager: node_id = str(uuid4()) node = self._NODE_CLASS(name, node_id, project, self, *args, **kwargs) - if asyncio.iscoroutinefunction(node.create): + if inspect.iscoroutinefunction(node.create): await node.create() else: node.create() @@ -245,7 +246,7 @@ class BaseManager: """ node = self.get_node(node_id) - if asyncio.iscoroutinefunction(node.close): + if inspect.iscoroutinefunction(node.close): await node.close() else: node.close() diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index 1ab159de4..567d1ef32 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -38,9 +38,9 @@ log = logging.getLogger(__name__) # Be careful to keep it consistent -DOCKER_MINIMUM_API_VERSION = "1.25" -DOCKER_MINIMUM_VERSION = "1.13" -DOCKER_PREFERRED_API_VERSION = "1.30" +DOCKER_MINIMUM_API_VERSION = "1.40" +DOCKER_MINIMUM_VERSION = "19.03.8" +DOCKER_PREFERRED_API_VERSION = "1.44" CHUNK_SIZE = 1024 * 8 # 8KB @@ -125,22 +125,28 @@ class Docker(BaseManager): if not self._connected: try: self._connected = True - version = await self.query("GET", "version") + docker_info = await self.query("GET", "version") except (aiohttp.ClientError, FileNotFoundError): self._connected = False - raise DockerError("Can't connect to docker daemon") + raise DockerError("Can't connect to Docker daemon") - docker_version = parse_version(version["ApiVersion"]) + api_version = parse_version(docker_info['ApiVersion']) + version = docker_info["Version"] - if docker_version < parse_version(DOCKER_MINIMUM_API_VERSION): - raise DockerError( - f"Docker version is {version['Version']}. " - f"GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}" + if api_version < parse_version(DOCKER_MINIMUM_API_VERSION): + raise DockerError(f"Docker version is {version}. " + f"GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}" ) preferred_api_version = parse_version(DOCKER_PREFERRED_API_VERSION) - if docker_version >= preferred_api_version: + if api_version >= preferred_api_version: self._api_version = DOCKER_PREFERRED_API_VERSION + else: + # use the Min API version supported by the daemon + self._api_version = docker_info['MinAPIVersion'] + log.warning("Using Docker client with the minimum API version {}".format(self._api_version)) + + log.info("Connected to Docker daemon version {} using API version {}".format(version, self._api_version)) def connector(self): @@ -188,7 +194,7 @@ class Docker(BaseManager): :param method: HTTP method :param path: Endpoint in API - :param data: Dictionnary with the body. Will be transformed to a JSON + :param data: Dictionary with the body. Will be transformed to a JSON :param params: Parameters added as a query arg :param timeout: Timeout :returns: HTTP response @@ -199,12 +205,11 @@ class Docker(BaseManager): timeout = 60 * 60 * 24 * 31 # One month timeout if path == 'version': - url = "http://docker/v1.24/" + path + url = "http://docker/" + path else: - url = "http://docker/v" + DOCKER_MINIMUM_API_VERSION + "/" + path + await self._check_connection() # version is use by check connection + url = "http://docker/v" + self._api_version + "/" + path try: - if path != "version": # version is use by check connection - await self._check_connection() if self._session is None or self._session.closed: connector = self.connector() self._session = aiohttp.ClientSession(connector=connector) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 2360bc8f9..a72c5173c 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -195,6 +195,14 @@ class DockerVM(BaseNode): def ethernet_adapters(self): return self._ethernet_adapters + @property + def docker_name(self): + """ + Container name in Docker + """ + + return "GNS3.{}.{}".format(self.name, self._project.id) + @property def mac_address(self): """ @@ -539,7 +547,8 @@ class DockerVM(BaseNode): if extra_hosts: params["Env"].append(f"GNS3_EXTRA_HOSTS={extra_hosts}") - result = await self.manager.query("POST", "containers/create", data=params) + # Support name in Doker: [a-zA-Z0-9][a-zA-Z0-9_.-] + result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params) self._cid = result["Id"] log.info(f"Docker container '{self._name}' [{self._id}] created") if self._cpus > 0: @@ -745,6 +754,7 @@ class DockerVM(BaseNode): "-rfbport", str(self.console), "-AlwaysShared", "-SecurityTypes", "None", + "-desktop", self.name, ":{}".format(self._display), stdout=fd, stderr=subprocess.STDOUT) 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" diff --git a/gns3server/compute/project_manager.py b/gns3server/compute/project_manager.py index d1f77cb82..a2ab578d2 100644 --- a/gns3server/compute/project_manager.py +++ b/gns3server/compute/project_manager.py @@ -106,7 +106,7 @@ class ProjectManager: if project_id is not None and project_id in self._projects: return self._projects[project_id] project = Project(name=name, project_id=project_id, path=path, variables=variables) - self._check_available_disk_space(project) + #self._check_available_disk_space(project) # FIXME: disabled for now self._projects[project.id] = project return project diff --git a/gns3server/configs/iou_l2_base_startup-config.txt b/gns3server/configs/iou_l2_base_startup-config.txt index 0ce9f3653..f5d6344e8 100644 --- a/gns3server/configs/iou_l2_base_startup-config.txt +++ b/gns3server/configs/iou_l2_base_startup-config.txt @@ -13,8 +13,7 @@ logging console discriminator EXCESS ! no ip icmp rate-limit unreachable ! -! due to some bugs with IOU, try to change the following line to 'ip cef' if your routing does not work -no ip cef +ip cef no ip domain lookup ! ! diff --git a/gns3server/configs/iou_l3_base_startup-config.txt b/gns3server/configs/iou_l3_base_startup-config.txt index 2706875d7..8a25be616 100644 --- a/gns3server/configs/iou_l3_base_startup-config.txt +++ b/gns3server/configs/iou_l3_base_startup-config.txt @@ -12,8 +12,7 @@ no ip icmp rate-limit unreachable ! ! ! -! due to some bugs with IOU, try to change the following line to 'ip cef' if your routing does not work -no ip cef +ip cef no ip domain lookup ! ! diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 17a207fd4..3861711d3 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -58,7 +58,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "https://61bb46252cabeebd49ee1e09fb8ba72e@o19455.ingest.us.sentry.io/38482" + DSN = "https://1c0ae79fd51b77c674718c6942337300@o19455.ingest.us.sentry.io/38482" _instance = None def __init__(self): diff --git a/gns3server/db/tasks.py b/gns3server/db/tasks.py index 6e147dcee..bfb7dac48 100644 --- a/gns3server/db/tasks.py +++ b/gns3server/db/tasks.py @@ -78,7 +78,7 @@ async def connect_to_db(app: FastAPI) -> None: db_path = os.path.join(Config.instance().config_dir, "gns3_controller.db") db_url = os.environ.get("GNS3_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}") - engine = create_async_engine(db_url, connect_args={"check_same_thread": False, "timeout": 20}, future=True) + engine = create_async_engine(db_url, connect_args={"check_same_thread": False, "timeout": 20}, future=True, pool_size=512, max_overflow=1024) alembic_cfg = config.Config() alembic_cfg.set_main_option("script_location", "gns3server:db_migrations") #alembic_cfg.set_main_option('sqlalchemy.url', db_url) diff --git a/gns3server/utils/asyncio/__init__.py b/gns3server/utils/asyncio/__init__.py index 9b14e17e7..34ae5d441 100644 --- a/gns3server/utils/asyncio/__init__.py +++ b/gns3server/utils/asyncio/__init__.py @@ -20,6 +20,7 @@ import asyncio import sys import os import threading +import inspect async def wait_run_in_executor(func, *args, **kwargs): @@ -90,9 +91,6 @@ 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 """ @@ -106,7 +104,7 @@ async def wait_for_process_termination(process, timeout=10): async def _check_process(process, termination_callback): if not hasattr(sys, "_called_from_test") or not sys._called_from_test: returncode = await process.wait() - if asyncio.iscoroutinefunction(termination_callback): + if inspect.iscoroutinefunction(termination_callback): await termination_callback(returncode) else: termination_callback(returncode) diff --git a/gns3server/utils/file_watcher.py b/gns3server/utils/file_watcher.py index c4f037594..94b64626d 100644 --- a/gns3server/utils/file_watcher.py +++ b/gns3server/utils/file_watcher.py @@ -60,7 +60,14 @@ class FileWatcher: self._hashed[path] = zlib.adler32(open(path, "rb").read()) except OSError: self._hashed[path] = None - asyncio.get_event_loop().call_later(self._delay, self._check_config_file_change) + + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + loop.call_later(self._delay, self._check_config_file_change) def __del__(self): self._closed = True diff --git a/gns3server/version.py b/gns3server/version.py index fb3835633..a42f1dfd7 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -22,8 +22,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "3.0.5" -__version_info__ = (3, 0, 5, 0) +__version__ = "3.0.6" +__version_info__ = (3, 0, 6, 0) if "dev" in __version__: try: diff --git a/pyproject.toml b/pyproject.toml index cddf3df93..b16daf0af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "gns3-server" description = "GNS3 graphical interface for the GNS3 server." -license = {file = "LICENSE"} +license-files = ["LICENSE"] authors = [ { name = "Jeremy Grossmann", email = "developers@gns3.com" } ] diff --git a/requirements.txt b/requirements.txt index a10ece344..527d08c33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,24 @@ -uvicorn==0.34.2 # uvicorn 0.33 is the last version supporting Python 3.8 -pydantic==2.11.4 -fastapi==0.115.12 -python-multipart==0.0.20 -websockets==15.0.1 -aiohttp>=3.11.16,<3.12 -async-timeout==5.0.1; python_version < '3.11' -aiofiles>=24.1.0,<25.0 +uvicorn==0.39.0 # version 0.39.0 is the last version supporting Python 3.9 +pydantic==2.12.5 +fastapi==0.128.0 +python-multipart==0.0.20 # version 0.0.20 is the last to support Python 3.9 +websockets==15.0.1 # version 15.0.1 is the last to support Python 3.9 +aiohttp>=3.13.3,<3.14 +aiofiles>=25.1.0,<26.0 Jinja2>=3.1.6,<3.2 -sentry-sdk>=2.26.1,<2.27 # optional dependency -psutil>=7.0.0 -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; python_version < '3.11' # this library has effectively been upstreamed into Python 3.11+ distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 -greenlet==3.2.0 # necessary to run sqlalchemy on Python 3.13 -sqlalchemy==2.0.40 +greenlet==3.3.1; python_version >= '3.13' # necessary to run sqlalchemy on Python >= 3.13 +sqlalchemy==2.0.43 aiosqlite==0.21.0 alembic==1.15.2 -bcrypt==4.3.0 -joserfc==1.0.4 -email-validator==2.2.0 +bcrypt==5.0.0 +joserfc==1.6.1 +email-validator==2.3.0 watchdog==6.0.0 -zstandard==0.23.0 +zstandard==0.25.0 platformdirs>=2.4.0,<3 # platformdirs >=3 conflicts when building Debian packages -importlib-resources>=1.3; python_version <= '3.9' -truststore>=0.10.1; python_version >= '3.10' +truststore>=0.10.4; python_version >= '3.10' 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() diff --git a/tests/api/routes/controller/test_projects.py b/tests/api/routes/controller/test_projects.py index cd3439c04..9dc23ab37 100644 --- a/tests/api/routes/controller/test_projects.py +++ b/tests/api/routes/controller/test_projects.py @@ -303,7 +303,7 @@ class TestControllerProjectRoutes: params={"include_images": "yes"}) assert response.status_code == status.HTTP_200_OK assert response.headers['CONTENT-TYPE'] == 'application/gns3project' - assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{}.gns3project"'.format(project.name) + assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name) with open(str(tmpdir / 'project.zip'), 'wb+') as f: f.write(response.content) @@ -346,8 +346,7 @@ class TestControllerProjectRoutes: params={"include_images": "0"}) assert response.status_code == status.HTTP_200_OK assert response.headers['CONTENT-TYPE'] == 'application/gns3project' - assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{}.gns3project"'.format(project.name) - + assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name) with open(str(tmpdir / 'project.zip'), 'wb+') as f: f.write(response.content) @@ -412,8 +411,7 @@ class TestControllerProjectRoutes: if response.status_code == status.HTTP_200_OK: assert response.headers['CONTENT-TYPE'] == 'application/gns3project' - assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{}.gns3project"'.format(project.name) - + assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name) with open(str(tmpdir / 'project.zip'), 'wb+') as f: f.write(response.content) diff --git a/tests/api/routes/controller/test_users.py b/tests/api/routes/controller/test_users.py index 87ea74708..e608e1e68 100644 --- a/tests/api/routes/controller/test_users.py +++ b/tests/api/routes/controller/test_users.py @@ -183,7 +183,7 @@ class TestAuthTokens: jwt_secret = config.settings.Controller.jwt_secret_key token = auth_service.create_access_token(test_user.username) with pytest.raises(ValueError): - jwt.decode(token, jwt_secret, algorithms=["ES256"]) + jwt.decode(token, jwt_secret, algorithms=["HS256"]) async def test_can_retrieve_username_from_token( self, diff --git a/tests/compute/docker/test_docker.py b/tests/compute/docker/test_docker.py index d1f3c6e5e..917255f78 100644 --- a/tests/compute/docker/test_docker.py +++ b/tests/compute/docker/test_docker.py @@ -49,7 +49,7 @@ async def test_query_success(vm): vm._session.request = AsyncioMagicMock(return_value=response) data = await vm.query("POST", "test", data={"a": True}, params={"b": 1}) vm._session.request.assert_called_with('POST', - 'http://docker/v1.25/test', + 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -72,7 +72,7 @@ async def test_query_error(vm): with pytest.raises(DockerError): await vm.query("POST", "test", data={"a": True}, params={"b": 1}) vm._session.request.assert_called_with('POST', - 'http://docker/v1.25/test', + 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -93,7 +93,7 @@ async def test_query_error_json(vm): with pytest.raises(DockerError): await vm.query("POST", "test", data={"a": True}, params={"b": 1}) vm._session.request.assert_called_with('POST', - 'http://docker/v1.25/test', + 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -188,7 +188,9 @@ async def test_docker_check_connection_docker_minimum_version(vm): async def test_docker_check_connection_docker_preferred_version_against_newer(vm): response = { - 'ApiVersion': '1.31' + 'ApiVersion': '1.52', + 'Version': '29.0.1', + } with patch("gns3server.compute.docker.Docker.connector"), \ @@ -202,7 +204,9 @@ async def test_docker_check_connection_docker_preferred_version_against_newer(vm async def test_docker_check_connection_docker_preferred_version_against_older(vm): response = { - 'ApiVersion': '1.27', + 'ApiVersion': '1.43', + 'Version': '24.0.2', + 'MinAPIVersion': '1.40' } with patch("gns3server.compute.docker.Docker.connector"), \ @@ -212,6 +216,21 @@ async def test_docker_check_connection_docker_preferred_version_against_older(vm assert vm._api_version == DOCKER_MINIMUM_API_VERSION +@pytest.mark.asyncio +async def test_docker_check_connection_docker_unsupported_version(vm): + + response = { + 'ApiVersion': '1.25', + 'Version': '1.13.1', + } + + with patch("gns3server.compute.docker.Docker.connector"), \ + asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): + vm._connected = False + with pytest.raises(DockerError) as e: + await vm._check_connection() + + @pytest.mark.asyncio async def test_install_busybox(): diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 8eaaef6f0..33eea1cbb 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -99,7 +99,7 @@ async def test_create(compute_project, manager): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -150,7 +150,7 @@ async def test_create_with_tag(compute_project, manager): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:16.04") await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -204,7 +204,7 @@ async def test_create_vnc(compute_project, manager): vm._start_vnc = MagicMock() vm._display = 42 await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -356,7 +356,7 @@ async def test_create_start_cmd(compute_project, manager): vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") vm._start_command = "/bin/ls" await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -469,7 +469,7 @@ async def test_create_image_not_available(compute_project, manager): with asyncio_patch("gns3server.compute.docker.DockerVM.pull_image", return_value=True) as mock_pull: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -524,7 +524,7 @@ async def test_create_with_user(compute_project, manager): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -624,7 +624,7 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1"]) await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -680,7 +680,7 @@ async def test_create_with_extra_volumes_duplicate_2_user(compute_project, manag with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1", "/vol/1"]) await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -736,7 +736,7 @@ async def test_create_with_extra_volumes_duplicate_3_subdir(compute_project, man with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1/", "/vol"]) await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -792,7 +792,7 @@ async def test_create_with_extra_volumes_duplicate_4_backslash(compute_project, with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol//", "/vol"]) await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -848,7 +848,7 @@ async def test_create_with_extra_volumes_duplicate_5_subdir_issue_1595(compute_p with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc"]) await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -899,7 +899,7 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc/test", "/etc"]) await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -956,7 +956,7 @@ async def test_create_with_extra_volumes(compute_project, manager): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/2"]) await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -1243,7 +1243,7 @@ async def test_update(vm): await vm.update() mock_query.assert_any_call("DELETE", "containers/e90e34656842", params={"force": 1, "v": 1}) - mock_query.assert_any_call("POST", "containers/create", data={ + mock_query.assert_any_call("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -1325,7 +1325,7 @@ async def test_update_running(vm): await vm.update() mock_query.assert_any_call("DELETE", "containers/e90e34656842", params={"force": 1, "v": 1}) - mock_query.assert_any_call("POST", "containers/create", data={ + mock_query.assert_any_call("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -1685,7 +1685,7 @@ async def test_start_vnc(vm): with asyncio_patch("asyncio.create_subprocess_exec") as mock_exec: await vm._start_vnc() assert vm._display is not None - assert mock_exec.call_args[0] == ("/bin/Xtigervnc", "-extension", "MIT-SHM", "-geometry", vm.console_resolution, "-depth", "16", "-interface", "127.0.0.1", "-rfbport", str(vm.console), "-AlwaysShared", "-SecurityTypes", "None", ":{}".format(vm._display)) + assert mock_exec.call_args[0] == ("/bin/Xtigervnc", "-extension", "MIT-SHM", "-geometry", vm.console_resolution, "-depth", "16", "-interface", "127.0.0.1", "-rfbport", str(vm.console), "-AlwaysShared", "-SecurityTypes", "None", "-desktop", "test", ":{}".format(vm._display)) mock_wait.assert_called_with("/tmp/.X11-unix/X{}".format(vm._display)) @@ -1791,7 +1791,7 @@ async def test_cpus(compute_project, manager): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", cpus=0.5) await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, @@ -1842,7 +1842,7 @@ async def test_memory(compute_project, manager): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", memory=32) await vm.create() - mock.assert_called_with("POST", "containers/create", data={ + mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ "Tty": True, "OpenStdin": True, "StdinOnce": False, diff --git a/tests/utils/test_asyncio.py b/tests/utils/test_asyncio.py index 1b0056587..cb59bfe39 100644 --- a/tests/utils/test_asyncio.py +++ b/tests/utils/test_asyncio.py @@ -18,11 +18,9 @@ import asyncio import pytest -import sys -from unittest.mock import MagicMock -from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output, wait_for_process_termination, locking -from tests.utils import AsyncioMagicMock +from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output, locking + @pytest.mark.asyncio diff --git a/win-requirements.txt b/win-requirements.txt index 784162589..1cc7ba1e2 100644 --- a/win-requirements.txt +++ b/win-requirements.txt @@ -1,4 +1,4 @@ -r requirements.txt -pywin32==306 +pywin32==311 wmi==1.5.1