1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/instances/tests/test_vm_report.py
catborise a80d093c94 feat(instances): a report of every VM with its addresses and OS, as an Excel file
The Administration menu gets "VM report (.xlsx)", a workbook for
superusers. The VMs sheet has one row per VM on every compute, shut-off
ones and those the panel has not registered too: name,
state, host, OS and OS family, vCPUs, memory, IP addresses and where they
came from, MAC addresses, owners, and whether the panel has registered
it. A Coverage sheet says when it was made and what could not be read.

The addresses come from the same live reading as the address search,
now shared by both. The OS is the guest agent's (libvirt's guest info)
or, without one, the libosinfo id a tool such as virt-install wrote into
the definition. Link-local IPv6 addresses are left out of the report.
Text cells stay text: a VM name starting with "=" is not a formula.

openpyxl 3.1.5 writes the workbook.
2026-10-24 22:30:07 +03:00

100 lines
5.7 KiB
Python

"""The VM report (instances.views.vm_report): every VM the user may see, with
its addresses and OS, as an Excel workbook."""
from io import BytesIO
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.contrib.auth.models import Permission
from django.contrib.messages import get_messages
from django.test import TestCase
from django.urls import reverse
from instances import addresses
from instances.models import Instance
from lxml import html
from openpyxl import load_workbook
UUID_A = "11111111-2222-3333-4444-00000000000a"
UUID_B = "11111111-2222-3333-4444-00000000000b"
UNKNOWN = "11111111-2222-3333-4444-00000000000c"
COUNTS = {"checked": 2, "no_agent": 1, "unread": 0, "hosts_down": ["host-three"], "hosts_unfinished": []}
def vm(uuid, name, compute_id, state=1, addresses_=(), os="AlmaLinux 10.0", family="Linux"):
return {"compute_id": compute_id, "uuid": uuid, "name": name, "state": state, "vcpus": 2, "memory": 2048,
"macs": ["52:54:00:00:00:01"], "os": os, "os_family": family, "addresses": list(addresses_)}
class VmReportTests(TestCase):
def setUp(self):
User = get_user_model()
self.admin = User.objects.create_superuser("vr-admin", "vr@example.com", "pw")
self.owner = User.objects.create_user("vr-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)
Instance.objects.create(compute=self.c2, name="vm-b", uuid=UUID_B)
UserInstance.objects.create(user=self.owner, instance=self.a)
def report(self, user, vms=(), side_effect=None):
self.client.force_login(user)
with patch("instances.addresses.inventory", return_value={"vms": list(vms), **COUNTS},
side_effect=side_effect) as inventory:
response = self.client.get(reverse("instances:vm_report"))
return response, inventory
def sheets(self, response):
book = load_workbook(BytesIO(response.content))
return ([list(row) for row in book["VMs"].iter_rows(values_only=True)],
[row[0] for row in book["Coverage"].iter_rows(values_only=True)])
def test_an_administrator_gets_every_vm_with_hosts_and_owners(self):
response, inventory = self.report(self.admin, [
vm(UUID_A, "vm-a", self.c1.id, addresses_=[("192.0.2.10", ["ARP", "agent"]), ("2001:db8::10", ["agent"]),
("fe80::1", ["agent"])]), # link-local: not an address to report
vm(UNKNOWN, "made-with-virsh", self.c2.id, state=5, os="", family=""),
])
self.assertEqual({(t.compute.id, t.uuids) for t in inventory.call_args.args[0]},
{(self.c1.id, None), (self.c2.id, None)})
self.assertEqual(response["Content-Type"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
self.assertRegex(response["Content-Disposition"], r'attachment; filename="vms-\d{8}-\d{4}\.xlsx"')
rows, coverage = self.sheets(response)
self.assertEqual(rows[0], ["VM", "State", "Host", "OS", "OS family", "vCPUs", "Memory (MiB)", "IP addresses",
"Address sources", "MAC addresses", "Owners", "In the panel"])
self.assertEqual(rows[1], ["vm-a", "Active", "host-one", "AlmaLinux 10.0", "Linux", 2, 2048,
"192.0.2.10, 2001:db8::10", "ARP, agent", "52:54:00:00:00:01", "vr-owner", "yes"])
self.assertEqual(rows[2][:3] + rows[2][-1:], ["made-with-virsh", "Off", "host-two", "no"])
self.assertIn("Hosts not reached: host-three", coverage)
def test_only_superusers_get_it(self):
viewer = get_user_model().objects.create_user("vr-viewer", password="pw")
viewer.user_permissions.add(Permission.objects.get(codename="view_instances"))
for user in (self.owner, viewer):
with self.subTest(user=user.username):
response, inventory = self.report(user)
self.assertEqual(response.status_code, 403)
inventory.assert_not_called()
def test_the_administration_menu_offers_it_to_superusers_only(self):
for user, offered in ((self.admin, True), (self.owner, False)):
with self.subTest(user=user.username):
self.client.force_login(user)
page = html.fromstring(self.client.get(reverse("accounts:profile")).content)
links = page.xpath("//div[@aria-labelledby='administration']//a/@href")
self.assertEqual(reverse("instances:vm_report") in links, offered)
def test_too_many_searches_return_to_the_list(self):
response, _ = self.report(self.admin, side_effect=addresses.Busy)
self.assertRedirects(response, reverse("instances:index"), fetch_redirect_response=False)
self.assertIn("try again shortly", " ".join(str(m) for m in get_messages(response.wsgi_request)))
def test_text_that_looks_like_a_formula_stays_text(self):
# names come from VM owners and guests: Excel must not run them
response, _ = self.report(self.admin, [vm(UUID_A, '=HYPERLINK("http://example.com","x")', self.c1.id,
os="+cmd|' /C calc'!A0")])
cells = load_workbook(BytesIO(response.content))["VMs"]["A2":"D2"][0]
self.assertEqual([c.data_type for c in cells], ["s", "s", "s", "s"])
self.assertEqual(cells[0].value, '=HYPERLINK("http://example.com","x")')