diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 845e4d9f..bdf8dd3a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest"] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] #include: # only test with Python 3.10 on Windows # - os: windows-latest diff --git a/CHANGELOG b/CHANGELOG index c0623d74..a5a834f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,15 @@ # Change Log +## 2.2.51 07/11/2024 + +* Catch error when cannot resize Docker container TTY. +* Do not use "ide" if there is a disk image and no interface type has been explicitly configured. +* Use @locking when sending uBridge commands. Ref https://github.com/GNS3/gns3-gui/issues/3651 +* Fix run Docker containers with user namespaces enabled. Fixes #2414 +* Python 3.13 support +* Upgrade dependencies +* Fix errors in init.sh. Fixes #2431 + ## 2.2.50 21/10/2024 * Bundle web-ui v2.2.50 diff --git a/gns3server/appliances/nixos.gns3a b/gns3server/appliances/nixos.gns3a new file mode 100644 index 00000000..32d3b609 --- /dev/null +++ b/gns3server/appliances/nixos.gns3a @@ -0,0 +1,52 @@ +{ + "appliance_id": "00714342-14b2-4281-aa20-9043ca8dc26e", + "name": "NixOS", + "category": "guest", + "description": "NixOS QEMU Appliance for images created with nixos-generator. Automatically sets hostname based on vmname.", + "vendor_name": "NixOS", + "vendor_url": "https://nixos.org/", + "vendor_logo_url": "https://avatars.githubusercontent.com/u/487568", + "documentation_url": "https://github.com/ob7/gns3-nixos-appliance", + "product_name": "NixOS", + "product_url": "https://github.com/NixOS/nixpkgs", + "registry_version": 4, + "status": "experimental", + "availability": "free", + "maintainer": "ob7dev", + "maintainer_email": "dev@ob7.us", + "usage": "For custom NixOS images, create qcow2 VM with: nixos-generate -f qcow -c ./server.nix Import it into GNS3 as image. VM name is passed into QEMU guest with Advanced Options field entry: -fw_cfg name=opt/vm_hostname,string=%vm-name%", + "symbol": ":/symbols/affinity/circle/gray/template.svg", + "first_port_name": "eth0", + "port_name_format": "eth{0}", + "qemu": { + "adapter_type": "e1000", + "adapters": 4, + "ram": 256, + "cpus": 1, + "hda_disk_interface": "ide", + "arch": "x86_64", + "console_type": "telnet", + "kvm": "allow", + "options": "-fw_cfg name=opt/vm_hostname,string=%vm-name%", + "on_close": "power_off" + }, + "images": [ + { + "filename": "nixos-24-11.qcow2", + "version": "24.11", + "md5sum": "2459f05136836dd430402d75cba0f205", + "download_url": "https://github.com/nix-community/nixos-generators", + "filesize": 1749483520, + "download_url": "https://f.ob7.us/gns3/", + "direct_download_url": "http://ob7.us/nixos-24-11.qcow2" + } + ], + "versions": [ + { + "name": "24.11", + "images": { + "hda_disk_image": "nixos-24-11.qcow2" + } + } + ] +} diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 639a5d41..c95138e4 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -406,13 +406,11 @@ class BaseNode: Stops the telnet proxy. """ - if self._wrapper_telnet_server: + if self._wrap_console_writer: self._wrap_console_writer.close() - if sys.version_info >= (3, 7, 0): - try: - await self._wrap_console_writer.wait_closed() - except ConnectionResetError: - pass + await self._wrap_console_writer.wait_closed() + self._wrap_console_writer = None + if self._wrapper_telnet_server: self._wrapper_telnet_server.close() await self._wrapper_telnet_server.wait_closed() self._wrapper_telnet_server = None @@ -662,6 +660,7 @@ class BaseNode: path = shutil.which(path) return path + @locking async def _ubridge_send(self, command): """ Sends a command to uBridge hypervisor. diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index fff4e435..24a70e94 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -383,8 +383,8 @@ class DockerVM(BaseNode): "CapAdd": ["ALL"], "Privileged": True, "Binds": self._mount_binds(image_infos), + "UsernsMode": "host", }, - "UsernsMode": "host", "Volumes": {}, "Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573 "Cmd": [], @@ -710,7 +710,10 @@ class DockerVM(BaseNode): """ # resize the container TTY. - await self._manager.query("POST", "containers/{}/resize?h={}&w={}".format(self._cid, rows, columns)) + try: + await self._manager.query("POST", "containers/{}/resize?h={}&w={}".format(self._cid, rows, columns)) + except DockerError as e: + log.warning(f"Could not resize the container TTY: {e}") async def _start_console(self): """ diff --git a/gns3server/compute/docker/resources/init.sh b/gns3server/compute/docker/resources/init.sh index 9811bdeb..05c6ee2e 100755 --- a/gns3server/compute/docker/resources/init.sh +++ b/gns3server/compute/docker/resources/init.sh @@ -25,7 +25,7 @@ PATH=/gns3/bin:/tmp/gns3/bin:/sbin:$PATH # bootstrap busybox commands if [ ! -d /tmp/gns3/bin ]; then busybox mkdir -p /tmp/gns3/bin - for applet in `busybox --list-full` + for applet in `busybox --list` do ln -s /gns3/bin/busybox "/tmp/gns3/bin/$applet" done diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 99b2d711..f8932d2b 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -1915,11 +1915,6 @@ class QemuVM(BaseNode): continue interface = getattr(self, "hd{}_disk_interface".format(drive)) - # fail-safe: use "ide" if there is a disk image and no interface type has been explicitly configured - if interface == "none": - interface = "ide" - setattr(self, "hd{}_disk_interface".format(drive), interface) - disk_name = "hd" + drive if not os.path.isfile(disk_image) or not os.path.exists(disk_image): if os.path.islink(disk_image): diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index d0bf23dd..6ad998a5 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://decb06a5d7bda07bef342a41c5c9d6e1@o19455.ingest.us.sentry.io/38482" + DSN = "https://088679fcf3aa35f775356982a80fe37c@o19455.ingest.us.sentry.io/38482" _instance = None def __init__(self): diff --git a/gns3server/version.py b/gns3server/version.py index a8d7b0c2..533a4376 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.50" -__version_info__ = (2, 2, 50, 0) +__version__ = "2.2.51" +__version_info__ = (2, 2, 51, 0) if "dev" in __version__: try: diff --git a/requirements.txt b/requirements.txt index 8d64c5d0..9a94f5ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ jsonschema>=4.23,<4.24 -aiohttp>=3.10.3,<3.11 +aiohttp>=3.10.10,<3.11 aiohttp-cors>=0.7.0,<0.8 aiofiles>=24.1.0,<25.0 Jinja2>=3.1.4,<3.2 -sentry-sdk==2.12,<2.13 # optional dependency -psutil==6.0.0 +sentry-sdk>=2.17,<2.18 # optional dependency +psutil>=6.1.0 async-timeout>=4.0.3,<4.1 distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 platformdirs>=2.4.0 importlib-resources>=1.3; python_version < '3.9' -truststore>=0.9.1; python_version >= '3.10' +truststore>=0.10.0; python_version >= '3.10' diff --git a/setup.py b/setup.py index 034335d8..289e15b3 100644 --- a/setup.py +++ b/setup.py @@ -16,9 +16,6 @@ # along with this program. If not, see . import sys -import os -import shutil -import subprocess from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand @@ -67,8 +64,8 @@ setup( include_package_data=True, zip_safe=False, platforms="any", - python_requires='>=3.8', - setup_requires=["setuptools>=17.1"], + python_requires=">=3.8", + setup_requires=["setuptools>=61.0"], classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -86,6 +83,7 @@ setup( "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", ], ) diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 3afea049..cb2099d2 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -107,9 +107,9 @@ async def test_create(compute_project, manager): "{}:/gns3:ro".format(Docker.resources_path()), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -146,9 +146,9 @@ async def test_create_with_tag(compute_project, manager): "{}:/gns3:ro".format(Docker.resources_path()), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -189,9 +189,9 @@ async def test_create_vnc(compute_project, manager): "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "/tmp/.X11-unix/X{0}:/tmp/.X11-unix/X{0}:ro".format(vm._display) ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -319,9 +319,9 @@ async def test_create_start_cmd(compute_project, manager): "{}:/gns3:ro".format(Docker.resources_path()), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "Entrypoint": ["/gns3/init.sh"], "Cmd": ["/bin/ls"], @@ -418,9 +418,9 @@ async def test_create_image_not_available(compute_project, manager): "{}:/gns3:ro".format(Docker.resources_path()), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -462,9 +462,9 @@ async def test_create_with_user(compute_project, manager): "{}:/gns3:ro".format(Docker.resources_path()), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -546,9 +546,9 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")), ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -586,9 +586,9 @@ async def test_create_with_extra_volumes_duplicate_2_user(compute_project, manag "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")), ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -626,9 +626,9 @@ async def test_create_with_extra_volumes_duplicate_3_subdir(compute_project, man "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/vol".format(os.path.join(vm.working_dir, "vol")), ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -666,9 +666,9 @@ async def test_create_with_extra_volumes_duplicate_4_backslash(compute_project, "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/vol".format(os.path.join(vm.working_dir, "vol")), ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -705,9 +705,9 @@ async def test_create_with_extra_volumes_duplicate_5_subdir_issue_1595(compute_p "{}:/gns3:ro".format(Docker.resources_path()), "{}:/gns3volumes/etc".format(os.path.join(vm.working_dir, "etc")), ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -744,9 +744,9 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p "{}:/gns3:ro".format(Docker.resources_path()), "{}:/gns3volumes/etc".format(os.path.join(vm.working_dir, "etc")), ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -791,9 +791,9 @@ async def test_create_with_extra_volumes(compute_project, manager): "{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")), "{}:/gns3volumes/vol/2".format(os.path.join(vm.working_dir, "vol", "2")), ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -1040,9 +1040,9 @@ async def test_update(vm): "{}:/gns3:ro".format(Docker.resources_path()), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -1109,9 +1109,9 @@ async def test_update_running(vm): "{}:/gns3:ro".format(Docker.resources_path()), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", diff --git a/tests/compute/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index 59d00687..a194684c 100644 --- a/tests/compute/qemu/test_qemu_vm.py +++ b/tests/compute/qemu/test_qemu_vm.py @@ -353,16 +353,23 @@ async def test_set_platform(compute_project, manager): async def test_disk_options(vm, tmpdir, fake_qemu_img_binary): vm._hda_disk_image = str(tmpdir / "test.qcow2") + vm._hda_disk_interface = "ide" + vm._hdb_disk_image = str(tmpdir / "test2.qcow2") open(vm._hda_disk_image, "w+").close() + open(vm._hdb_disk_image, "w+").close() - with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM._find_disk_file_format", return_value="qcow2"): + with (asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM._find_disk_file_format", return_value="qcow2")): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: options = await vm._disk_options() assert process.called args, kwargs = process.call_args - assert args == (fake_qemu_img_binary, "create", "-o", "backing_file={}".format(vm._hda_disk_image), "-F", "qcow2", "-f", "qcow2", os.path.join(vm.working_dir, "hda_disk.qcow2")) + assert args == (fake_qemu_img_binary, "create", "-o", "backing_file={}".format(vm._hda_disk_image), "-F", "qcow2", "-f", "qcow2", os.path.join(vm.working_dir, "hda_disk.qcow2")) or \ + args == (fake_qemu_img_binary, "create", "-o", "backing_file={}".format(vm._hdb_disk_image), "-F", "qcow2", "-f", "qcow2", os.path.join(vm.working_dir, "hdb_disk.qcow2")) - assert options == ['-drive', 'file=' + os.path.join(vm.working_dir, "hda_disk.qcow2") + ',if=ide,index=0,media=disk,id=drive0'] + assert options == [ + '-drive', 'file=' + os.path.join(vm.working_dir, "hda_disk.qcow2") + ',if=ide,index=0,media=disk,id=drive0', + '-drive', 'file=' + os.path.join(vm.working_dir, "hdb_disk.qcow2") + ',if=none,index=1,media=disk,id=drive1', + ] async def test_cdrom_option(vm, tmpdir, fake_qemu_img_binary): @@ -440,9 +447,13 @@ async def test_tpm_option(vm, tmpdir, fake_qemu_img_binary): async def test_disk_options_multiple_disk(vm, tmpdir, fake_qemu_img_binary): vm._hda_disk_image = str(tmpdir / "test0.qcow2") + vm._hda_disk_interface = "ide" vm._hdb_disk_image = str(tmpdir / "test1.qcow2") + vm._hdb_disk_interface = "ide" vm._hdc_disk_image = str(tmpdir / "test2.qcow2") + vm._hdc_disk_interface = "ide" vm._hdd_disk_image = str(tmpdir / "test3.qcow2") + vm._hdd_disk_interface = "ide" open(vm._hda_disk_image, "w+").close() open(vm._hdb_disk_image, "w+").close() open(vm._hdc_disk_image, "w+").close()