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:
parent
75fcc6f6c0
commit
400b19cf42
4 changed files with 88 additions and 60 deletions
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue