The success paths were fine; several failure paths drew a confident conclusion without checking the thing they waited for. Registry (was: any parse error killed the whole server, since load() runs in the lifespan): - quarantine an unreadable file and start empty instead of raising, so the tools that stop runaway VMs keep working when bookkeeping is damaged - skip malformed or invalidly-named records rather than failing the load; report both through list_vms as registry_warnings - read-modify-write under an exclusive flock so a second instance merges instead of clobbering, with a PID-unique temp file - drop the lifespan shutdown save, which could resurrect deleted records - version the schema and round-trip unknown record fields sandbox_destroy (the only tool that deletes files): - verify the process actually died, escalating to SIGKILL, and refuse to delete an overlay QEMU still holds open - assert the target is inside the VM state tree before rmtree - report cleanup errors instead of swallowing them; destroyed now reflects what happened Launch races: - reserve the name before the first await so two concurrent launches cannot race over one set of sockets - refuse to unlink a QMP socket that is still accepting connections - register the VM with a warning rather than orphaning it when the pidfile is unreadable but QEMU is up Guest agent and QMP: - bound every guest-agent call, not just the handshake; cap max_bytes and stop guest_file_read spinning on a zero-progress agent - serialize QMP sessions per VM (the monitor is single-client) and say "another operation holds it" instead of "the VM has likely exited" - poll liveness while waiting for SHUTDOWN so a crashed VM is reported as exited rather than as a guest ignoring ACPI - default command timeout, with a longer bound for savevm/loadvm - stricter snapshot tags; log destructive operations to stderr Adds tests/test_reliability.py covering the conditions above.
60 lines
2.3 KiB
Python
60 lines
2.3 KiB
Python
"""list_vms / vm_info status reporting."""
|
|
|
|
from fastmcp import Client
|
|
|
|
from conftest import FakeQMPClient, result_data, write_registry
|
|
from mcqemu.server import mcp
|
|
from test_lifecycle import seeded_record
|
|
|
|
|
|
async def test_list_vms_stopped(dirs, all_pids_dead):
|
|
write_registry(dirs, seeded_record(dirs, "vm1"))
|
|
async with Client(mcp) as client:
|
|
result = result_data(await client.call_tool("list_vms", {}))
|
|
assert result["vms"][0]["status"] == "stopped"
|
|
assert result["registry_warnings"] == []
|
|
|
|
|
|
async def test_list_vms_running(dirs, fake_qmp, all_pids_alive):
|
|
FakeQMPClient.responses["query-status"] = {"status": "running"}
|
|
write_registry(dirs, seeded_record(dirs, "vm1"))
|
|
async with Client(mcp) as client:
|
|
result = result_data(await client.call_tool("list_vms", {}))
|
|
assert result["vms"][0]["status"] == "running"
|
|
|
|
|
|
async def test_list_vms_unreachable_when_qmp_fails(dirs, fake_qmp, all_pids_alive):
|
|
FakeQMPClient.connect_error = OSError("connection refused")
|
|
write_registry(dirs, seeded_record(dirs, "vm1"))
|
|
async with Client(mcp) as client:
|
|
result = result_data(await client.call_tool("list_vms", {}))
|
|
assert result["vms"][0]["status"] == "unreachable"
|
|
|
|
|
|
async def test_vm_info_running_includes_block_devices(dirs, fake_qmp, all_pids_alive):
|
|
FakeQMPClient.responses["query-status"] = {"status": "running"}
|
|
FakeQMPClient.responses["query-cpus-fast"] = [{"cpu-index": 0}, {"cpu-index": 1}]
|
|
FakeQMPClient.responses["query-block"] = [
|
|
{
|
|
"device": "virtio0",
|
|
"inserted": {"file": "/vms/a.qcow2", "drv": "qcow2", "ro": False},
|
|
},
|
|
{"device": "empty-cd"},
|
|
]
|
|
write_registry(dirs, seeded_record(dirs, "vm1"))
|
|
async with Client(mcp) as client:
|
|
info = result_data(await client.call_tool("vm_info", {"name": "vm1"}))
|
|
assert info["status"] == "running"
|
|
assert info["vcpus"] == 2
|
|
assert info["block_devices"] == [
|
|
{"device": "virtio0", "file": "/vms/a.qcow2", "format": "qcow2", "read_only": False}
|
|
]
|
|
|
|
|
|
async def test_vm_info_stopped_skips_qmp(dirs, all_pids_dead):
|
|
write_registry(dirs, seeded_record(dirs, "vm1"))
|
|
async with Client(mcp) as client:
|
|
info = result_data(await client.call_tool("vm_info", {"name": "vm1"}))
|
|
assert info["status"] == "stopped"
|
|
assert "vcpus" not in info
|