Merge origin/3.1 into image-manager-out-of-sync

Preserve image reconciliation, locking, and regression coverage while integrating upstream release, dependency, typing, and formatting updates. Align feature code with the new Ruff checks.
This commit is contained in:
Cristi committed 2026-09-28 23:16:24 +03:00
commit 42d30651da
497 files changed
+22962 -24152

No files matched your search

+82 -97
View File
@@ -43,7 +43,7 @@ def iou_32_bit_image(tmpdir) -> str:
path = os.path.join(tmpdir, "iou_32bit.bin")
with open(path, "wb+") as f:
f.write(b'\x7fELF\x01\x01\x01')
f.write(b"\x7fELF\x01\x01\x01")
return path
@@ -55,7 +55,7 @@ def iou_64_bit_image(tmpdir) -> str:
path = os.path.join(tmpdir, "iou_64bit.bin")
with open(path, "wb+") as f:
f.write(b'\x7fELF\x02\x01\x01')
f.write(b"\x7fELF\x02\x01\x01")
return path
@@ -67,7 +67,7 @@ def ios_image(tmpdir) -> str:
path = os.path.join(tmpdir, "ios_image.bin")
with open(path, "wb+") as f:
f.write(b'\x7fELF\x01\x02\x01')
f.write(b"\x7fELF\x01\x02\x01")
return path
@@ -79,7 +79,7 @@ def qcow2_image(tmpdir) -> str:
path = os.path.join(tmpdir, "image.qcow2")
with open(path, "wb+") as f:
f.write(b'QFI\xfb\x00\x00\x00')
f.write(b"QFI\xfb\x00\x00\x00")
return path
@@ -91,7 +91,7 @@ def invalid_image(tmpdir) -> str:
path = os.path.join(tmpdir, "invalid_image.bin")
with open(path, "wb+") as f:
f.write(b'\x01\x01\x01\x01')
f.write(b"\x01\x01\x01\x01")
return path
@@ -103,23 +103,23 @@ def empty_image(tmpdir) -> str:
path = os.path.join(tmpdir, "empty_image.bin")
with open(path, "wb+") as f:
f.write(b'')
f.write(b"")
return path
class TestImageRoutes:
async def test_create_image(self, app: FastAPI, client: AsyncClient, images_dir) -> None:
async def create_disk(path, options):
with open(path, "wb") as f:
f.write(b'QFI\xfb\x00\x00\x00')
f.write(b"QFI\xfb\x00\x00\x00")
Qemu.instance().create_disk_image = AsyncioMagicMock(side_effect=create_disk)
path = os.path.join(os.path.join(images_dir, "QEMU", "new_image.qcow2"))
image_name = os.path.basename(path)
response = await client.post(
app.url_path_for("create_qemu_image", image_path=image_name), json={"format": "qcow2", "size": 30})
app.url_path_for("create_qemu_image", image_path=image_name), json={"format": "qcow2", "size": 30}
)
assert response.status_code == status.HTTP_201_CREATED
assert os.path.isfile(path)
@@ -132,18 +132,18 @@ class TestImageRoutes:
("ios", "ios_image", True),
("ios", "invalid_image", False),
("qemu", "qcow2_image", True),
("qemu", "empty_image", False)
("qemu", "empty_image", False),
),
)
async def test_upload_image(
self,
app: FastAPI,
client: AsyncClient,
images_dir: str,
image_type: str,
fixture_name: str,
valid_request: bool,
request
self,
app: FastAPI,
client: AsyncClient,
images_dir: str,
image_type: str,
fixture_name: str,
valid_request: bool,
request,
) -> None:
image_path = request.getfixturevalue(fixture_name)
@@ -153,9 +153,7 @@ class TestImageRoutes:
image_data = f.read()
image_checksum.update(image_data)
response = await client.post(
app.url_path_for("upload_image", image_path=image_name),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path=image_name), content=image_data)
if valid_request:
assert response.status_code == status.HTTP_201_CREATED
@@ -182,21 +180,15 @@ class TestImageRoutes:
with open(qcow2_image, "rb") as f:
image_data = f.read()
response = await client.post(
app.url_path_for("upload_image", image_path="image1.qcow2"),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path="image1.qcow2"), content=image_data)
assert response.status_code == status.HTTP_201_CREATED
# same image with same name is uploaded again, it should return 409 Conflict
response = await client.post(
app.url_path_for("upload_image", image_path="image1.qcow2"),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path="image1.qcow2"), content=image_data)
assert response.status_code == status.HTTP_409_CONFLICT
# same image with different name but same checksum is uploaded again, it should return 409 Conflict
response = await client.post(
app.url_path_for("upload_image", image_path="image2.qcow2"),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path="image2.qcow2"), content=image_data)
assert response.status_code == status.HTTP_409_CONFLICT
async def test_image_delete(self, app: FastAPI, client: AsyncClient, qcow2_image: str) -> None:
@@ -211,14 +203,14 @@ class TestImageRoutes:
response = await client.get(app.url_path_for("get_image", image_path=image_name))
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_image_deleted_on_disk(self, app: FastAPI, client: AsyncClient, images_dir: str, qcow2_image: str) -> None:
async def test_image_deleted_on_disk(
self, app: FastAPI, client: AsyncClient, images_dir: str, qcow2_image: str
) -> None:
image_name = os.path.basename(qcow2_image)
with open(qcow2_image, "rb") as f:
image_data = f.read()
response = await client.post(
app.url_path_for("upload_image", image_path=image_name),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path=image_name), content=image_data)
assert response.status_code == status.HTTP_201_CREATED
response = await client.delete(app.url_path_for("delete_image", image_path=image_name))
@@ -233,42 +225,30 @@ class TestImageRoutes:
),
)
async def test_upload_image_subdir(
self,
app: FastAPI,
client: AsyncClient,
images_dir: str,
qcow2_image: str,
subdir: str,
expected_result: int,
db_session: AsyncSession
self,
app: FastAPI,
client: AsyncClient,
images_dir: str,
qcow2_image: str,
subdir: str,
expected_result: int,
db_session: AsyncSession,
) -> None:
image_name = os.path.basename(qcow2_image)
with open(qcow2_image, "rb") as f:
image_data = f.read()
image_path = os.path.join(subdir, image_name)
response = await client.post(
app.url_path_for("upload_image", image_path=image_path),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path=image_path), content=image_data)
assert response.status_code == expected_result
async def test_image_delete_multiple_match(
self,
app: FastAPI,
client: AsyncClient,
qcow2_image: str
) -> None:
async def test_image_delete_multiple_match(self, app: FastAPI, client: AsyncClient, qcow2_image: str) -> None:
image_name = os.path.basename(qcow2_image)
response = await client.delete(app.url_path_for("delete_image", image_path=image_name))
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_image_delete_with_subdir(
self,
app: FastAPI,
client: AsyncClient,
qcow2_image: str
) -> None:
async def test_image_delete_with_subdir(self, app: FastAPI, client: AsyncClient, qcow2_image: str) -> None:
image_name = os.path.basename(qcow2_image)
image_path = os.path.join("subdir", image_name)
@@ -305,17 +285,20 @@ class TestImageRoutes:
with open(os.path.join(project_dir, f"{name}.gns3"), "w+") as f:
json.dump(topology, f)
return await controller.add_project(
project_id=project_id, name=name, path=project_dir,
filename=f"{name}.gns3", status="closed",
project_id=project_id,
name=name,
path=project_dir,
filename=f"{name}.gns3",
status="closed",
)
async def test_image_delete_used_by_project(
self,
app: FastAPI,
client: AsyncClient,
controller: Controller,
db_session: AsyncSession,
tmpdir: str,
self,
app: FastAPI,
client: AsyncClient,
controller: Controller,
db_session: AsyncSession,
tmpdir: str,
) -> None:
"""
An image referenced by a node in a project must not be deleted,
@@ -324,15 +307,21 @@ class TestImageRoutes:
image_path = os.path.join(tmpdir, "used.qcow2")
with open(image_path, "wb+") as f:
f.write(b'\x42\x42\x42\x42')
f.write(b"\x42\x42\x42\x42")
images_repo = ImagesRepository(db_session)
await images_repo.add_image("used.qcow2", "qemu", 42, image_path, "e342eb86c1229b6c154367a5476969b5", "md5")
guarded_project = await self._add_closed_project_with_node(
controller, "Guarded",
{"node_id": str(uuid.uuid4()), "node_type": "qemu", "compute_id": "local",
"name": "n1", "properties": {"hda_disk_image_backing_file": "used.qcow2"}},
controller,
"Guarded",
{
"node_id": str(uuid.uuid4()),
"node_type": "qemu",
"compute_id": "local",
"name": "n1",
"properties": {"hda_disk_image_backing_file": "used.qcow2"},
},
)
response = await client.delete(app.url_path_for("delete_image", image_path="used.qcow2"))
@@ -347,12 +336,12 @@ class TestImageRoutes:
assert not os.path.exists(image_path)
async def test_prune_images_keeps_project_referenced(
self,
app: FastAPI,
client: AsyncClient,
controller: Controller,
db_session: AsyncSession,
tmpdir: str,
self,
app: FastAPI,
client: AsyncClient,
controller: Controller,
db_session: AsyncSession,
tmpdir: str,
) -> None:
"""
Pruning must keep images referenced by a project node while
@@ -363,13 +352,19 @@ class TestImageRoutes:
for filename in ("used.qcow2", "unused.qcow2"):
image_path = os.path.join(tmpdir, filename)
with open(image_path, "wb+") as f:
f.write(b'\x42\x42\x42\x42')
f.write(b"\x42\x42\x42\x42")
await images_repo.add_image(filename, "qemu", 42, image_path, "e342eb86c1229b6c154367a5476969b5", "md5")
await self._add_closed_project_with_node(
controller, "Guarded",
{"node_id": str(uuid.uuid4()), "node_type": "qemu", "compute_id": "local",
"name": "n1", "properties": {"hda_disk_image_backing_file": "used.qcow2"}},
controller,
"Guarded",
{
"node_id": str(uuid.uuid4()),
"node_type": "qemu",
"compute_id": "local",
"name": "n1",
"properties": {"hda_disk_image_backing_file": "used.qcow2"},
},
)
response = await client.delete(app.url_path_for("prune_images"))
@@ -381,10 +376,7 @@ class TestImageRoutes:
assert not os.path.exists(os.path.join(tmpdir, "unused.qcow2"))
async def test_image_upload_create_appliance(
self, app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
controller: Controller
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, controller: Controller
) -> None:
await controller.appliance_manager.install_builtin_appliances()
@@ -396,7 +388,8 @@ class TestImageRoutes:
response = await client.post(
app.url_path_for("upload_image", image_path=image_name),
params={"install_appliances": "true"},
content=image_data)
content=image_data,
)
assert response.status_code == status.HTTP_201_CREATED
templates_repo = TemplatesRepository(db_session)
@@ -407,19 +400,14 @@ class TestImageRoutes:
await templates_repo.delete_template(templates[0].template_id)
async def test_install_all(
self, app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
controller: Controller
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, controller: Controller
) -> None:
image_path = "tests/resources/empty100G.qcow2"
image_name = os.path.basename(image_path)
with open(image_path, "rb") as f:
image_data = f.read()
response = await client.post(
app.url_path_for("upload_image", image_path=image_name),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path=image_name), content=image_data)
assert response.status_code == status.HTTP_201_CREATED
controller.appliance_manager.load_appliances() # make sure appliances are loaded
@@ -440,10 +428,7 @@ class TestImageRoutes:
await templates_repo.delete_template(templates[0].template_id)
async def test_install_all_skips_existing_template_name(
self, app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
controller: Controller
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, controller: Controller
) -> None:
# two images matching two versions of the same appliance must not
# produce two templates with the same name
@@ -458,8 +443,8 @@ class TestImageRoutes:
with open(image_path, "rb") as f:
image_data = f.read()
response = await client.post(
app.url_path_for("upload_image", image_path=os.path.basename(image_path)),
content=image_data)
app.url_path_for("upload_image", image_path=os.path.basename(image_path)), content=image_data
)
assert response.status_code == status.HTTP_201_CREATED
controller.appliance_manager.load_appliances() # make sure appliances are loaded