mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
fix(qemu): Fixed missing image replacement for linked-clone nodes
This commit is contained in:
1 parent
59a367b88d
commit
da16dc9874
7 files changed
+308
-27
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
Reference in new issue
Block a user