fix(qemu): Fixed missing image replacement for linked-clone nodes

This commit is contained in:
Cristi committed 2026-09-18 23:58:49 +03:00
1 parent 59a367b88d
commit da16dc9874
7 files changed
+308 -27

No files matched your search

+123
View File
@@ -20,6 +20,7 @@ import pytest_asyncio
import os
import stat
import shutil
import uuid
from fastapi import FastAPI, status
from httpx import AsyncClient
@@ -144,6 +145,128 @@ class TestQemuNodesRoutes:
assert response.json()["project_id"] == compute_project.id
assert response.json()["ram"] == 1024
assert response.json()["hda_disk_image"] == "linux载.img"
async def test_qemu_create_discards_stale_overlay_for_replacement(
self,
app: FastAPI,
compute_client: AsyncClient,
compute_project: Project,
base_params: dict,
fake_qemu_vm: str
):
node_id = str(uuid.uuid4())
working_dir = os.path.join(compute_project.path, "project-files", "qemu", node_id)
os.makedirs(working_dir)
stale_overlay = os.path.join(working_dir, "hda_disk.qcow2")
with open(stale_overlay, "w+") as f:
f.write("stale linked clone")
stale_checksum = stale_overlay + ".md5sum"
with open(stale_checksum, "w+") as f:
f.write("0" * 32)
params = {
**base_params,
"node_id": node_id,
"linked_clone": True,
"hda_disk_image": "linux载.img",
"disk_images_to_reset": ["hda_disk_image"],
}
response = await compute_client.post(
app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=params
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["hda_disk_image"] == "linux载.img"
assert not os.path.exists(stale_overlay)
assert not os.path.exists(stale_checksum)
async def test_qemu_create_preserves_existing_overlay_without_reset_marker(
self,
app: FastAPI,
compute_client: AsyncClient,
compute_project: Project,
base_params: dict,
fake_qemu_vm: str
):
node_id = str(uuid.uuid4())
working_dir = os.path.join(compute_project.path, "project-files", "qemu", node_id)
os.makedirs(working_dir)
overlay = os.path.join(working_dir, "hda_disk.qcow2")
shutil.copy("tests/resources/empty8G.qcow2", overlay)
params = {
**base_params,
"node_id": node_id,
"linked_clone": True,
"hda_disk_image": "linux载.img",
}
response = await compute_client.post(
app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=params
)
assert response.status_code == status.HTTP_201_CREATED
assert os.path.exists(overlay)
async def test_qemu_create_preserves_overlay_when_replacement_is_missing(
self,
app: FastAPI,
compute_client: AsyncClient,
compute_project: Project,
base_params: dict,
fake_qemu_vm: str
):
node_id = str(uuid.uuid4())
working_dir = os.path.join(compute_project.path, "project-files", "qemu", node_id)
os.makedirs(working_dir)
overlay = os.path.join(working_dir, "hda_disk.qcow2")
shutil.copy("tests/resources/empty8G.qcow2", overlay)
params = {
**base_params,
"node_id": node_id,
"linked_clone": True,
"hda_disk_image": "does-not-exist.qcow2",
"disk_images_to_reset": ["hda_disk_image"],
}
response = await compute_client.post(
app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=params
)
assert response.status_code == status.HTTP_409_CONFLICT
assert os.path.exists(overlay)
async def test_qemu_create_resets_valid_replacement_when_another_disk_is_missing(
self,
app: FastAPI,
compute_client: AsyncClient,
compute_project: Project,
base_params: dict,
fake_qemu_vm: str
):
node_id = str(uuid.uuid4())
working_dir = os.path.join(compute_project.path, "project-files", "qemu", node_id)
os.makedirs(working_dir)
stale_overlay = os.path.join(working_dir, "hda_disk.qcow2")
shutil.copy("tests/resources/empty8G.qcow2", stale_overlay)
params = {
**base_params,
"node_id": node_id,
"linked_clone": True,
"hda_disk_image": "linux载.img",
"hdb_disk_image": "does-not-exist.qcow2",
"disk_images_to_reset": ["hda_disk_image"],
}
response = await compute_client.post(
app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=params
)
assert response.status_code == status.HTTP_409_CONFLICT
assert not os.path.exists(stale_overlay)
@pytest.mark.parametrize(
+13
View File
@@ -806,6 +806,19 @@ def test_hda_disk_image(vm, images_dir):
assert vm.hda_disk_image == force_unix_path(os.path.join(images_dir, "QEMU", "test2"))
def test_delete_disk_image_removes_cached_checksum(vm):
disk_path = os.path.join(vm.working_dir, "hda_disk.qcow2")
checksum_path = disk_path + ".md5sum"
open(disk_path, "w+").close()
open(checksum_path, "w+").close()
vm.delete_disk_image("hda_disk.qcow2")
assert not os.path.exists(disk_path)
assert not os.path.exists(checksum_path)
@pytest.mark.asyncio
async def test_hda_disk_image_non_linked_clone(vm, images_dir, compute_project, manager, fake_qemu_binary):
"""
+55
View File
@@ -584,6 +584,45 @@ async def test_update_recreates_missing_image_node(project, compute, node):
assert project.restore_deferred_links.called is True
@pytest.mark.asyncio
async def test_update_replaces_qemu_linked_clone_backing_image(project, compute):
"""Stale linked-clone metadata must not override a replacement image."""
qemu_node = Node(
project,
compute,
"old-qemu",
node_id=str(uuid.uuid4()),
node_type="qemu",
properties={
"hda_disk_image": "hda_disk.qcow2",
"hda_disk_image_backing_file": "missing.qcow2",
"hda_disk_image_md5sum": "old-checksum",
},
)
qemu_node._missing_images = [
{"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"}
]
response = MagicMock()
response.json = {"console": 2048}
compute.post = AsyncioMagicMock(return_value=response)
project.restore_deferred_links = AsyncioMagicMock()
await qemu_node.update(
properties={
**qemu_node.properties,
"hda_disk_image": "replacement.qcow2",
}
)
request_data = compute.post.call_args.kwargs["data"]
assert request_data["hda_disk_image"] == "replacement.qcow2"
assert request_data["disk_images_to_reset"] == ["hda_disk_image"]
assert "hda_disk_image_backing_file" not in request_data
assert "hda_disk_image_md5sum" not in request_data
assert qemu_node.missing_image is False
@pytest.mark.asyncio
async def test_update_missing_image_node_rolls_back_after_create_error(project, compute, node):
original_properties = {"image": "missing.image"}
@@ -1005,6 +1044,22 @@ async def test_upload_missing_image(compute, controller, images_dir):
compute.post.assert_called_with("/qemu/images/linux.img", data=ANY, timeout=None)
@pytest.mark.asyncio
async def test_upload_missing_image_from_nested_directory(compute, controller, images_dir):
project = Project(str(uuid.uuid4()), controller=controller)
node = Node(project, compute, "demo",
node_id=str(uuid.uuid4()),
node_type="qemu",
properties={"hda_disk_image": "nested.img"})
nested_dir = os.path.join(images_dir, "vendor")
os.makedirs(nested_dir)
open(os.path.join(nested_dir, "nested.img"), "w+").close()
assert await node._upload_missing_image("qemu", "nested.img") is True
compute.post.assert_called_with("/qemu/images/nested.img", data=ANY, timeout=None)
@pytest.mark.asyncio
async def test_sync_missing_docker_image_from_controller_daemon(compute, controller):