-
- {% bs_icon 'exclamation-triangle' %} {% trans "Warning" %}: {% trans "Hypervisor doesn't have any Secrets" %}
-
+ {% else %}
+
+
- {% else %}
-
-
-
-
-
- | {% trans "UUID" %} |
- {% trans "Usage Type" %} |
- {% trans "Data" %} |
- {% trans "Action" %} |
+
+
+
+
+ | {% trans "Type" %} |
+ {% trans "Usage" %} |
+ {% trans "Kept" %} |
+ {% trans "Used by" %} |
+ {% trans "Actions" %} |
-
-
- {% for secret in secrets_all %}
-
- | {{ secret.uuid }} |
- {{ secret.usage_type }} |
- {{ secret.usage }} |
-
-
-
-
-
- |
-
- {% endfor %}
-
-
-
+
+
+ {% for secret in secrets_all %}
+
+ | {{ secret.type_label }} |
+
+ {{ secret.usage|default:"–" }}
+ {{ secret.uuid }}
+ |
+
+ {% if secret.ephemeral %}{% trans "In memory" %}{% else %}{% trans "On disk" %}{% endif %}{% if secret.private %}, {% trans "private" %}{% endif %}
+ |
+
+ {% for vm in secret.vms %}
+
+ {% bs_icon 'pc-display' %}
+ {% if vm.id %}
+ {{ vm.name }}
+ {% else %}
+ {{ vm.name }}
+ {% endif %}
+
+ {% endfor %}
+ {% for pool in secret.pools %}
+
+ {% endfor %}
+ {% if not secret.vms and not secret.pools %}–{% endif %}
+ |
+
+
+
+
+
+
+
+
+
+ |
+
+ {% endfor %}
+
+
- {% endif %}
-
+
+
{% trans "No secret matches the search." %}
+
+
+
+ {# one dialog for every secret's value: its row's Set value fills in the secret #}
+
+ {% endif %}
+{% endblock %}
+
+{% block script %}
+{% if secrets_all %}{% endif %}
+
{% endblock %}
diff --git a/virtsecrets/tests/test_secrets_page.py b/virtsecrets/tests/test_secrets_page.py
index f552f530..810bb38f 100644
--- a/virtsecrets/tests/test_secrets_page.py
+++ b/virtsecrets/tests/test_secrets_page.py
@@ -1,5 +1,6 @@
-"""The secrets page never holds a secret's value: the edit dialog only
-writes a new one."""
+"""The secrets page never holds a secret's value: the Set value dialog only
+writes a new one. Each secret shows the VMs and pools that use it, and one in
+use is not offered for deletion."""
from unittest.mock import patch
@@ -7,29 +8,51 @@ from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
+from instances.models import Instance
from lxml import html
+UUID = "0e5b1f0a-8f3e-4c1a-9d2b-5c6d7e8f9a0b"
+
class SecretsPageTests(TestCase):
- def page(self, usage_type):
+ def page(self, usage_type="ceph", users=None):
compute = Compute.objects.create(name="secrets-page", hostname="localhost", login="", password="", type=4)
+ Instance.objects.create(compute=compute, name="db-1", uuid="00000000-0000-4000-8000-0000000000d1")
self.client.force_login(get_user_model().objects.create_superuser("page", "page@example.com", "pw"))
with patch("virtsecrets.views.wvmSecrets") as conn:
conn.return_value.get_secrets_info.return_value = [
- {"uuid": "0e5b1f0a-8f3e-4c1a-9d2b-5c6d7e8f9a0b", "usage_type": usage_type, "usage": "client.libvirt",
- "ephemeral": False, "private": False}]
- conn.return_value.get_users.return_value = {}
- conn.return_value.wvm.secretLookupByUUIDString.return_value.value.return_value = b"cephx key"
+ {"uuid": UUID, "usage_type": usage_type, "usage": "client.libvirt", "ephemeral": False, "private": True}]
+ conn.return_value.get_users.return_value = users or {}
response = self.client.get(reverse("virtsecrets", args=[compute.id]))
self.assertEqual(response.status_code, 200)
- return response
+ return html.fromstring(response.content)
def test_a_vtpm_secret_is_listed(self):
- self.assertContains(self.page("vtpm"), "vtpm")
+ self.assertEqual(self.page("vtpm").xpath("//tbody/tr/td[1]/span/text()"), ["vTPM"])
def test_the_page_holds_no_secret_value(self):
- response = self.page("ceph")
- self.assertFalse(b"cephx key" in response.content, "the secret's value is on the page")
- field = html.fromstring(response.content).get_element_by_id("secret_value_0e5b1f0a-8f3e-4c1a-9d2b-5c6d7e8f9a0b")
+ field = self.page().get_element_by_id("set_value")
self.assertIsNone(field.get("value"))
self.assertEqual(field.get("type"), "password")
+
+ def test_a_secret_in_use_shows_its_users_and_is_not_deleted(self):
+ page = self.page(users={UUID: {"vms": ["db-1", "gone-1"], "pools": ["ceph-rbd"]}})
+ instance = Instance.objects.get(name="db-1")
+ self.assertEqual(page.xpath("//tbody/tr/td[4]//a/@href")[0], reverse("instances:instance", args=[instance.id]))
+ self.assertIn("gone-1", page.xpath("string(//tbody/tr/td[4])"))
+ self.assertTrue(page.xpath("//tbody/tr/td[4]//a[contains(., 'ceph-rbd')]"))
+ self.assertIsNotNone(page.xpath("//button[@name='delete']")[0].get("disabled"))
+
+ def test_an_unused_secret_can_be_deleted(self):
+ self.assertIsNone(self.page().xpath("//button[@name='delete']")[0].get("disabled"))
+
+ def test_a_secret_without_usage_is_named_by_its_uuid(self):
+ compute = Compute.objects.create(name="secrets-none", hostname="localhost", login="", password="", type=4)
+ self.client.force_login(get_user_model().objects.create_superuser("none", "none@example.com", "pw"))
+ with patch("virtsecrets.views.wvmSecrets") as conn:
+ conn.return_value.get_secrets_info.return_value = [
+ {"uuid": UUID, "usage_type": "none", "usage": "", "ephemeral": True, "private": False}]
+ conn.return_value.get_users.return_value = {}
+ page = html.fromstring(self.client.get(reverse("virtsecrets", args=[compute.id])).content)
+ self.assertEqual(page.xpath("//button[@data-bs-target='#SetSecretValue']/@data-usage"), [UUID])
+ self.assertEqual(page.xpath("//tbody/tr/td[1]/span/text()"), ["none"])
diff --git a/virtsecrets/views.py b/virtsecrets/views.py
index 6fee6464..e33d466b 100644
--- a/virtsecrets/views.py
+++ b/virtsecrets/views.py
@@ -3,6 +3,7 @@ from computes.models import Compute
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
+from instances.models import Instance
from libvirt import libvirtError
from logs.audit import audit
from vrtManager.virtsecrets import wvmSecrets
@@ -44,8 +45,13 @@ def secrets(request, compute_id):
# shown only with their users: a list without them would offer to delete secrets in use
secrets = conn.get_secrets_info()
users = conn.get_users(secrets)
+ vm_ids = dict(Instance.objects.filter(compute=compute).values_list("name", "id"))
+ labels = dict(AddSecret.base_fields["usage_type"].choices)
for secret in secrets:
- secret.update(users.get(secret["uuid"], {"vms": [], "pools": []}))
+ used = users.get(secret["uuid"], {"vms": [], "pools": []})
+ secret["vms"] = [{"name": name, "id": vm_ids.get(name)} for name in used["vms"]]
+ secret["pools"] = used["pools"]
+ secret["type_label"] = labels.get(secret["usage_type"], secret["usage_type"])
secrets_all = secrets
except libvirtError as err: # OperationError too: a secret in use, a Ceph key not base64
messages.error(request, err)