1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/instances/tests/test_find_address.py
catborise ecb850739f feat(instances): the address search finds a VM by its MAC address too
The "Find IP" dialog takes a MAC address (aa:bb:..., aa-bb-... or
aabb...) as well as an IP address or a network. A MAC finds the VMs with
a network card of that MAC, shut-off ones too (from their definitions),
with that card's addresses when the VM runs: for a MAC seen on a switch
port or in an external DHCP server's log. A MAC on more than one VM is
marked as a possible conflict. Only the VMs with that MAC are read, and
the card's link-local address, which comes from the MAC, is left out.
2026-10-24 22:45:47 +03:00

126 lines
6.9 KiB
Python

"""The Instances page's search for the VMs using an address
(instances.views.find_address): scope by permission, and what each user sees."""
from unittest.mock import patch
from accounts.models import UserInstance
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 import addresses
from instances.models import Instance
from lxml import html
UUID_A = "11111111-2222-3333-4444-00000000000a"
UUID_B = "11111111-2222-3333-4444-00000000000b"
UNKNOWN = "11111111-2222-3333-4444-00000000000c"
def row(uuid, name, compute_id, address="192.0.2.10"):
return {"uuid": uuid, "name": name, "compute_id": compute_id, "mac": "52:54:00:00:00:01",
"address": address, "sources": ["agent"], "conflict": False}
class FindAddressTests(TestCase):
def setUp(self):
User = get_user_model()
self.admin = User.objects.create_superuser("fa-admin", "fa@example.com", "pw")
self.owner = User.objects.create_user("fa-owner", password="pw")
self.c1 = Compute.objects.create(name="host-one", hostname="192.0.2.1", login="u", password="p", type=1)
self.c2 = Compute.objects.create(name="host-two", hostname="192.0.2.2", login="u", password="p", type=1)
self.a = Instance.objects.create(compute=self.c1, name="vm-a", uuid=UUID_A)
self.b = Instance.objects.create(compute=self.c2, name="vm-b", uuid=UUID_B)
UserInstance.objects.create(user=self.owner, instance=self.a)
def search(self, user, q="192.0.2.10", result=None, side_effect=None):
self.client.force_login(user)
answer = {"matches": [], "checked": 1, "no_agent": 0, "unread": 0, "hosts_down": [],
"hosts_unfinished": [], **(result or {})}
with patch("instances.addresses.find", return_value=answer, side_effect=side_effect) as find:
response = self.client.get(reverse("instances:find_address"), {"q": q})
return response, find
def test_an_administrator_gets_hosts_and_unregistered_vms(self):
response, find = self.search(self.admin, result={
"matches": [row(UUID_A, "vm-a", self.c1.id), row(UNKNOWN, "made-with-virsh", self.c2.id)],
"hosts_down": ["host-three"], "hosts_unfinished": ["host-four"],
})
targets = find.call_args.args[0]
self.assertEqual({(t.compute.id, t.uuids) for t in targets}, {(self.c1.id, None), (self.c2.id, None)})
body = response.json()
known, unknown = body["matches"]
self.assertEqual((known["host"], known["registered"], known["vm"]), ("host-one", True, "vm-a"))
self.assertEqual(known["url"], reverse("instances:instance", args=[self.a.id]) + "#network")
self.assertEqual((unknown["host"], unknown["registered"], unknown["url"]), ("host-two", False, None))
self.assertEqual((body["coverage"]["hosts_down"], body["coverage"]["hosts_unfinished"]),
(["host-three"], ["host-four"]))
self.assertIn("read_at", body)
def test_an_owner_is_scoped_to_their_vms_and_sees_no_hosts(self):
response, find = self.search(self.owner, result={
"matches": [row(UUID_A, "vm-a", self.c1.id), row(UUID_B, "vm-b", self.c2.id)],
"hosts_down": ["host-two"],
})
(target,) = find.call_args.args[0]
self.assertEqual((target.compute.id, target.uuids), (self.c1.id, {UUID_A}))
body = response.json()
self.assertEqual([m["vm"] for m in body["matches"]], ["vm-a"])
self.assertNotIn("host", body["matches"][0])
self.assertNotIn("hosts_down", body["coverage"])
self.assertTrue(body["coverage"]["some_unchecked"])
self.assertNotIn("host-two", response.content.decode())
def test_an_owners_vm_on_two_computes_is_searched_on_both(self):
# one UUID on two hosts: Instance is unique per compute, not per UUID
twin = Instance.objects.create(compute=self.c2, name="vm-a-copy", uuid=UUID_A)
UserInstance.objects.create(user=self.owner, instance=twin)
response, find = self.search(self.owner, result={
"matches": [row(UUID_A, "vm-a", self.c1.id), row(UUID_A, "vm-a-copy", self.c2.id)]})
self.assertEqual({(t.compute.id, frozenset(t.uuids)) for t in find.call_args.args[0]},
{(self.c1.id, frozenset({UUID_A})), (self.c2.id, frozenset({UUID_A}))})
self.assertEqual([m["url"] for m in response.json()["matches"]],
[reverse("instances:instance", args=[i.id]) + "#network" for i in (self.a, twin)])
def test_a_mac_is_looked_up_by_its_nics(self):
self.client.force_login(self.admin)
answer = {"matches": [{**row(UUID_A, "vm-a", self.c1.id), "address": "", "sources": []}],
"checked": 1, "no_agent": 0, "unread": 0, "hosts_down": [], "hosts_unfinished": []}
with patch("instances.addresses.find_mac", return_value=answer) as find_mac, \
patch("instances.addresses.find") as find:
response = self.client.get(reverse("instances:find_address"), {"q": "52-54-00-00-00-01"})
self.assertEqual(find_mac.call_args.args[1], "52:54:00:00:00:01")
find.assert_not_called()
self.assertEqual([(m["vm"], m["address"]) for m in response.json()["matches"]], [("vm-a", "")])
def test_a_bad_query_is_refused_without_libvirt(self):
for q in ("", "abc", "192.0.2.300"):
with self.subTest(q=q):
response, find = self.search(self.admin, q=q)
self.assertEqual(response.status_code, 400)
self.assertIn("MAC", response.json()["error"])
find.assert_not_called()
def test_too_many_searches_answer_409(self):
response, _ = self.search(self.admin, side_effect=addresses.Busy)
self.assertEqual(response.status_code, 409)
self.assertEqual(response.json()["error"], "Too many searches are still running; try again shortly.")
class FindAddressPageTests(TestCase):
"""static/js/find_address.js fills the dialog; only administrators get a Host column."""
def page(self, user):
self.client.force_login(user)
return html.fromstring(self.client.get(reverse("instances:index")).content)
def test_the_page_offers_the_search(self):
User = get_user_model()
for user, host_column in ((User.objects.create_superuser("fp-admin", "fp@example.com", "pw"), True),
(User.objects.create_user("fp-owner", password="pw"), False)):
with self.subTest(user=user.username):
doc = self.page(user)
self.assertEqual(len(doc.xpath("//button[@id='find-address-open']")), 1)
self.assertEqual(len(doc.xpath("//div[@id='find-address']//input[@name='q']")), 1)
headers = [" ".join(th.text_content().split()) for th in doc.xpath("//div[@id='find-address']//th")]
self.assertEqual("Host" in headers, host_column)