mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
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.
126 lines
6.9 KiB
Python
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)
|