Merge branch '2.2' into 3.0

# Conflicts:
#	CHANGELOG
#	gns3server/crash_report.py
#	gns3server/static/web-ui/index.html
#	gns3server/static/web-ui/main.9297104511b6616fc55c.js
#	gns3server/utils/images.py
#	gns3server/version.py
#	tests/api/routes/compute/test_dynamips_nodes.py
#	tests/handlers/api/compute/test_qemu.py
#	tests/utils/test_images.py
This commit is contained in:
grossmj 2024-12-02 12:16:34 +10:00
commit cbaa563996
No known key found for this signature in database
GPG Key ID: 0A2D76AC45EA25CD
10 changed files with 208 additions and 80 deletions

View File

@ -1,5 +1,14 @@
# Change Log
## 2.2.52 02/12/2024
* Bundle web-ui v2.2.52
* Sync appliances
* Remove restrictions based on file extension when listing images and fix ELF header checks
* Fix use project name instead of ID for fast duplication when running local server. Fixes #2446
* Overwrite user resources when the originals have changed.
* Relax setuptools requirement to allow for easier Debian packaging on Ubuntu Focal & Jammy
## 3.0.0rc2 20/11/2024
* Bundle web-ui v3.0.0rc2

View File

@ -33,11 +33,23 @@
"md5sum": "cbbbea66a253f1dac0fcf81274dc778d",
"filesize": 87756936
},
{
"filename": "c7200-adventerprisek9-mz.152-4.M11.image",
"version": "152-4.M11",
"md5sum": "9a2005ad09ce1ec6fe7cf9af1e9b099e",
"filesize": 128487680
},
{
"filename": "c7200-adventerprisek9-mz.124-24.T5.image",
"version": "124-24.T5",
"md5sum": "6b89d0d804e1f2bb5b8bda66b5692047",
"filesize": 102345240
},
{
"filename": "c7200-a3jk9s-mz.124-25g.image",
"version": "124-25G",
"md5sum": "9c7cc9b3f3b3571411a7f62faaa2c036",
"filesize": 71528984
}
],
"versions": [
@ -55,12 +67,26 @@
"image": "c7200-advipservicesk9-mz.152-4.S5.image"
}
},
{
"name": "152-4.M11",
"idlepc": "0x6062e5c0",
"images": {
"image": "c7200-adventerprisek9-mz.152-4.M11.image"
}
},
{
"name": "124-24.T5",
"idlepc": "0x606df838",
"images": {
"image": "c7200-adventerprisek9-mz.124-24.T5.image"
}
},
{
"name": "124-25G",
"idlepc": "0x6066a998",
"images": {
"image": "c7200-a3jk9s-mz.124-25g.image"
}
}
]
}

View File

