1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00

fix(instances): a VM whose guest agent hangs no longer stops its page

The VM page read the guest's addresses with interfaceAddresses() from
the agent, and the OS details with agent commands without a timeout.
A connected agent that hangs (a frozen guest) kept the page from
loading at all and held a worker: seen still loading after 120 s.

Both now go through the 10 s guest agent command the root password and
SSH key use. The addresses come from guest-network-get-interfaces, in
the form the page's address code already reads; an agent that does not
answer leaves the ARP address. With the agent frozen the page loads in
about 10 s and the OS details answer with the agent's error (400) in
5 s, instead of a server error.
This commit is contained in:
catborise 2026-10-25 15:48:22 +03:00
parent 75fcc6f6c0
commit 400b19cf42
4 changed files with 88 additions and 60 deletions

View file

@ -183,3 +183,21 @@ class GuestAgentAccessTestCase(TestCase):
self.proxy.set_user_password.side_effect = err
self.assertEqual(self.post("rootpasswd", {"passwd": "S3cret-pass"}),
["The host of this virtual machine cannot be reached right now."])
class OsInfoErrorTestCase(TestCase):
"""The VM page asks for the guest's OS details; an agent that does not
answer in time is an error answer, not a server error."""
def setUp(self):
self.client.login(request=RequestFactory().get("/"), username="admin", password="admin")
compute = Compute.objects.create(name="osinfo-compute", hostname="192.0.2.1", type=1)
self.instance = Instance.objects.create(compute=compute, name="osinfo-vm",
uuid="22222222-3333-4444-5555-666666666666")
@patch.object(Instance, "proxy")
def test_a_silent_agent_is_an_error_answer(self, proxy):
proxy.osinfo.side_effect = libvirtError("guest agent didn't respond to command within '10' seconds")
response = self.client.get(reverse("instances:osinfo", args=[self.instance.id]))
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {"error": "guest agent didn't respond to command within '10' seconds"})

View file

@ -420,9 +420,10 @@ def stats(request, pk):
def osinfo(request, pk):
instance = get_instance(request.user, pk)
results = instance.proxy.osinfo()
return JsonResponse(results)
try:
return JsonResponse(instance.proxy.osinfo())
except libvirtError as err:
return JsonResponse({"error": error_text(request, err)}, status=400)
@superuser_only

View file

