"""A secret a VM or a pool needs is not deleted: without it that VM or pool no longer starts. A VM names its secret in a disk, a host device or its vTPM, a pool in its source, by UUID or by usage within the usage type; as they run and as saved. libvirt's test driver has no secrets, so the connection is a mock.""" from unittest.mock import MagicMock, patch import libvirt from computes.models import Compute from django.contrib.auth import get_user_model from django.contrib.messages import get_messages from django.test import SimpleTestCase, TestCase from django.urls import reverse from vrtManager.util import OperationError from vrtManager.virtsecrets import wvmSecrets CEPH = "8f2c0000-0000-4000-8000-000000000001" ISCSI = "8f2c0000-0000-4000-8000-000000000002" VTPM = "8f2c0000-0000-4000-8000-000000000003" def secret(uuid, usage_type, usage): return MagicMock(**{"UUIDString.return_value": uuid, "XMLDesc.return_value": f"{usage}"}) def owner(name, xml, saved=None, active=False, persistent=True): """A domain or a pool: xml as it runs, saved as defined for its next start.""" return MagicMock(**{"name.return_value": name, "XMLDesc.side_effect": lambda flags=0: (saved or xml) if flags else xml, "isActive.return_value": active, "isPersistent.return_value": persistent}) def vm(name, devices, **kwargs): return owner(name, f"{devices}", **kwargs) def pool(name, auth, **kwargs): return owner(name, f"{auth}", **kwargs) def gone(code): err = libvirt.libvirtError("gone") err.get_error_code = lambda: code return MagicMock(**{"name.return_value": "gone", "XMLDesc.side_effect": err}) # the same usage, of two types: a reference by usage names the one of its type SECRETS = [secret(CEPH, "ceph", "client.libvirt"), secret(ISCSI, "iscsi", "client.libvirt"), secret(VTPM, "vtpm", "tpm")] def connection(domains=(), pools=(), secrets=SECRETS): with patch("vrtManager.connection.connection_manager.get_connection") as get: conn = wvmSecrets("host", "login", "pw", 1) get.return_value.configure_mock(**{"listAllDomains.return_value": list(domains), "listAllStoragePools.return_value": list(pools), "listAllSecrets.return_value": list(secrets)}) return conn def users(**kwargs): conn = connection(**kwargs) return conn.get_users(conn.get_secrets_info()) class UsersTests(SimpleTestCase): def test_vms_name_secrets_in_disks_host_devices_and_vtpm(self): found = users(domains=[ vm("rbd-1", f""), vm("lun-1", "" ""), vm("tpm-1", f""), ]) self.assertEqual(found, {CEPH: {"vms": ["rbd-1"], "pools": []}, ISCSI: {"vms": ["lun-1"], "pools": []}, VTPM: {"vms": ["tpm-1"], "pools": []}}) def test_a_pool_names_its_secret_by_the_auth_type_and_as_saved(self): found = users(pools=[ pool("ceph-rbd", ""), # chap is iSCSI; named only for its next start pool("lun", "", saved="" "", active=False), ]) self.assertEqual(found, {CEPH: {"vms": [], "pools": ["ceph-rbd"]}, ISCSI: {"vms": [], "pools": ["lun"]}}) def test_a_vm_uses_a_secret_as_it_runs_and_as_saved(self): disk = "" # saved only: read whether it runs or not, as it may stop between the reads for kwargs in ({"active": True, "saved": f"{disk}"}, {"active": False, "saved": f"{disk}"}, {"active": False}, {"active": True, "persistent": False}): devices = "" if "saved" in kwargs else disk with self.subTest(**kwargs): self.assertEqual(users(domains=[vm("db-1", devices, **kwargs)]), {CEPH: {"vms": ["db-1"], "pools": []}}) def test_what_is_gone_meanwhile_is_left_out_other_errors_are_raised(self): found = users(domains=[gone(libvirt.VIR_ERR_NO_DOMAIN)], pools=[gone(libvirt.VIR_ERR_NO_STORAGE_POOL)], secrets=[gone(libvirt.VIR_ERR_NO_SECRET)]) self.assertEqual(found, {}) with self.assertRaises(libvirt.libvirtError): users(domains=[gone(libvirt.VIR_ERR_OPERATION_FAILED)]) class NoValueReadTests(SimpleTestCase): def test_the_list_and_its_users_read_no_value(self): for listed in SECRETS: listed.value.side_effect = AssertionError("a secret's value was read") conn = connection(domains=[vm("db-1", f"")]) conn.get_users(conn.get_secrets_info()) conn.wvm.secretLookupByUUIDString.assert_not_called() class DeleteTests(SimpleTestCase): def test_a_secret_a_vm_or_a_pool_names_is_not_deleted(self): conn = connection(domains=[vm("db-2", f"")], pools=[pool("ceph-rbd", "")]) with self.assertRaisesRegex(OperationError, f"Secret {CEPH} is used by VMs db-2; pools ceph-rbd"): conn.delete_secret(CEPH) conn.wvm.secretLookupByUUIDString.return_value.undefine.assert_not_called() def test_an_unused_secret_is_deleted(self): conn = connection(domains=[vm("web-1", f"")]) conn.delete_secret(ISCSI) conn.wvm.secretLookupByUUIDString.assert_called_once_with(ISCSI) conn.wvm.secretLookupByUUIDString.return_value.undefine.assert_called_once_with() class SecretInUsePageTests(TestCase): def setUp(self): compute = Compute.objects.create(name="secret-use", hostname="localhost", login="", password="", type=4) self.client.force_login(get_user_model().objects.create_superuser("secret-use", "su@example.com", "pw")) self.url = reverse("virtsecrets", args=[compute.id]) def test_the_page_says_who_needs_it(self): with patch("virtsecrets.views.wvmSecrets") as conn: conn.return_value.delete_secret.side_effect = OperationError(f"Secret {CEPH} is used by VMs db-2") response = self.client.post(self.url, {"delete": "", "uuid": CEPH}) self.assertRedirects(response, self.url, fetch_redirect_response=False) self.assertEqual([str(m) for m in get_messages(response.wsgi_request)], [f"Secret {CEPH} is used by VMs db-2"]) def test_a_post_to_a_host_not_reached_is_redirected(self): with patch("virtsecrets.views.wvmSecrets", side_effect=libvirt.libvirtError("unable to connect")): response = self.client.post(self.url, {"delete": "", "uuid": CEPH}) self.assertRedirects(response, self.url, fetch_redirect_response=False) def test_the_list_is_not_shown_without_its_users(self): # it would offer to delete the secrets in use with patch("virtsecrets.views.wvmSecrets") as conn: conn.return_value.get_secrets_info.return_value = [{"uuid": CEPH, "usage_type": "ceph", "usage": "client.libvirt", "ephemeral": False, "private": False}] conn.return_value.get_users.side_effect = libvirt.libvirtError("connection reset") response = self.client.get(self.url) self.assertEqual(response.context["secrets_all"], [])