@ -12,7 +12,7 @@
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "There is no default password and enable password. A default configuration is present. ASAv goes through a double-boot before becoming active. This is normal and expected.",
"usage": "There is no default password and enable password. A default configuration is present. ASAv goes through a double-boot before becoming active. This is normal and expected. Switch to the Telnet console type after the first boot.",
"symbol": ":/symbols/asa.svg",
"first_port_name": "Management0/0",
"port_name_format": "Gi0/{0}",
@ -26,6 +26,13 @@
"kvm": "require"
},
"images": [
{
"filename": "asav9-22-1-1.qcow2",
"version": "9.22.1.1 CML",
"md5sum": "250a924cdc2370208eaac9d1dc8dc9e3",
"filesize": 379518976,
"download_url": "https://learningnetworkstore.cisco.com/cisco-modeling-labs-personal/cisco-modeling-labs-personal/CML-PERSONAL.html"
},
{
"filename": "asav9-18-2.qcow2",
"version": "9.18.2 CML",
@ -126,6 +133,12 @@
}
],
"versions": [
{
"name": "9.22.1.1 CML",
"images": {
"hda_disk_image": "asav9-22-1-1.qcow2"
}
},
{
"name": "9.18.2 CML",
"images": {

View File

@ -13,11 +13,17 @@
"iou": {
"ethernet_adapters": 4,
"serial_adapters": 0,
"nvram": 128,
"ram": 256,
"nvram": 512,
"ram": 512,
"startup_config": "iou_l2_base_startup-config.txt"
},
"images": [
{
"filename": "x86_64_crb_linux_l2-adventerprisek9-ms.iol",
"version": "17.15.1",
"md5sum": "6c587cdfd5056078e70b3f6c26800d66",
"filesize": 243251976
},
{
"filename": "x86_64_crb_linux_l2-adventerprisek9-ms.bin",
"version": "17.12.1",
@ -44,6 +50,12 @@
}
],
"versions": [
{
"name": "17.15.1",
"images": {
"image": "x86_64_crb_linux_l2-adventerprisek9-ms.iol"
}
},
{
"name": "17.12.1",
"images": {

View File

@ -13,11 +13,17 @@
"iou": {
"ethernet_adapters": 2,
"serial_adapters": 2,
"nvram": 128,
"ram": 256,
"nvram": 512,
"ram": 512,
"startup_config": "iou_l3_base_startup-config.txt"
},
"images": [
{
"filename": "x86_64_crb_linux-adventerprisek9-ms.iol",
"version": "17.15.1",
"md5sum": "5d584f6cfbeaadc87d55f613da1049ed",
"filesize": 292001512
},
{
"filename": "x86_64_crb_linux-adventerprisek9-ms.bin",
"version": "17.12.1",
@ -44,6 +50,12 @@
}
],
"versions": [
{
"name": "17.15.1",
"images": {
"image": "x86_64_crb_linux-adventerprisek9-ms.iol"
}
},
{
"name": "17.12.1",
"images": {

View File

@ -595,7 +595,7 @@ class Project:
if node_type == "iou":
async with self._iou_id_lock:
# wait for a IOU node to be completely created before adding a new one
# wait for an IOU node to be completely created before adding a new one
# this is important otherwise we allocate the same application ID (used
# to generate MAC addresses) when creating multiple IOU node at the same time
if "properties" in kwargs.keys():
@ -1352,7 +1352,10 @@ class Project:
p_work = pathlib.Path(self.path).parent.absolute()
t0 = time.time()
new_project_id = str(uuid.uuid4())
new_project_path = p_work.joinpath(new_project_id)
if location:
new_project_path = p_work.joinpath(location)
else:
new_project_path = p_work.joinpath(new_project_id)
# copy dir
await wait_run_in_executor(shutil.copytree, self.path, new_project_path.as_posix(), symlinks=True, ignore_dangling_symlinks=True)
log.info("Project content copied from '{}' to '{}' in {}s".format(self.path, new_project_path, time.time() - t0))

View File

@ -62,42 +62,53 @@ async def list_images(image_type):
directory = os.path.normpath(directory)
for root, _, filenames in _os_walk(directory, recurse=recurse):
for filename in filenames:
if filename not in files:
if filename.endswith(".md5sum") or filename.startswith("."):
if filename in files:
log.debug("File {} has already been found, skipping...".format(filename))
continue
if filename.endswith(".md5sum") or filename.startswith("."):
continue
files.add(filename)
filesize = os.stat(os.path.join(root, filename)).st_size
if filesize < 7:
log.debug("File {} is too small to be an image, skipping...".format(filename))
continue
try:
with open(os.path.join(root, filename), "rb") as f:
# read the first 7 bytes of the file.
elf_header_start = f.read(7)
if image_type == "dynamips" and elf_header_start != b'\x7fELF\x01\x02\x01':
# IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1
log.warning("IOS image {} does not start with a valid ELF magic number, skipping...".format(filename))
continue
elif image_type == "iou" and elf_header_start != b'\x7fELF\x02\x01\x01' and elf_header_start != b'\x7fELF\x01\x01\x01':
# IOU images must start with the ELF magic number, be 32-bit or 64-bit, little endian and have an ELF version of 1
log.warning("IOU image {} does not start with a valid ELF magic number, skipping...".format(filename))
continue
elif image_type == "qemu" and elf_header_start[:4] == b'\x7fELF':
# QEMU images should not start with an ELF magic number
log.warning("QEMU image {} starts with an ELF magic number, skipping...".format(filename))
continue
elif (
((filename.endswith(".image") or filename.endswith(".bin")) and image_type == "dynamips")
or ((filename.endswith(".bin") or filename.startswith("i86bi")) and image_type == "iou")
or (not filename.endswith(".bin") and not filename.endswith(".image") and image_type == "qemu")
):
files.add(filename)
# It the image is located in the standard directory the path is relative
if os.path.commonprefix([root, default_directory]) != default_directory:
path = os.path.join(root, filename)
else:
path = os.path.relpath(os.path.join(root, filename), default_directory)
# It the image is located in the standard directory the path is relative
if os.path.commonprefix([root, default_directory]) != default_directory:
path = os.path.join(root, filename)
else:
path = os.path.relpath(os.path.join(root, filename), default_directory)
try:
if image_type in ["dynamips", "iou"]:
with open(os.path.join(root, filename), "rb") as f:
# read the first 7 bytes of the file.
elf_header_start = f.read(7)
# valid IOU or IOS images must start with the ELF magic number, be 32-bit or 64-bit,
# little endian and have an ELF version of 1
if elf_header_start != b'\x7fELF\x02\x01\x01' and elf_header_start != b'\x7fELF\x01\x01\x01':
continue
images.append(
{
"filename": filename,
"path": force_unix_path(path),
"md5sum": await wait_run_in_executor(md5sum, os.path.join(root, filename)),
"filesize": os.stat(os.path.join(root, filename)).st_size,
images.append(
{
"filename": filename,
"path": force_unix_path(path),
"md5sum": await wait_run_in_executor(md5sum, os.path.join(root, filename)),
"filesize": filesize,
}
)
except OSError as e:
log.warning(f"Can't add image {path}: {str(e)}")
except OSError as e:
log.warning(f"Can't add image {path}: {str(e)}")
return images
@ -155,6 +166,23 @@ async def discover_images(image_type: str, skip_image_paths: list = None) -> Lis
except InvalidImageError as e:
log.debug(str(e))
continue
# It the image is located in the standard directory the path is relative
if os.path.commonprefix([root, default_directory]) != default_directory:
path = os.path.join(root, filename)
else:
path = os.path.relpath(os.path.join(root, filename), default_directory)
images.append(
{
"filename": filename,
"path": force_unix_path(path),
"md5sum": md5sum(os.path.join(root, filename)),
"filesize": filesize
}
)
except OSError as e:
log.warning("Can't add image {}: {}".format(path, str(e)))
return images

View File

@ -146,7 +146,7 @@ def fake_image(tmpdir) -> str:
path = str(tmpdir / "7200.bin")
with open(path, "wb+") as f:
f.write(b'\x7fELF\x01\x01\x01')
f.write(b'\x7fELF\x01\x02\x01')
os.chmod(path, stat.S_IREAD)
return path
@ -170,7 +170,7 @@ async def test_images(app: FastAPI, compute_client: AsyncClient, tmpdir, fake_im
assert response.json() == [{"filename": "7200.bin",
"path": "7200.bin",
"filesize": 7,
"md5sum": "e573e8f5c93c6c00783f20c7a170aa6c"}]
"md5sum": "b0d5aa897d937aced5a6b1046e8f7e2e"}]
async def test_upload_image(app: FastAPI, compute_client: AsyncClient, images_dir: str) -> None:

View File

@ -217,12 +217,12 @@ async def test_list_images(qemu, tmpdir):
os.makedirs(tmp_images_dir, exist_ok=True)
for image in fake_images:
with open(os.path.join(tmp_images_dir, image), "w+") as f:
f.write("1")
f.write("1234567")
with patch("gns3server.utils.images.default_images_directory", return_value=str(tmp_images_dir)):
assert sorted(await qemu.list_images(), key=lambda k: k['filename']) == [
{"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1},
{"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1}
{"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7},
{"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7}
]
@ -234,19 +234,19 @@ async def test_list_images_recursives(qemu, tmpdir):
fake_images = ["a.qcow2", "b.qcow2", ".blu.qcow2", "a.qcow2.md5sum"]
for image in fake_images:
with open(os.path.join(tmp_images_dir, image), "w+") as f:
f.write("1")
f.write("1234567")
os.makedirs(os.path.join(tmp_images_dir, "c"))
fake_images = ["c.qcow2", "c.qcow2.md5sum"]
for image in fake_images:
with open(os.path.join(tmp_images_dir, "c", image), "w+") as f:
f.write("1")
f.write("1234567")
with patch("gns3server.utils.images.default_images_directory", return_value=str(tmp_images_dir)):
assert sorted(await qemu.list_images(), key=lambda k: k['filename']) == [
{"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1},
{"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1},
{"filename": "c.qcow2", "path": force_unix_path(os.path.sep.join(["c", "c.qcow2"])), "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1}
{"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7},
{"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7},
{"filename": "c.qcow2", "path": force_unix_path(os.path.sep.join(["c", "c.qcow2"])), "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7}
]

View File

@ -114,64 +114,89 @@ def test_remove_checksum(tmpdir):
@pytest.mark.asyncio
async def test_list_images(tmpdir, config):
path1 = tmpdir / "images1" / "IOS" / "test1.image"
path1.write(b'\x7fELF\x01\x01\x01', ensure=True)
path1 = force_unix_path(str(path1))
# IOS image in the images directory
ios_image_1 = tmpdir / "images1" / "IOS" / "ios_image_1.image"
ios_image_1.write(b'\x7fELF\x01\x02\x01', ensure=True)
ios_image_1 = force_unix_path(str(ios_image_1))
path2 = tmpdir / "images2" / "test2.image"
path2.write(b'\x7fELF\x01\x01\x01', ensure=True)
path2 = force_unix_path(str(path2))
# IOS image in an additional images path
ios_image_2 = tmpdir / "images2" / "ios_image_2.image"
ios_image_2.write(b'\x7fELF\x01\x02\x01', ensure=True)
ios_image_2 = force_unix_path(str(ios_image_2))
# Invalid image because not a valid elf file
path = tmpdir / "images2" / "test_invalid.image"
path.write(b'NOTANELF', ensure=True)
# Not a valid elf file
not_elf_file = tmpdir / "images1" / "IOS" / "not_elf.image"
not_elf_file.write(b'NOTANELF', ensure=True)
not_elf_file = force_unix_path(str(not_elf_file))
# Invalid image because it is very small
small_file = tmpdir / "images1" / "too_small.image"
small_file.write(b'1', ensure=True)
if sys.platform.startswith("linux"):
path3 = tmpdir / "images1" / "IOU" / "test3.bin"
path3.write(b'\x7fELF\x02\x01\x01', ensure=True)
path3 = force_unix_path(str(path3))
# 64-bit IOU image
iou_image_1 = tmpdir / "images1" / "IOU" / "iou64.bin"
iou_image_1.write(b'\x7fELF\x02\x01\x01', ensure=True)
iou_image_1 = force_unix_path(str(iou_image_1))
# 32-bit IOU image
iou_image_2 = tmpdir / "images1" / "IOU" / "iou32.bin"
iou_image_2.write(b'\x7fELF\x01\x01\x01', ensure=True) # 32-bit IOU image
iou_image_2 = force_unix_path(str(iou_image_2))
path4 = tmpdir / "images1" / "QEMU" / "test4.qcow2"
path4.write("1", ensure=True)
path4 = force_unix_path(str(path4))
path5 = tmpdir / "images1" / "QEMU" / "test4.qcow2.md5sum"
path5.write("1", ensure=True)
path5 = force_unix_path(str(path5))
# Qemu image
qemu_image_1 = tmpdir / "images1" / "QEMU" / "qemu_image.qcow2"
qemu_image_1.write("1234567", ensure=True)
qemu_image_1 = force_unix_path(str(qemu_image_1))
# ELF file inside the Qemu
elf_file = tmpdir / "images1" / "QEMU" / "elf_file.bin"
elf_file.write(b'\x7fELF\x02\x01\x01', ensure=True) # ELF file
elf_file = force_unix_path(str(elf_file))
md5sum_file = tmpdir / "images1" / "QEMU" / "image.qcow2.md5sum"
md5sum_file.write("1", ensure=True)
md5sum_file = force_unix_path(str(md5sum_file))
config.settings.Server.images_path = str(tmpdir / "images1")
config.settings.Server.additional_images_paths = "/tmp/null24564;" + str(tmpdir / "images2")
assert await list_images("dynamips") == [
assert list_images("dynamips") == [
{
'filename': 'test1.image',
'filename': 'ios_image_1.image',
'filesize': 7,
'md5sum': 'e573e8f5c93c6c00783f20c7a170aa6c',
'path': 'test1.image'
'md5sum': 'b0d5aa897d937aced5a6b1046e8f7e2e',
'path': 'ios_image_1.image'
},
{
'filename': 'test2.image',
'filename': 'ios_image_2.image',
'filesize': 7,
'md5sum': 'e573e8f5c93c6c00783f20c7a170aa6c',
'path': str(path2)
'md5sum': 'b0d5aa897d937aced5a6b1046e8f7e2e',
'path': str(ios_image_2)
}
]
if sys.platform.startswith("linux"):
assert await list_images("iou") == [
assert list_images("iou") == [
{
'filename': 'test3.bin',
'filename': 'iou64.bin',
'filesize': 7,
'md5sum': 'c73626d23469519894d58bc98bee9655',
'path': 'test3.bin'
'path': 'iou64.bin'
},
{
'filename': 'iou32.bin',
'filesize': 7,
'md5sum': 'e573e8f5c93c6c00783f20c7a170aa6c',
'path': 'iou32.bin'
}
]
assert await list_images("qemu") == [
assert list_images("qemu") == [
{
'filename': 'test4.qcow2',
'filesize': 1,
'md5sum': 'c4ca4238a0b923820dcc509a6f75849b',
'path': 'test4.qcow2'
'filename': 'qemu_image.qcow2',
'filesize': 7,
'md5sum': 'fcea920f7412b5da7be0cf42b8c93759',
'path': 'qemu_image.qcow2'
}
]