@ -15,7 +15,6 @@ try:
from libvirt import (
VIR_DOMAIN_AFFECT_CONFIG,
VIR_DOMAIN_AFFECT_LIVE,
VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT,
VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP,
VIR_DOMAIN_XML_SECURE,
VIR_DOMAIN_XML_INACTIVE,
@ -51,7 +50,7 @@ try:
VIR_ERR_NO_DOMAIN_BACKUP,
libvirtError,
)
from libvirt_qemu import VIR_DOMAIN_QEMU_AGENT_COMMAND_DEFAULT, qemuAgentCommand
from libvirt_qemu import qemuAgentCommand
except Exception:
from libvirt import libvirtError, VIR_DOMAIN_XML_SECURE, VIR_DOMAIN_XML_INACTIVE, VIR_MIGRATE_LIVE
@ -375,36 +374,10 @@ class wvmInstance(wvmConnect):
self.instance = self.wvm.lookupByUUIDString(uuid) if uuid else self.get_instance(vname)
def osinfo(self):
info_results = qemuAgentCommand(
self.instance,
'{"execute":"guest-get-osinfo"}',
VIR_DOMAIN_QEMU_AGENT_COMMAND_DEFAULT,
0,
)
timezone_results = qemuAgentCommand(
self.instance,
'{"execute":"guest-get-timezone"}',
VIR_DOMAIN_QEMU_AGENT_COMMAND_DEFAULT,
0,
)
hostname_results = qemuAgentCommand(
self.instance,
'{"execute":"guest-get-host-name"}',
VIR_DOMAIN_QEMU_AGENT_COMMAND_DEFAULT,
0,
)
info_results = json.loads(info_results).get("return")
timezone_results = json.loads(timezone_results).get("return")
hostname_results = json.loads(hostname_results).get("return")
info_results.update(timezone_results)
info_results.update(hostname_results)
return info_results
info = self._agent_command("guest-get-osinfo")
info.update(self._agent_command("guest-get-timezone"))
info.update(self._agent_command("guest-get-host-name"))
return info
def start(self, flags=0):
self.instance.createWithFlags(flags)
@ -602,6 +575,18 @@ class wvmInstance(wvmConnect):
return self.instance.interfaceAddresses(source)
return {}
def _agent_interfaces(self):
"""The guest's interfaces from its agent, in interfaceAddresses()'s
form, which waits for a hung agent indefinitely."""
interfaces = {}
for iface in self._agent_command("guest-network-get-interfaces"):
addrs = []
for address in iface.get("ip-addresses", []):
kind = 0 if address["ip-address-type"] == "ipv4" else 1
addrs.append({"type": kind, "addr": address["ip-address"], "prefix": address["prefix"]})
interfaces[iface["name"]] = {"hwaddr": iface.get("hardware-address"), "addrs": addrs}
return interfaces
def refresh_interface_addresses(self):
self._ip_cache = {"qemuga": {}, "arp": {}}
@ -609,9 +594,8 @@ class wvmInstance(wvmConnect):
return
if self.is_agent_ready():
self._ip_cache["qemuga"] = self._get_interface_addresses(
VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT
)
with contextlib.suppress(libvirtError):
self._ip_cache["qemuga"] = self._agent_interfaces()
self._ip_cache["arp"] = self._get_interface_addresses(VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP)
@ -2525,12 +2509,15 @@ class wvmInstance(wvmConnect):
return util.get_xml_path(self._XMLDesc(0), func=_get_agent)
def _agent_command(self, command, arguments):
"""One guest agent command that waits AGENT_TIMEOUT seconds at most:
libvirt's own calls for it wait for a hung agent indefinitely, and
the domain's agent timeout is shared by every agent call."""
request = json.dumps({"execute": command, "arguments": arguments})
qemuAgentCommand(self.instance, request, AGENT_TIMEOUT, 0)
def _agent_command(self, command, arguments=None):
"""The answer of one guest agent command that waits AGENT_TIMEOUT
seconds at most: libvirt's own calls for it wait for a hung agent
indefinitely, and the domain's agent timeout is shared by every agent
call."""
request = {"execute": command}
if arguments is not None:
request["arguments"] = arguments
return json.loads(qemuAgentCommand(self.instance, json.dumps(request), AGENT_TIMEOUT, 0))["return"]
def set_user_password(self, user, crypted):
"""Set a user's password in the running guest through its agent;

View file

@ -1,6 +1,9 @@
"""A running VM's addresses come from the guest agent when it answers and
from the host's ARP table, so a VM without an agent shows its IPv4 address."""
from the host's ARP table, so a VM without an agent shows its IPv4 address.
The agent is asked with a timeout: interfaceAddresses() would wait for a
hung agent indefinitely, and the VM page with it."""
import json
import unittest
from unittest.mock import MagicMock, patch
@ -9,45 +12,64 @@ from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from libvirt import VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT, VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP
from libvirt import VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP, libvirtError
from vrtManager import instance as module
from vrtManager.instance import wvmInstance
MAC = "52:54:00:00:00:01"
ARP = {"vnet0": {"hwaddr": MAC, "addrs": [{"type": 0, "addr": "192.0.2.10", "prefix": 0}]}}
AGENT = {"eth0": {"hwaddr": MAC, "addrs": [{"type": 0, "addr": "192.0.2.20", "prefix": 24}]}}
# guest-network-get-interfaces: the loopback has no hardware address
AGENT = [{"name": "lo", "ip-addresses": [{"ip-address-type": "ipv4", "ip-address": "127.0.0.1", "prefix": 8}]},
{"name": "eth0", "hardware-address": MAC, "ip-addresses": [
{"ip-address-type": "ipv4", "ip-address": "192.0.2.20", "prefix": 24},
{"ip-address-type": "ipv6", "ip-address": "2001:db8::20", "prefix": 64}]}]
def vm(agent):
inst = wvmInstance.__new__(wvmInstance)
inst._ip_cache = None
inst.instance = MagicMock()
inst.instance.interfaceAddresses.side_effect = lambda source: {
VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT: AGENT,
VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP: ARP,
}[source]
inst.instance.interfaceAddresses.side_effect = lambda source: {VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP: ARP}[source]
patch.object(inst, "get_status", return_value=1).start()
patch.object(inst, "is_agent_ready", return_value=agent).start()
return inst
class InterfaceAddressesTestCase(unittest.TestCase):
def setUp(self):
self.agent = patch.object(module, "qemuAgentCommand", return_value=json.dumps({"return": AGENT})).start()
def tearDown(self):
patch.stopall()
def test_a_vm_without_an_agent_shows_its_arp_address(self):
inst = vm(agent=False)
self.assertEqual(inst.get_interface_addresses(MAC), (["192.0.2.10"], []))
self.assertEqual(
[c.args for c in inst.instance.interfaceAddresses.call_args_list], [(VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP,)]
)
self.agent.assert_not_called()
def test_the_agents_address_comes_first(self):
def test_the_agents_address_comes_first_and_is_asked_with_a_timeout(self):
inst = vm(agent=True)
self.assertEqual(inst.get_interface_addresses(MAC), (["192.0.2.20"], []))
self.assertEqual(
[c.args for c in inst.instance.interfaceAddresses.call_args_list],
[(VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT,), (VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP,)],
)
self.assertEqual(inst.get_interface_addresses(MAC), (["192.0.2.20"], ["2001:db8::20/64"]))
self.agent.assert_called_once_with(inst.instance, '{"execute": "guest-network-get-interfaces"}', 10, 0)
self.assertEqual([c.args for c in inst.instance.interfaceAddresses.call_args_list],
[(VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP,)])
def test_an_agent_that_does_not_answer_leaves_the_arp_address(self):
self.agent.side_effect = libvirtError("guest agent didn't respond to command within '10' seconds")
self.assertEqual(vm(agent=True).get_interface_addresses(MAC), (["192.0.2.10"], []))
class OsInfoTestCase(unittest.TestCase):
def test_the_os_details_are_asked_with_a_timeout(self):
answers = {"guest-get-osinfo": {"kernel-release": "6.12"}, "guest-get-timezone": {"zone": "UTC"},
"guest-get-host-name": {"host-name": "web"}}
inst = wvmInstance.__new__(wvmInstance)
inst.instance = MagicMock()
with patch.object(module, "qemuAgentCommand",
side_effect=lambda dom, cmd, timeout, flags: json.dumps(
{"return": answers[json.loads(cmd)["execute"]]})) as agent:
self.assertEqual(inst.osinfo(), {"kernel-release": "6.12", "zone": "UTC", "host-name": "web"})
self.assertEqual({c.args[2] for c in agent.call_args_list}, {10})
if __name__ == "__main__":