diff --git a/instances/tests/test_live_xml_effects.py b/instances/tests/test_live_xml_effects.py
index 834f95df..8d382ff4 100644
--- a/instances/tests/test_live_xml_effects.py
+++ b/instances/tests/test_live_xml_effects.py
@@ -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",
diff --git a/instances/tests/test_pending_edits.py b/instances/tests/test_pending_edits.py
index d14f1c2a..99be4186 100644
--- a/instances/tests/test_pending_edits.py
+++ b/instances/tests/test_pending_edits.py
@@ -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
diff --git a/instances/urls.py b/instances/urls.py
index bd4064c3..e7d8e541 100755
--- a/instances/urls.py
+++ b/instances/urls.py
@@ -54,6 +54,8 @@ urlpatterns = [
path("
/change_xml/", views.change_xml, name="change_xml"),
path("/set_guest_agent/", views.set_guest_agent, name="set_guest_agent"),
path("/set_video_model/", views.set_video_model, name="set_video_model"),
+ path("/set_rng/", views.set_rng, name="set_rng"),
+ path("/set_on_crash/", views.set_on_crash, name="set_on_crash"),
path("/change_network/", views.change_network, name="change_network"),
path("/add_network/", views.add_network, name="add_network"),
path("/delete_network/", views.delete_network, name="delete_network"),
diff --git a/instances/views.py b/instances/views.py
index e23bc85d..46b95b24 100755
--- a/instances/views.py
+++ b/instances/views.py
@@ -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
diff --git a/vrtManager/instance.py b/vrtManager/instance.py
index 67c555be..3d07ff53 100644
--- a/vrtManager/instance.py
+++ b/vrtManager/instance.py
@@ -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 = "/dev/urandom"
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
diff --git a/vrtManager/tests/test_device_edits.py b/vrtManager/tests/test_device_edits.py
index 1e61c504..a653121b 100644
--- a/vrtManager/tests/test_device_edits.py
+++ b/vrtManager/tests/test_device_edits.py
@@ -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(),
+ "/dev/urandom")
+ 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 = "{}"
+ for inner, found in ((backend.format("/dev/urandom") + "", "virtio"),
+ (backend.format("/dev/hwrng"), "custom"),
+ (backend.format("/dev/urandom") + "", "custom"),
+ (backend.format("/dev/urandom") + "", "custom")):
+ with self.subTest(inner):
+ p = proxy()
+ p._XMLDesc = lambda flags, inner=inner: PERSISTENT.replace("