mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 04:36:48 +00:00
feat(instances): a VM's TPM 2.0 is switched on and off on its Options tab
A VM made without a TPM, a Windows 10 to be upgraded to Windows 11, had no way to get one but the XML editor. The Options tab has a TPM 2.0 switch, as the RNG one: it adds an emulated TPM of the host's model to the definition for the next start, or removes it. Its state stays on the host until the VM is undefined, so a TPM added again has it back. A TPM the switch did not make (passthrough, 1.2, encrypted, with its own state path) is left to the XML editor, and a host without swtpm says so.
This commit is contained in:
parent
2a05576e18
commit
e35342bff1
10 changed files with 288 additions and 130 deletions
|
|
@ -289,6 +289,10 @@ class Instance(models.Model):
|
|||
def config_rng(self) -> str | None:
|
||||
return self.proxy.get_rng()
|
||||
|
||||
@cached_property
|
||||
def config_tpm(self) -> str | None:
|
||||
return self.proxy.get_tpm()
|
||||
|
||||
@cached_property
|
||||
def config_on_crash(self) -> str:
|
||||
return self.proxy.get_on_crash()
|
||||
|
|
|
|||
|
|
@ -936,6 +936,26 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form class="list-group-item p-3 p-md-4" action="{% url 'instances:set_tpm' 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="tpm">{% trans "TPM" %}</label>
|
||||
<p class="small text-body-secondary mb-0" id="tpm-help">{% trans "An emulated TPM 2.0, as Windows 11 needs with UEFI Secure Boot. Its state stays on the host until the VM is destroyed: a TPM added again has it back. 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="tpm" value="True" id="tpm" aria-describedby="tpm-help tpm-state"
|
||||
{% if instance.config_tpm %}checked{% endif %}{% if instance.config_tpm == "custom" or not instance.config_tpm and not tpm_support %} disabled{% endif %} onchange="this.form.submit()">
|
||||
<label class="form-check-label" for="tpm">{% trans "TPM 2.0" %}</label>
|
||||
</div>
|
||||
{% if instance.config_tpm == "custom" %}
|
||||
<p class="small text-body-secondary mt-1 mb-0" id="tpm-state">{% trans "The VM has its own TPM: change it in the XML editor." %}</p>
|
||||
{% elif not instance.config_tpm and not tpm_support %}
|
||||
<p class="small text-body-secondary mt-1 mb-0" id="tpm-state">{% trans "This host offers no TPM 2.0: install swtpm on it." %}</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">
|
||||
|
|
|
|||
|
|
@ -130,6 +130,11 @@ EDITS = {
|
|||
lambda t: not t.xpath("devices/rng"),
|
||||
["devices/rng"], True, False,
|
||||
),
|
||||
"set_tpm": (
|
||||
lambda vm: vm.set_tpm(True),
|
||||
lambda t: first(t, "devices/tpm/backend/@version") == "2.0",
|
||||
["devices/tpm"], True, False,
|
||||
),
|
||||
"set_on_crash": (
|
||||
lambda vm: vm.set_on_crash("restart"),
|
||||
lambda t: t.findtext("on_crash") == "restart",
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ class PendingEditsTestCase(TestCase):
|
|||
compute = Compute.objects.create(name="pe", hostname="127.0.0.1:1", login="root", password="", type=1)
|
||||
self.instance = Instance.objects.create(compute=compute, name="pe-vm", uuid="aaaaaaaa-1111-2222-3333-444444444444")
|
||||
|
||||
def page(self, live=(), config=(), nics=((), ()), status=1, **getters):
|
||||
def page(self, live=(), config=(), nics=((), ()), status=1, tpm_model="tpm-crb", **getters):
|
||||
"""live and config: disks; nics and getters (get_vcpu=(2, 4)): values of
|
||||
the live and the persistent definition."""
|
||||
the live and the persistent definition; tpm_model: the host's TPM."""
|
||||
with patch("instances.models.wvmInstance") as wvm, patch.object(
|
||||
Compute, "status", new_callable=PropertyMock, return_value=True
|
||||
):
|
||||
|
|
@ -54,6 +54,7 @@ class PendingEditsTestCase(TestCase):
|
|||
proxy.get_console_type.return_value = "vnc"
|
||||
proxy.get_video_models.return_value = ["vga", "virtio"]
|
||||
proxy.get_cache_modes.return_value = {"default": "Default", "none": "Disabled"}
|
||||
proxy.get_tpm_model.return_value = tpm_model
|
||||
response = self.client.get(reverse("instances:instance", args=[self.instance.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return html.fromstring(response.content)
|
||||
|
|
@ -220,6 +221,20 @@ class PendingEditsTestCase(TestCase):
|
|||
self.assertEqual(doc.xpath("//input[@name='rng']/@checked"), ["checked"])
|
||||
self.assertEqual(doc.xpath("//select[@name='on_crash']/option[@selected]/@value"), ["restart"])
|
||||
|
||||
def test_the_tpm_switch_is_off_where_it_cannot_change(self):
|
||||
tpm = "//input[@name='tpm']"
|
||||
doc = self.page(get_tpm=("emulator", "emulator"))
|
||||
self.assertEqual((doc.xpath(f"{tpm}/@checked"), doc.xpath(f"{tpm}/@disabled")), (["checked"], []))
|
||||
doc = self.page(get_tpm=("custom", "custom"))
|
||||
self.assertTrue(doc.xpath(f"{tpm}/@disabled"))
|
||||
self.assertIn("change it in the XML editor", doc.xpath("string(//p[@id='tpm-state'])"))
|
||||
# no swtpm: none can be added, one the VM has can still go
|
||||
doc = self.page(get_tpm=(None, None), tpm_model=None)
|
||||
self.assertTrue(doc.xpath(f"{tpm}/@disabled"))
|
||||
self.assertIn("install swtpm", doc.xpath("string(//p[@id='tpm-state'])"))
|
||||
doc = self.page(get_tpm=("emulator", "emulator"), tpm_model=None)
|
||||
self.assertEqual(doc.xpath(f"{tpm}/@disabled"), [])
|
||||
|
||||
def test_the_users_tab_shows_each_owner_and_offers_only_new_owners(self):
|
||||
from accounts.models import UserInstance
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ urlpatterns = [
|
|||
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_tpm/", views.set_tpm, name="set_tpm"),
|
||||
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"),
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ def _instance_page(request, pk, instance, compute):
|
|||
memory_host = instance.proxy.get_max_memory()
|
||||
bus_host = instance.proxy.get_disk_bus_types(instance.arch, instance.machine)
|
||||
cdrom_buses = cdrom_bus_choices(instance)
|
||||
tpm_support = bool(instance.proxy.get_tpm_model(instance.arch, instance.machine))
|
||||
networks_host = sorted(instance.proxy.get_networks())
|
||||
interfaces_host = sorted(instance.proxy.get_ifaces())
|
||||
nwfilters_host = instance.proxy.get_nwfilters()
|
||||
|
|
@ -1883,6 +1884,20 @@ def set_rng(request, pk):
|
|||
return _back(request, pk, "options")
|
||||
|
||||
|
||||
@require_POST
|
||||
@superuser_only
|
||||
@serialize_instance_mutation
|
||||
def set_tpm(request, pk):
|
||||
instance = get_instance(request.user, pk)
|
||||
enabled = bool(request.POST.get("tpm"))
|
||||
try:
|
||||
with audit_vm(request, instance, "add TPM" if enabled else "remove TPM"):
|
||||
instance.proxy.set_tpm(enabled)
|
||||
except util.OperationError as err:
|
||||
messages.error(request, err)
|
||||
return _back(request, pk, "options")
|
||||
|
||||
|
||||
@require_POST
|
||||
@superuser_only
|
||||
@serialize_instance_mutation
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue