1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 20:26:48 +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:
catborise 2026-10-30 16:20:32 +03:00
parent a993c12db0
commit 1150b941be
8 changed files with 177 additions and 0 deletions

View file

@ -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()

View file

@ -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">

View file

@ -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",

View file

@ -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

View file

@ -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"),

View file

@ -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