mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
feat(instances): a VM's RNG device and crash action in its options
Two settings the owner chose to add to Settings > Options after the page's review with Codex. Both change the persistent definition, so they are saved while the VM runs and used from its next start. - Random number generator: a switch for a virtio RNG device fed from the host's /dev/urandom, so a new VM does not wait for entropy at boot. Another RNG device, or the panel's with a rate or driver, is shown as the VM's own and left to the XML editor. - When the guest crashes: Restart or Stop (on_crash restart or destroy); libvirt's default, Stop, when the definition has none. Stop keeps a VM that crashes again and again down for a look. Any other action is shown and left to the XML editor.
This commit is contained in:
parent
a993c12db0
commit
1150b941be
8 changed files with 177 additions and 0 deletions
|
|
@ -275,6 +275,14 @@ class Instance(models.Model):
|
|||
"""Whether the next start has hotpluggable vCPUs; the hot plug form shows it."""
|
||||
return any(vcpu["hotpluggable"] == "yes" for vcpu in self.proxy.get_vcpus(config=True).values())
|
||||
|
||||
@cached_property
|
||||
def config_rng(self) -> str | None:
|
||||
return self.proxy.get_rng()
|
||||
|
||||
@cached_property
|
||||
def config_on_crash(self) -> str:
|
||||
return self.proxy.get_on_crash()
|
||||
|
||||
@cached_property
|
||||
def guest_agent_ready(self) -> bool:
|
||||
return self.proxy.is_agent_ready()
|
||||
|
|
|
|||
|
|
@ -870,6 +870,49 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form class="list-group-item p-3 p-md-4" action="{% url 'instances:set_rng' instance.id %}" method="post">{% csrf_token %}
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-12 col-lg-4">
|
||||
<label class="form-label fw-semibold mb-0" for="rng">{% trans "Random number generator" %}</label>
|
||||
<p class="small text-body-secondary mb-0" id="rng-help">{% trans "A virtio device that feeds the guest's entropy from the host: a new VM does not wait for it at boot. From the next start." %}</p>
|
||||
</div>
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" name="rng" value="True" id="rng" aria-describedby="rng-help rng-state"
|
||||
{% if instance.config_rng %}checked{% endif %}{% if instance.config_rng == "custom" %} disabled{% endif %} onchange="this.form.submit()">
|
||||
<label class="form-check-label" for="rng">{% trans "Virtio RNG" %}</label>
|
||||
</div>
|
||||
{% if instance.config_rng == "custom" %}
|
||||
<p class="small text-body-secondary mt-1 mb-0" id="rng-state">{% trans "The VM has its own RNG device: change it in the XML editor." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form class="list-group-item p-3 p-md-4" action="{% url 'instances:set_on_crash' instance.id %}" method="post">{% csrf_token %}
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-12 col-lg-4">
|
||||
<label class="form-label fw-semibold mb-0" for="on_crash">{% trans "When the guest crashes" %}</label>
|
||||
<p class="small text-body-secondary mb-0" id="on-crash-help">{% trans "For a crash the guest reports, not a hang. Stop keeps a VM that crashes again and again down for a look. From the next start." %}</p>
|
||||
</div>
|
||||
<div class="col-12 col-lg-8">
|
||||
{% if instance.config_on_crash == "restart" or instance.config_on_crash == "destroy" %}
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-sm">
|
||||
<select id="on_crash" class="form-select" name="on_crash" aria-describedby="on-crash-help">
|
||||
<option value="restart"{% if instance.config_on_crash == "restart" %} selected{% endif %}>{% trans "Restart" %}</option>
|
||||
<option value="destroy"{% if instance.config_on_crash == "destroy" %} selected{% endif %}>{% trans "Stop" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto d-grid">
|
||||
<button class="btn btn-secondary" type="submit" name="set_on_crash">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="small mb-0" id="on_crash">{% blocktrans with action=instance.config_on_crash %}{{ action }}: change it in the XML editor.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form class="list-group-item p-3 p-md-4" action="{% url 'instances:set_guest_agent' instance.id %}" method="post">{% csrf_token %}
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-12 col-lg-4">
|
||||
|
|
|
|||
|
|
@ -125,6 +125,16 @@ EDITS = {
|
|||
lambda t: first(t, "devices/video/model/@type") == "virtio",
|
||||
["devices/video"], True, False,
|
||||
),
|
||||
"set_rng": (
|
||||
lambda vm: vm.set_rng(False),
|
||||
lambda t: not t.xpath("devices/rng"),
|
||||
["devices/rng"], True, False,
|
||||
),
|
||||
"set_on_crash": (
|
||||
lambda vm: vm.set_on_crash("restart"),
|
||||
lambda t: t.findtext("on_crash") == "restart",
|
||||
["on_crash"], True, False,
|
||||
),
|
||||
"resize_cpu": (
|
||||
lambda vm: vm.resize_cpu("1", "2"),
|
||||
lambda t: t.findtext("vcpu") == "2" and first(t, "vcpu/@current") == "1",
|
||||
|
|
|
|||
|
|
@ -141,6 +141,15 @@ class PendingEditsTestCase(TestCase):
|
|||
self.assertEqual(doc.xpath("//select[@name='vcpu_hotplug']/option[@selected]/@value"), ["True"])
|
||||
self.assertIn("No channel", doc.xpath("string(//form[.//select[@name='guest_agent']])"))
|
||||
|
||||
def test_a_custom_rng_or_crash_action_is_left_to_the_xml_editor(self):
|
||||
doc = self.page(get_rng=("custom", "custom"), get_on_crash=("preserve", "preserve"))
|
||||
self.assertIsNotNone(doc.xpath("//input[@name='rng']")[0].get("disabled"))
|
||||
self.assertEqual(doc.xpath("//select[@name='on_crash']"), [])
|
||||
self.assertIn("preserve: change it in the XML editor.", doc.xpath("string(//p[@id='on_crash'])"))
|
||||
doc = self.page(get_rng=("virtio", "virtio"), get_on_crash=("restart", "restart")) # read from the persistent definition
|
||||
self.assertEqual(doc.xpath("//input[@name='rng']/@checked"), ["checked"])
|
||||
self.assertEqual(doc.xpath("//select[@name='on_crash']/option[@selected]/@value"), ["restart"])
|
||||
|
||||
def test_buttons_shown_as_disabled_cannot_submit(self):
|
||||
# running: the CPU resize, vCPU hot plug, template and disk removal forms wait for a shutdown;
|
||||
# the title, description and video model are of the next start, saved at once
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ urlpatterns = [
|
|||
path("<int:pk>/change_xml/", views.change_xml, name="change_xml"),
|
||||
path("<int:pk>/set_guest_agent/", views.set_guest_agent, name="set_guest_agent"),
|
||||
path("<int:pk>/set_video_model/", views.set_video_model, name="set_video_model"),
|
||||
path("<int:pk>/set_rng/", views.set_rng, name="set_rng"),
|
||||
path("<int:pk>/set_on_crash/", views.set_on_crash, name="set_on_crash"),
|
||||
path("<int:pk>/change_network/", views.change_network, name="change_network"),
|
||||
path("<int:pk>/add_network/", views.add_network, name="add_network"),
|
||||
path("<int:pk>/delete_network/", views.delete_network, name="delete_network"),
|
||||
|
|
|
|||
|
|
@ -1749,6 +1749,34 @@ def set_guest_agent(request, pk):
|
|||
return _back(request, pk, "options")
|
||||
|
||||
|
||||
@require_POST
|
||||
@superuser_only
|
||||
@serialize_instance_mutation
|
||||
def set_rng(request, pk):
|
||||
instance = get_instance(request.user, pk)
|
||||
enabled = bool(request.POST.get("rng"))
|
||||
try:
|
||||
with audit_vm(request, instance, "add RNG device" if enabled else "remove RNG device"):
|
||||
instance.proxy.set_rng(enabled)
|
||||
except util.OperationError as err:
|
||||
messages.error(request, err)
|
||||
return _back(request, pk, "options")
|
||||
|
||||
|
||||
@require_POST
|
||||
@superuser_only
|
||||
@serialize_instance_mutation
|
||||
def set_on_crash(request, pk):
|
||||
instance = get_instance(request.user, pk)
|
||||
action = request.POST.get("on_crash", "")
|
||||
try:
|
||||
with audit_vm(request, instance, f"set crash action {action}"):
|
||||
instance.proxy.set_on_crash(action)
|
||||
except util.OperationError as err:
|
||||
messages.error(request, err)
|
||||
return _back(request, pk, "options")
|
||||
|
||||
|
||||
@require_POST
|
||||
@superuser_only
|
||||
@serialize_instance_mutation
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ PERSISTENT_XML = VIR_DOMAIN_XML_INACTIVE | VIR_DOMAIN_XML_SECURE
|
|||
USB_HOSTDEVS = "devices/hostdev[@mode='subsystem'][@type='usb']"
|
||||
PCI_HOSTDEVS = "devices/hostdev[@mode='subsystem'][@type='pci']"
|
||||
# what of the host a VM holds: host devices, and NICs that are host devices
|
||||
RNG_DEVICE = "<rng model='virtio'><backend model='random'>/dev/urandom</backend></rng>"
|
||||
GUEST_AGENT_CHANNEL = "/domain/devices/channel[@type='unix']/target[@name='org.qemu.guest_agent.0']"
|
||||
HOST_DEVICES = "devices/hostdev | devices/interface[@type='hostdev'] | devices/interface/actual[@type='hostdev']"
|
||||
|
||||
|
|
@ -2866,6 +2867,46 @@ class wvmInstance(wvmConnect):
|
|||
option = etree.SubElement(tree, o)
|
||||
option.text = option_value
|
||||
|
||||
def get_rng(self):
|
||||
"""The persistent definition's RNG device: "virtio" for the panel's
|
||||
own (virtio, from the host's /dev/urandom), "custom" for another one,
|
||||
None without one."""
|
||||
rngs = etree.fromstring(self._XMLDesc(PERSISTENT_XML)).xpath("devices/rng")
|
||||
if not rngs:
|
||||
return None
|
||||
# libvirt adds only its address and alias to the panel's device; a rate or driver makes it another one
|
||||
own = "@model='virtio' and backend[@model='random']='/dev/urandom' and not(*[not(self::backend or self::address or self::alias)])"
|
||||
if len(rngs) == 1 and rngs[0].xpath(own):
|
||||
return "virtio"
|
||||
return "custom"
|
||||
|
||||
def set_rng(self, enabled):
|
||||
"""Adds the panel's virtio RNG device to the persistent definition, or
|
||||
removes it; another RNG device is left to the XML editor."""
|
||||
if self.get_rng() == "custom":
|
||||
raise util.OperationError("The VM has its own RNG device: change it in the XML editor")
|
||||
tree = etree.fromstring(self._XMLDesc(PERSISTENT_XML))
|
||||
for rng in tree.xpath("devices/rng"):
|
||||
rng.getparent().remove(rng)
|
||||
if enabled:
|
||||
tree.find("devices").append(etree.fromstring(RNG_DEVICE))
|
||||
self._defineXML(etree.tostring(tree).decode())
|
||||
|
||||
def get_on_crash(self):
|
||||
"""What the next start does when the guest crashes; libvirt's default is destroy."""
|
||||
return etree.fromstring(self._XMLDesc(PERSISTENT_XML)).findtext("on_crash") or "destroy"
|
||||
|
||||
def set_on_crash(self, action):
|
||||
"""restart or destroy (stop) the VM when the guest crashes, from the next start."""
|
||||
if action not in ("restart", "destroy"):
|
||||
raise util.OperationError(f"Unknown crash action {action}")
|
||||
tree = etree.fromstring(self._XMLDesc(PERSISTENT_XML))
|
||||
on_crash = tree.find("on_crash")
|
||||
if on_crash is None:
|
||||
on_crash = etree.SubElement(tree, "on_crash")
|
||||
on_crash.text = action
|
||||
self._defineXML(etree.tostring(tree).decode())
|
||||
|
||||
def set_options(self, options):
|
||||
"""
|
||||
Function change description, title
|
||||
|
|
|
|||
|
|
@ -134,6 +134,42 @@ class DeviceEditsTestCase(unittest.TestCase):
|
|||
self.assertIsNotNone(p.get_guest_agent(config=True))
|
||||
self.assertEqual([v["hotpluggable"] for v in p.get_vcpus(config=True).values()], ["no", "yes"])
|
||||
|
||||
def test_the_rng_device_is_the_panel_s_own_or_left_to_the_xml_editor(self):
|
||||
p = proxy()
|
||||
self.assertIsNone(p.get_rng())
|
||||
p.set_rng(True)
|
||||
self.assertEqual(etree.tostring(defined(p).find("devices/rng")).decode(),
|
||||
"<rng model=\"virtio\"><backend model=\"random\">/dev/urandom</backend></rng>")
|
||||
added = etree.tostring(defined(p)).decode()
|
||||
p._XMLDesc = lambda flags: added
|
||||
self.assertEqual(p.get_rng(), "virtio")
|
||||
p.set_rng(False)
|
||||
self.assertIsNone(defined(p).find("devices/rng"))
|
||||
# the panel's device with the address libvirt gives it; another RNG device, or the panel's with
|
||||
# a rate or driver, is shown as custom and changed only in the XML editor
|
||||
backend = "<backend model='random'>{}</backend>"
|
||||
for inner, found in ((backend.format("/dev/urandom") + "<address type='pci' bus='0x08' slot='0x00'/>", "virtio"),
|
||||
(backend.format("/dev/hwrng"), "custom"),
|
||||
(backend.format("/dev/urandom") + "<rate bytes='1024' period='1000'/>", "custom"),
|
||||
(backend.format("/dev/urandom") + "<driver iommu='on'/>", "custom")):
|
||||
with self.subTest(inner):
|
||||
p = proxy()
|
||||
p._XMLDesc = lambda flags, inner=inner: PERSISTENT.replace("<video>", f"<rng model='virtio'>{inner}</rng><video>")
|
||||
self.assertEqual(p.get_rng(), found)
|
||||
if found == "custom":
|
||||
with self.assertRaises(util.OperationError):
|
||||
p.set_rng(False)
|
||||
p._defineXML.assert_not_called()
|
||||
|
||||
def test_the_crash_action_is_restart_or_stop(self):
|
||||
p = proxy()
|
||||
self.assertEqual(p.get_on_crash(), "destroy") # libvirt's default
|
||||
p.set_on_crash("restart")
|
||||
self.assertEqual(defined(p).findtext("on_crash"), "restart")
|
||||
with self.assertRaises(util.OperationError):
|
||||
p.set_on_crash("preserve")
|
||||
p._defineXML.assert_called_once()
|
||||
|
||||
def test_cpu_title_and_description_are_read_from_either_definition(self):
|
||||
p = proxy()
|
||||
self.assertEqual((p.get_vcpu(), p.get_cur_vcpu(), p.get_title(), p.get_description()), (1, 1, "", ""))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue