Merge branch '2.2' into 3.1

# Conflicts:
#	gns3server/compute/qemu/qemu_vm.py
#	gns3server/controller/import_project.py
#	gns3server/controller/project.py
#	gns3server/crash_report.py
#	gns3server/version.py
#	tests/compute/docker/test_docker_vm.py
#	tests/controller/test_import_project.py
#	tests/controller/test_project.py
This commit is contained in:
grossmj committed 2026-08-08 16:27:14 +02:00
commit dff79f65d3
11 files changed
+260 -32

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
{
"appliance_id": "b3b90fde-143a-4129-8031-ccbba73c5e02",
"name": "armbian",
"category": "guest",
"description": "A highly optimized base operating system specialized for single board computers (SBCs) and its extensive build framework.",
"vendor_name": "The Armbian team",
"vendor_url": "https://armbian.com/",
"documentation_url": "https://docs.armbian.com/",
"product_name": "Armbian UEFI x86",
"product_url": "https://armbian.com/boards/uefi-x86",
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "By first login you create root password and new sudo user.\n\nBoot disk from UEFI shell, type: FS0:EFI\\BOOT\\BOOTX64 and press <Enter>",
"port_name_format": "Ethernet{0}",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 2,
"ram": 256,
"hda_disk_interface": "virtio",
"arch": "x86_64",
"console_type": "spice+agent",
"uefi": false,
"boot_priority": "c",
"kvm": "require",
"options": "-nographic"
},
"images": [
{
"filename": "OVMF-edk2-stable202305.fd",
"version": "stable202305",
"md5sum": "6c4cf1519fec4a4b95525d9ae562963a",
"filesize": 4194304,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/OVMF-edk2-stable202305.fd.zip/download",
"compression": "zip"
},
{
"filename": "Armbian_26.5.1_Uefi-x86_trixie_cloud_6.18.32_minimal.img.qcow2",
"version": "Armbian 26.5.1 Minimal (CLI)",
"md5sum": "7f4c915668718d6135406de5a6c4fc30",
"filesize": 877920512,
"download_url": "https://armbian.com/boards/uefi-x86",
"direct_download_url": "https://armbian.atomonetworks.com/dl/uefi-x86/archive/Armbian_26.5.1_Uefi-x86_trixie_cloud_6.18.32_minimal.img.qcow2.xz",
"compression": "xz"
}
],
"versions": [
{
"name": "Armbian 26.5.1 Minimal (CLI)",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "Armbian_26.5.1_Uefi-x86_trixie_cloud_6.18.32_minimal.img.qcow2"
}
}
]
}
+1 -1
View File
@@ -1048,7 +1048,7 @@ class DockerVM(BaseNode):
await self._fix_permissions()
state = await self._get_container_state()
if state != "stopped" or state != "exited":
if state != "stopped" and state != "exited":
# t=5 number of seconds to wait before killing the container
try:
await self.manager.query("POST", f"containers/{self._cid}/stop", params={"t": 5})
+4 -2
View File
@@ -2341,7 +2341,7 @@ class QemuVM(BaseNode):
options.extend(["-drive", "if=pflash,format=raw,file={}".format(ovmf_vars_node_path)])
# edk2 firmware requires a Random Number Generator (RNG) device in order to turn network adapters on
options.extend(["-object", "rng-random,filename=/dev/urandom,id=rng0 "])
options.extend(["-object", "rng-random,filename=/dev/urandom,id=rng0"])
options.extend(["-device", "virtio-rng-pci,rng=rng0"])
return options
@@ -2665,7 +2665,6 @@ class QemuVM(BaseNode):
elif sys.platform.startswith("darwin"):
command.extend(["-enable-hax"])
command.extend(["-boot", f"order={self._boot_priority}"])
command.extend(self._bios_option())
command.extend(self._cdrom_option())
command.extend(await self._disk_options())
command.extend(self._linux_boot_options())
@@ -2675,6 +2674,9 @@ class QemuVM(BaseNode):
command.extend(self._aux_options())
command.extend(self._monitor_options())
command.extend(await self._network_options())
command.extend((await self._network_options()))
# bios options must be last to have predictable NIC numbering, see https://github.com/GNS3/gns3-server/issues/2838
command.extend(self._bios_option())
if self.on_close != "save_vm_state":
await self._clear_save_vm_stated()
else:
+31 -9
View File
@@ -182,24 +182,46 @@ async def import_project(
project = await controller.load_project(dot_gns3_path, load=False)
return project
def _create_symbolic_links(zip_file, path):
"""
Manually create symbolic links (if any) because ZipFile does not support it.
Refuse any target that escapes `path`.
:param zip_file: ZipFile instance
:param path: project location
"""
path_root = os.path.realpath(path) + os.sep
for zip_info in zip_file.infolist():
if stat.S_ISLNK(zip_info.external_attr >> 16):
symlink_target = zip_file.read(zip_info.filename).decode()
symlink_path = os.path.join(path, zip_info.filename)
try:
# remove the regular file and replace it by a symbolic link
os.remove(symlink_path)
os.symlink(symlink_target, symlink_path)
except OSError as e:
raise ControllerError(f"Cannot create symbolic link: {e}")
if not stat.S_ISLNK(zip_info.external_attr >> 16):
continue
symlink_target = zip_file.read(zip_info.filename).decode()
symlink_path = os.path.join(path, zip_info.filename)
# 1. Reject absolute targets outright.
if os.path.isabs(symlink_target):
raise ControllerError(f"Symlink {zip_info.filename!r} has absolute target {symlink_target!r}, refusing")
# 2. Reject paths where the entry name itself escapes (defence in depth;
# extractall normally would already have caught this).
member_abs = os.path.realpath(symlink_path)
if not (member_abs + os.sep).startswith(path_root) and member_abs + os.sep != path_root:
raise ControllerError(f"Symlink entry {zip_info.filename!r} escapes project dir, refusing")
# 3. Resolve the symlink target relative to the entry's own parent
# directory and verify the resolved real path stays inside `path`.
link_dir = os.path.realpath(os.path.dirname(symlink_path))
resolved_target = os.path.realpath(os.path.join(link_dir, symlink_target))
if not (resolved_target + os.sep).startswith(path_root) and resolved_target + os.sep != path_root:
raise ControllerError("Symlink {zip_info.filename!r} -> {symlink_target!r} escapes project dir, refusing")
try:
os.remove(symlink_path)
os.symlink(symlink_target, symlink_path)
except OSError as e:
raise ControllerError(f"Cannot create symbolic link: {e}")
def regenerate_topology_ids(topology, new_project_path, reset_mac_addresses=False):
"""
+6 -3
View File
@@ -1434,7 +1434,7 @@ class Project:
if self._status != "opened":
try:
await self.open()
await self.open(auto_start=False)
except ControllerError as e:
# ignore missing images or other conflicts when deleting a project
log.warning(f"Conflict while deleting project: {e}")
@@ -1524,9 +1524,12 @@ class Project:
return os.path.join(self.path, self._filename)
@locking
async def open(self):
async def open(self, auto_start=True):
"""
Load topology elements
:param auto_start: whether the nodes may be started when the project
has auto start enabled
"""
if self._closing is True:
@@ -1692,7 +1695,7 @@ class Project:
self._loading = False
self.emit_controller_notification("project.opened", self.asdict())
# Should we start the nodes when project is open
if self._auto_start:
if self._auto_start and auto_start:
# Start all in the background without waiting for completion
# we ignore errors because we want to let the user open
# their project and fix it