1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

test: each test is in the tests package of the code it tests

Tests that build vrtManager objects and touch no view, form or model of
an app lived in that app; vrtManager/tests already holds such tests, so
the convention was split. They move to vrtManager/tests: the firmware,
CPU mode and PCIe port definitions of a new VM (test_create), USB and PCI
host devices, the console listener, host CPU reads, interface reads,
deletes and XML, network filters, PCI passthrough rules, node devices,
network changes, addresses and leases, pool XML, secrets, and the live
tests that use vrtManager only (disk resize, ISO, snapshots, XML, live
clone). livetest.py, the live-test helper six apps import, and the
network test driver move with them.

Others go to their app: the quota with a host down to instances, the
401/403 Logs rows (webvirtcloud's middleware) to webvirtcloud, the VM
page's tab script test to instances, novncd's own tests out of the
console views tests, the compute lock and input validation tests out of
the computes views tests, the interface views out of the forms tests.
instances/tests/test_views.py, entirely live, is test_live_views.py.

Test modules imported fixtures from each other; shared ones are now in
vrtManager/tests/fakes.py (migration fakes, node devices, IOMMU groups,
leases, a NAT network, a Ceph secret), interfaces/tests/fakes.py,
console/tests/daemons.py (novncd loaded once), livetest.wait_for and
instances/tests/create_page.BODY. Every live test checks
livetest.enabled(). The suite runs the same 2097 tests.
This commit is contained in:
catborise 2026-11-06 11:33:42 +03:00
parent 4e0952aa50
commit bdc8a6755c
73 changed files with 2231 additions and 2132 deletions

View file

@ -6,8 +6,7 @@ import json
import shutil
import subprocess
import unittest
from unittest import TestCase as UnitTestCase
from unittest.mock import MagicMock, PropertyMock, patch
from unittest.mock import PropertyMock, patch
from django.conf import settings
from django.contrib.auth import get_user_model
@ -19,7 +18,6 @@ from lxml import html
from computes.models import Compute
from computes.views import CPU_SAMPLE_MAX_AGE, CPU_SAMPLE_SIGNER
from vrtManager.hostdetails import cpu_percent, wvmHostDetails
from vrtManager.util import ConnectionFailed
# the page script runs after the shared chart helpers
@ -27,52 +25,6 @@ CHARTS_JS = settings.BASE_DIR / "static" / "js" / "charts.js"
TOPOLOGY = {"nodes": 1, "sockets": 1, "cores": 2, "threads": 1, "mhz": 2400, "cpus": 2}
def host(*readings):
"""A wvmHostDetails whose getCPUStats returns readings, (idle, busy) each."""
h = wvmHostDetails.__new__(wvmHostDetails)
h.wvm = MagicMock()
h.wvm.getCPUStats.side_effect = [
{"kernel": busy // 2, "user": busy - busy // 2, "idle": idle, "iowait": 0, "guest": busy // 3, "utilization": 99}
for idle, busy in readings
]
return h
class CpuPercentTestCase(UnitTestCase):
def test_busy_share_between_two_samples(self):
self.assertEqual(cpu_percent((100, 200), (130, 300)), 70.0)
self.assertEqual(cpu_percent((0, 0), (1, 3)), 66.7)
def test_samples_that_do_not_follow_on(self):
for before, after in (((100, 200), (100, 200)), ((100, 200), (50, 100)), ((100, 200), (200, 250))):
with self.subTest(before=before, after=after):
self.assertIsNone(cpu_percent(before, after))
@patch("vrtManager.hostdetails.time")
class CpuUsageTestCase(UnitTestCase):
def test_since_a_previous_sample_without_waiting(self, clock):
clock.time.return_value = 105.0
h = host((130, 70))
usage = h.get_cpu_usage(previous=(100, 100, 100.0))
clock.sleep.assert_not_called()
# total = kernel + user + idle + iowait; guest (inside user) and utilization are left out
self.assertEqual(usage, {"usage": 70.0, "sample": (130, 200, 105.0), "window": 5.0})
def test_over_one_second_without_a_sample(self, clock):
clock.time.side_effect = [10.0, 11.0]
h = host((100, 100), (110, 190))
self.assertEqual(h.get_cpu_usage(), {"usage": 90.0, "sample": (110, 300, 11.0), "window": 1.0})
clock.sleep.assert_called_once_with(1)
def test_over_one_second_after_the_host_restarted(self, clock):
clock.time.side_effect = [10.0, 11.0]
h = host((5, 5), (6, 9))
usage = h.get_cpu_usage(previous=(1000, 5000, 5.0))
clock.sleep.assert_called_once_with(1)
self.assertEqual((usage["usage"], usage["window"]), (80.0, 1.0))
class GraphTestCase(TestCase):
def setUp(self):
self.compute = Compute.objects.create(name="graph", hostname="192.0.2.1", login="u", password="p", type=1)

View file

@ -6,6 +6,8 @@ from django.test import TransactionTestCase
from computes.utils import libvirt_compute_lock, libvirt_instance_lock
from operations import claims
from vrtManager.connection import CONN_SOCKET
from computes.models import Compute
class OperationLocksTestCase(TransactionTestCase):
@ -62,3 +64,205 @@ class OperationLocksTestCase(TransactionTestCase):
with libvirt_compute_lock(self.compute):
with libvirt_instance_lock(self.first):
pass
class ComputeConcurrencyTestCase(TransactionTestCase):
def test_concurrent_refresh_instance_database(self):
import threading
from django.db import connection
from unittest.mock import MagicMock
from computes.utils import refresh_instance_database
from instances.models import Instance
compute = Compute.objects.create(
name="concurrent-test",
hostname="127.0.0.1",
login="test",
password="pwd",
type=CONN_SOCKET,
)
mock_dom1 = MagicMock()
mock_dom1.UUIDString.return_value = "11111111-2222-3333-4444-555555555555"
mock_dom1.name.return_value = "vm-concurrent-1"
mock_dom2 = MagicMock()
mock_dom2.UUIDString.return_value = "22222222-3333-4444-5555-666666666666"
mock_dom2.name.return_value = "vm-concurrent-2"
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = [mock_dom1, mock_dom2]
compute.proxy = mock_proxy
compute.status = True # reachable
exceptions = []
def worker():
try:
refresh_instance_database(compute)
except Exception as e:
exceptions.append(e)
finally:
connection.close()
connection.close()
threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
self.assertEqual(exceptions, [])
self.assertEqual(Instance.objects.filter(compute=compute).count(), 2)
def test_refresh_skips_a_busy_compute(self):
from unittest.mock import MagicMock
from computes.utils import refresh_instance_database
from instances.models import Instance
from operations import claims
from operations.models import Claim
compute = Compute.objects.create(
name="lock-busy-test",
hostname="192.0.2.10",
login="test",
password="pwd",
type=CONN_SOCKET,
)
Instance.objects.create(compute=compute, name="wvc-test-existing", uuid="existing-uuid-1234")
# Libvirt returns an empty list: a refresh that ran would delete the VM
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = []
compute.proxy = mock_proxy
compute.status = True
claims.admit({claims.compute_key(compute.id): Claim.EXCLUSIVE}, holder="other")
refresh_instance_database(compute)
self.assertEqual(Instance.objects.filter(compute=compute).count(), 1)
mock_proxy.wvm.listAllDomains.assert_called_once() # the pre-check's read; no reconcile
def _reconcile_compute(self, name, domains):
"""A compute whose database differs from libvirt (a VM libvirt no
longer lists), so a refresh reconciles it: the listing that domains
answers second is the one inside the reconcile, the first is the
pre-check's."""
from unittest.mock import MagicMock
from instances.models import Instance
compute = Compute.objects.create(name=name, hostname="192.0.2.10", login="test", password="pwd",
type=CONN_SOCKET)
Instance.objects.create(compute=compute, name="wvc-test-gone", uuid="wvc-test-gone-uuid")
compute.proxy = MagicMock()
compute.proxy.wvm.listAllDomains.side_effect = domains
compute.status = True
return compute
def test_a_vm_operation_runs_while_a_reconcile_holds_the_compute(self):
import threading
from types import SimpleNamespace
from django.db import connection
from computes.utils import libvirt_instance_lock, refresh_instance_database
outcome = []
def vm_operation():
try:
with libvirt_instance_lock(SimpleNamespace(uuid="wvc-test-uuid-1", compute=compute)):
outcome.append("ran")
except Exception as err:
outcome.append(err)
finally:
connection.close()
calls = []
def list_domains():
calls.append("list")
if len(calls) == 2: # inside the reconcile
thread = threading.Thread(target=vm_operation)
thread.start()
thread.join(10)
return []
compute = self._reconcile_compute("reconcile-shared-test", list_domains)
refresh_instance_database(compute)
self.assertEqual(outcome, ["ran"])
def test_a_second_reconcile_of_the_compute_is_skipped_meanwhile(self):
"""Two reconciles would read libvirt at different times and could
commit in reverse order, an older listing undoing a newer one."""
import threading
from django.db import connection
from computes.utils import refresh_instance_database
calls = []
def second_reconcile():
try:
refresh_instance_database(compute)
finally:
connection.close()
def list_domains():
calls.append("list")
if len(calls) == 2: # inside the reconcile
thread = threading.Thread(target=second_reconcile)
thread.start()
thread.join(10)
return []
compute = self._reconcile_compute("reconcile-twice-test", list_domains)
refresh_instance_database(compute)
# the second one's pre-check, then no listing of its own
self.assertEqual(calls, ["list", "list", "list"])
def test_a_reconcile_runs_while_a_job_holds_the_compute_shared(self):
from computes.utils import refresh_instance_database
from instances.models import Instance
from operations import claims
from operations.models import Claim, Operation
dom = type("Dom", (), {"UUIDString": lambda self: "33333333-4444-5555-6666-777777777777",
"name": lambda self: "wvc-test-vm"})()
compute = self._reconcile_compute("reconcile-job-test", lambda: [dom])
op = Operation.objects.create(kind="test.job", username="wvc-test-user", submission_key="key-00000001",
state=Operation.RUNNING)
Claim.objects.create(operation=op, resource=claims.compute_key(compute.id), mode=Claim.SHARED)
refresh_instance_database(compute)
self.assertTrue(Instance.objects.filter(compute=compute, name="wvc-test-vm").exists())
def test_libvirt_compute_lock_held_by_another_holder_is_busy(self):
from computes.utils import libvirt_compute_lock
from operations import claims
from operations.models import Claim
compute = Compute.objects.create(
name="thread-lock-test",
hostname="192.0.2.10",
login="test",
password="pwd",
type=CONN_SOCKET,
)
claims.admit({claims.compute_key(compute.id): Claim.EXCLUSIVE}, holder="other")
with self.assertRaises(claims.Busy):
with libvirt_compute_lock(compute):
pass
def test_libvirt_compute_lock_reentrant(self):
from computes.utils import libvirt_compute_lock
compute = Compute.objects.create(
name="reentrant-lock-test",
hostname="192.0.2.10",
login="test",
password="pwd",
type=CONN_SOCKET,
)
executed = []
with libvirt_compute_lock(compute):
executed.append("outer")
with libvirt_compute_lock(compute):
executed.append("inner")
with libvirt_compute_lock(compute):
executed.append("deep")
self.assertEqual(executed, ["outer", "inner", "deep"])

View file

@ -1,26 +0,0 @@
import socket
from accounts.models import UserAttributes, UserInstance
from appsettings.settings import get_settings
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import TestCase
from instances.models import Instance
from instances.utils import QUOTA_UNVERIFIED, check_user_quota
class QuotaWithDownHostTestCase(TestCase):
def test_vm_on_an_unreachable_host_makes_the_quota_unverified(self):
# its usage is unknown, so an increase is refused (fail closed), without an error page
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
user = get_user_model().objects.create_user("quota_down", password="pw")
UserAttributes.objects.create(user=user, max_instances=5, max_cpus=8, max_memory=8192, max_disk_size=100)
compute = Compute.objects.create(name="down", hostname=f"127.0.0.1:{port}", login="", password="", type=1)
vm = Instance.objects.create(compute=compute, name="vm", uuid="55555555-5555-5555-5555-555555555555")
UserInstance.objects.create(user=user, instance=vm)
get_settings() # app settings are normally loaded by middleware
self.assertEqual(check_user_quota(user, 1, 1, 128, 1), QUOTA_UNVERIFIED)

View file

@ -7,6 +7,7 @@ import unittest
from django.core.exceptions import ValidationError
from computes.validators import validate_hostname, validate_login
from django.test import TestCase
class HostnameValidatorTestCase(unittest.TestCase):
@ -46,3 +47,41 @@ class LoginValidatorTestCase(unittest.TestCase):
validate_login(value)
class ComputeInputValidationTestCase(TestCase):
"""hostname and login reach libvirt URIs and ssh unquoted; the form and
the API reject values that would inject a URI parameter or an ssh option."""
def test_form_rejects_injecting_hostname(self):
from computes.forms import SshComputeForm
form = SshComputeForm(
data={"name": "c", "hostname": "192.0.2.1/system?command=/evil", "login": "root", "type": 2}
)
self.assertFalse(form.is_valid())
self.assertIn("hostname", form.errors)
def test_api_rejects_injecting_hostname_and_login(self):
from computes.api.serializers import ComputeSerializer
for field, data in (
("hostname", {"name": "c", "hostname": "192.0.2.1/system?command=/evil&a=", "login": "root", "type": 2}),
("login", {"name": "c", "hostname": "192.0.2.1", "login": "-oProxyCommand=x", "type": 2}),
):
with self.subTest(field=field):
serializer = ComputeSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn(field, serializer.errors)
def test_api_accepts_valid_values(self):
from computes.api.serializers import ComputeSerializer
serializer = ComputeSerializer(
data={"name": "c", "hostname": "host.example.com", "login": "root", "type": 2}
)
self.assertTrue(serializer.is_valid(), serializer.errors)
def test_api_accepts_empty_login_for_tcp(self):
from computes.api.serializers import ComputeSerializer
serializer = ComputeSerializer(data={"name": "c", "hostname": "192.0.2.1", "login": "", "type": 1})
self.assertTrue(serializer.is_valid(), serializer.errors)

View file

@ -4,11 +4,11 @@ from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import reverse
from django.test import TestCase, TransactionTestCase
from django.test import TestCase
from vrtManager.connection import CONN_SOCKET, connection_manager
from computes.models import Compute
from instances.tests import livetest
from vrtManager.tests import livetest
TEST_COMPUTE_HOST = os.environ.get("TEST_LIBVIRT_HOST", "localhost")
TEST_COMPUTE_LOGIN = os.environ.get("TEST_LIBVIRT_LOGIN", "")
@ -196,245 +196,3 @@ class ComputesTestCase(TestCase):
conn.return_value.get_disk_bus_types.return_value = ["virtio", "sata", "scsi"]
response = self.client.get(reverse("buses", args=[self.compute.id, "x86_64", "q35", "disk"]))
self.assertEqual(json.loads(response.content), {"bus": ["sata", "scsi", "virtio"]})
class ComputeConcurrencyTestCase(TransactionTestCase):
def test_concurrent_refresh_instance_database(self):
import threading
from django.db import connection
from unittest.mock import MagicMock
from computes.utils import refresh_instance_database
from instances.models import Instance
compute = Compute.objects.create(
name="concurrent-test",
hostname="127.0.0.1",
login="test",
password="pwd",
type=CONN_SOCKET,
)
mock_dom1 = MagicMock()
mock_dom1.UUIDString.return_value = "11111111-2222-3333-4444-555555555555"
mock_dom1.name.return_value = "vm-concurrent-1"
mock_dom2 = MagicMock()
mock_dom2.UUIDString.return_value = "22222222-3333-4444-5555-666666666666"
mock_dom2.name.return_value = "vm-concurrent-2"
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = [mock_dom1, mock_dom2]
compute.proxy = mock_proxy
compute.status = True # reachable
exceptions = []
def worker():
try:
refresh_instance_database(compute)
except Exception as e:
exceptions.append(e)
finally:
connection.close()
connection.close()
threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
self.assertEqual(exceptions, [])
self.assertEqual(Instance.objects.filter(compute=compute).count(), 2)
def test_refresh_skips_a_busy_compute(self):
from unittest.mock import MagicMock
from computes.utils import refresh_instance_database
from instances.models import Instance
from operations import claims
from operations.models import Claim
compute = Compute.objects.create(
name="lock-busy-test",
hostname="192.0.2.10",
login="test",
password="pwd",
type=CONN_SOCKET,
)
Instance.objects.create(compute=compute, name="wvc-test-existing", uuid="existing-uuid-1234")
# Libvirt returns an empty list: a refresh that ran would delete the VM
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = []
compute.proxy = mock_proxy
compute.status = True
claims.admit({claims.compute_key(compute.id): Claim.EXCLUSIVE}, holder="other")
refresh_instance_database(compute)
self.assertEqual(Instance.objects.filter(compute=compute).count(), 1)
mock_proxy.wvm.listAllDomains.assert_called_once() # the pre-check's read; no reconcile
def _reconcile_compute(self, name, domains):
"""A compute whose database differs from libvirt (a VM libvirt no
longer lists), so a refresh reconciles it: the listing that domains
answers second is the one inside the reconcile, the first is the
pre-check's."""
from unittest.mock import MagicMock
from instances.models import Instance
compute = Compute.objects.create(name=name, hostname="192.0.2.10", login="test", password="pwd",
type=CONN_SOCKET)
Instance.objects.create(compute=compute, name="wvc-test-gone", uuid="wvc-test-gone-uuid")
compute.proxy = MagicMock()
compute.proxy.wvm.listAllDomains.side_effect = domains
compute.status = True
return compute
def test_a_vm_operation_runs_while_a_reconcile_holds_the_compute(self):
import threading
from types import SimpleNamespace
from django.db import connection
from computes.utils import libvirt_instance_lock, refresh_instance_database
outcome = []
def vm_operation():
try:
with libvirt_instance_lock(SimpleNamespace(uuid="wvc-test-uuid-1", compute=compute)):
outcome.append("ran")
except Exception as err:
outcome.append(err)
finally:
connection.close()
calls = []
def list_domains():
calls.append("list")
if len(calls) == 2: # inside the reconcile
thread = threading.Thread(target=vm_operation)
thread.start()
thread.join(10)
return []
compute = self._reconcile_compute("reconcile-shared-test", list_domains)
refresh_instance_database(compute)
self.assertEqual(outcome, ["ran"])
def test_a_second_reconcile_of_the_compute_is_skipped_meanwhile(self):
"""Two reconciles would read libvirt at different times and could
commit in reverse order, an older listing undoing a newer one."""
import threading
from django.db import connection
from computes.utils import refresh_instance_database
calls = []
def second_reconcile():
try:
refresh_instance_database(compute)
finally:
connection.close()
def list_domains():
calls.append("list")
if len(calls) == 2: # inside the reconcile
thread = threading.Thread(target=second_reconcile)
thread.start()
thread.join(10)
return []
compute = self._reconcile_compute("reconcile-twice-test", list_domains)
refresh_instance_database(compute)
# the second one's pre-check, then no listing of its own
self.assertEqual(calls, ["list", "list", "list"])
def test_a_reconcile_runs_while_a_job_holds_the_compute_shared(self):
from computes.utils import refresh_instance_database
from instances.models import Instance
from operations import claims
from operations.models import Claim, Operation
dom = type("Dom", (), {"UUIDString": lambda self: "33333333-4444-5555-6666-777777777777",
"name": lambda self: "wvc-test-vm"})()
compute = self._reconcile_compute("reconcile-job-test", lambda: [dom])
op = Operation.objects.create(kind="test.job", username="wvc-test-user", submission_key="key-00000001",
state=Operation.RUNNING)
Claim.objects.create(operation=op, resource=claims.compute_key(compute.id), mode=Claim.SHARED)
refresh_instance_database(compute)
self.assertTrue(Instance.objects.filter(compute=compute, name="wvc-test-vm").exists())
def test_libvirt_compute_lock_held_by_another_holder_is_busy(self):
from computes.utils import libvirt_compute_lock
from operations import claims
from operations.models import Claim
compute = Compute.objects.create(
name="thread-lock-test",
hostname="192.0.2.10",
login="test",
password="pwd",
type=CONN_SOCKET,
)
claims.admit({claims.compute_key(compute.id): Claim.EXCLUSIVE}, holder="other")
with self.assertRaises(claims.Busy):
with libvirt_compute_lock(compute):
pass
def test_libvirt_compute_lock_reentrant(self):
from computes.utils import libvirt_compute_lock
compute = Compute.objects.create(
name="reentrant-lock-test",
hostname="192.0.2.10",
login="test",
password="pwd",
type=CONN_SOCKET,
)
executed = []
with libvirt_compute_lock(compute):
executed.append("outer")
with libvirt_compute_lock(compute):
executed.append("inner")
with libvirt_compute_lock(compute):
executed.append("deep")
self.assertEqual(executed, ["outer", "inner", "deep"])
class ComputeInputValidationTestCase(TestCase):
"""hostname and login reach libvirt URIs and ssh unquoted; the form and
the API reject values that would inject a URI parameter or an ssh option."""
def test_form_rejects_injecting_hostname(self):
from computes.forms import SshComputeForm
form = SshComputeForm(
data={"name": "c", "hostname": "192.0.2.1/system?command=/evil", "login": "root", "type": 2}
)
self.assertFalse(form.is_valid())
self.assertIn("hostname", form.errors)
def test_api_rejects_injecting_hostname_and_login(self):
from computes.api.serializers import ComputeSerializer
for field, data in (
("hostname", {"name": "c", "hostname": "192.0.2.1/system?command=/evil&a=", "login": "root", "type": 2}),
("login", {"name": "c", "hostname": "192.0.2.1", "login": "-oProxyCommand=x", "type": 2}),
):
with self.subTest(field=field):
serializer = ComputeSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn(field, serializer.errors)
def test_api_accepts_valid_values(self):
from computes.api.serializers import ComputeSerializer
serializer = ComputeSerializer(
data={"name": "c", "hostname": "host.example.com", "login": "root", "type": 2}
)
self.assertTrue(serializer.is_valid(), serializer.errors)
def test_api_accepts_empty_login_for_tcp(self):
from computes.api.serializers import ComputeSerializer
serializer = ComputeSerializer(data={"name": "c", "hostname": "192.0.2.1", "login": "", "type": 1})
self.assertTrue(serializer.is_valid(), serializer.errors)

10
console/tests/daemons.py Normal file
View file

@ -0,0 +1,10 @@
"""console/novncd, an extensionless script, loaded as a module the tests can patch."""
import importlib.machinery
import importlib.util
from pathlib import Path
NOVNCD_PATH = str(Path(__file__).resolve().parent.parent / "novncd")
_loader = importlib.machinery.SourceFileLoader("novncd_mod", NOVNCD_PATH)
novncd_mod = importlib.util.module_from_spec(importlib.util.spec_from_file_location("novncd_mod", NOVNCD_PATH, loader=_loader))
_loader.exec_module(novncd_mod)

View file

@ -17,7 +17,7 @@ from computes.models import Compute
from console import tokens
from console.tests.test_socketiod_child import consoles, console, start_socketiod, socketio
from instances.models import Instance
from instances.tests import livetest
from vrtManager.tests import livetest
P = livetest.PREFIX

View file

@ -0,0 +1,394 @@
"""novncd, the VNC console proxy: what it checks before it connects a browser, and a
port it cannot listen on ends it with a failure status, which systemd restarts."""
import socket
import subprocess
import sys
from pathlib import Path
from django.test import SimpleTestCase
from unittest.mock import MagicMock, patch
from accounts.models import UserInstance
from computes.models import Compute
from console import tokens
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.core.signing import TimestampSigner
from django.conf import settings
from django.test import TestCase, override_settings
from instances.models import Instance
from console.tests.daemons import novncd_mod
ROOT = Path(__file__).resolve().parent.parent.parent
class NovncdExitTestCase(SimpleTestCase):
def test_a_taken_port_ends_novncd_with_a_failure_status(self):
with socket.socket() as taken:
taken.bind(("127.0.0.1", 0))
taken.listen()
port = taken.getsockname()[1]
novncd = subprocess.run([sys.executable, str(ROOT / "console" / "novncd"), "-v", "-H", "127.0.0.1",
"-p", str(port)], cwd=ROOT, capture_output=True, timeout=60)
self.assertNotEqual(novncd.returncode, 0)
self.assertIn(b"Openening socket failed", novncd.stderr)
class NovncdDaemonLogicTestCase(TestCase):
def setUp(self):
self.get_parser = novncd_mod.get_parser
self.ConsoleClient = novncd_mod.ConsoleClient
user = MagicMock(pk=7)
user.username = "vnc-user"
for name, value in (("authorize", (MagicMock(), user)), ("session_user_id", "7")):
patcher = patch.object(novncd_mod, name, return_value=value)
setattr(self, name, patcher.start())
self.addCleanup(patcher.stop)
patcher = patch.object(novncd_mod.ConsoleClient, "headers", {}, create=True)
patcher.start()
self.addCleanup(patcher.stop)
def test_novncd_parser_defaults(self):
parser = self.get_parser()
opts = parser.parse_args([])
self.assertFalse(opts.verbose)
self.assertFalse(opts.debug)
self.assertEqual(opts.port, 6080)
def test_novncd_parser_custom_arguments(self):
parser = self.get_parser()
opts = parser.parse_args(["-v", "-d", "-H", "192.168.1.100", "-p", "7070"])
self.assertTrue(opts.verbose)
self.assertTrue(opts.debug)
self.assertEqual(opts.host, "192.168.1.100")
self.assertEqual(opts.port, 7070)
@patch.object(novncd_mod, "get_connection_infos")
def test_a_cookie_does_not_choose_the_console(self, mock_get_info):
# another tab's console in a cookie: the URL's own token is used
mock_get_info.return_value = ("host1", "root", 1, "192.168.1.50", 5900, None)
handler = self.ConsoleClient()
handler.headers = {"cookie": "token=1-uuid-of-another-tab"}
handler.path = "/novncd/?token=1-uuid-of-this-tab"
handler.msg = MagicMock()
handler.do_proxy = MagicMock()
handler._new_client(socket_factory=MagicMock())
self.authorize.assert_called_once_with("1-uuid-of-this-tab", "vnc")
@patch.object(novncd_mod, "get_connection_infos")
def test_compatibility_mixin_token_from_query_params(self, mock_get_info):
mock_get_info.return_value = (
"host2",
"root",
1,
"192.168.1.50",
5901,
None,
)
handler = self.ConsoleClient()
handler.headers = {}
handler.path = "/novncd/?token=1-uuid-from-query"
handler.msg = MagicMock()
handler.do_proxy = MagicMock()
socket_factory = MagicMock()
handler._new_client(socket_factory=socket_factory)
self.authorize.assert_called_once_with("1-uuid-from-query", "vnc")
# the address the console listens on
socket_factory.assert_called_once_with("192.168.1.50", 5901, connect=True)
@patch.object(novncd_mod, "get_connection_infos")
def test_a_local_socket_host_reaches_a_console_on_its_loopback(self, mock_get_info):
# libvirt on the panel's host, the VM's console listening on 127.0.0.1
mock_get_info.return_value = ("localhost", "", novncd_mod.CONN_SOCKET, "127.0.0.1", 5902, None)
handler = self.ConsoleClient()
handler.headers = {}
handler.path = "/novncd/?token=1-uuid"
handler.msg = MagicMock()
handler.do_proxy = MagicMock()
socket_factory = MagicMock()
handler._new_client(socket_factory=socket_factory)
socket_factory.assert_called_once_with("127.0.0.1", 5902, connect=True)
def test_an_ssh_hosts_console_always_goes_through_the_tunnel(self):
# whatever address the console listens on, the VNC stream is not sent unencrypted
for console_host, console_socket, target in (
("192.0.2.40", None, "192.0.2.40:5900"),
("0.0.0.0", None, "127.0.0.1:5900"),
("::", None, "[::1]:5900"),
("127.0.0.1", None, "127.0.0.1:5900"),
(None, "/run/libvirt/qemu/vnc.sock", "/run/libvirt/qemu/vnc.sock"),
):
with self.subTest(console_host=console_host), patch.object(
novncd_mod, "get_connection_infos",
return_value=("kvm1.example.com", "root", novncd_mod.CONN_SSH, console_host, 5900, console_socket),
), patch.object(novncd_mod, "open_tunnel", return_value=(MagicMock(), MagicMock())) as open_tunnel:
handler = self.ConsoleClient()
handler.path = "/novncd/?token=1-uuid"
handler.msg = MagicMock()
handler.do_proxy = MagicMock()
socket_factory = MagicMock()
handler._new_client(socket_factory=socket_factory)
open_tunnel.assert_called_once_with("kvm1.example.com", "root", target)
socket_factory.assert_not_called()
# the tunnel ends with the session
open_tunnel.return_value[1].close.assert_called_once_with()
def test_a_tcp_hosts_console_is_reached_where_it_listens(self):
for console_host, reached in (("0.0.0.0", "kvm1.example.com"), ("::", "kvm1.example.com"),
(None, "kvm1.example.com"), ("192.0.2.40", "192.0.2.40")):
with self.subTest(console_host=console_host), patch.object(
novncd_mod, "get_connection_infos",
return_value=("kvm1.example.com", "", 1,
console_host, 5900, None),
):
handler = self.ConsoleClient()
handler.path = "/novncd/?token=1-uuid"
handler.msg = MagicMock()
handler.do_proxy = MagicMock()
socket_factory = MagicMock()
handler._new_client(socket_factory=socket_factory)
socket_factory.assert_called_once_with(reached, 5900, connect=True)
def test_a_tcp_hosts_loopback_console_is_refused(self):
for console_host in ("127.0.0.1", "::1", "localhost"):
with self.subTest(console_host=console_host), patch.object(
novncd_mod, "get_connection_infos", return_value=("kvm1.example.com", "", 1, console_host, 5900, None),
):
handler = self.ConsoleClient()
handler.path = "/novncd/?token=1-uuid"
handler.msg = MagicMock()
socket_factory = MagicMock()
with self.assertRaises(Exception):
handler._new_client(socket_factory=socket_factory)
socket_factory.assert_not_called()
@patch.object(novncd_mod, "get_connection_infos")
def test_compatibility_mixin_missing_token_safe_exit(self, mock_get_info):
handler = self.ConsoleClient()
handler.headers = {}
handler.path = "/novncd/"
handler.msg = MagicMock()
socket_factory = MagicMock()
# a refusal is a warning: it shows in the log without -v
with self.assertLogs(level="WARNING") as log:
handler._new_client(socket_factory=socket_factory)
mock_get_info.assert_not_called()
socket_factory.assert_not_called()
self.assertIn("without a token", log.output[0])
@patch("vrtManager.instance.wvmInstance")
def test_get_connection_infos_reads_the_vm_it_was_given(self, mock_wvm):
mock_instance = MagicMock()
mock_instance.compute.hostname = "127.0.0.1"
mock_instance.compute.login = "root"
mock_instance.compute.password = ""
mock_instance.compute.type = 1
mock_instance.name = "test-vm"
mock_conn = MagicMock()
mock_conn.get_console_listener_addr.return_value = "127.0.0.1"
mock_conn.get_console_port.return_value = 5900
mock_conn.get_console_socket.return_value = None
mock_wvm.return_value = mock_conn
infos = novncd_mod.get_connection_infos(mock_instance)
self.assertEqual(infos[0], "127.0.0.1")
self.assertEqual(infos[4], 5900)
# the VM access was checked for, not whichever VM has its name now
self.assertEqual(mock_wvm.call_args.kwargs["uuid"], mock_instance.uuid)
def test_the_vnc_server_is_waited_for_a_bounded_time(self):
with patch.object(novncd_mod.socket, "create_connection") as create_connection:
tsock = novncd_mod.bounded_connect("192.0.2.40", 5900, connect=True)
create_connection.assert_called_once_with(("192.0.2.40", 5900), timeout=novncd_mod.CONNECT_TIMEOUT)
tsock.settimeout.assert_called_once_with(None)
def test_a_token_without_its_users_session_is_refused_and_logged(self):
from logs.models import Logs
self.session_user_id.return_value = None # read from a log, used in another browser
handler = self.ConsoleClient()
handler.headers = {"Cookie": "sessionid=another", "X-Real-IP": "192.0.2.9"}
handler.path = "/novncd/?token=1-uuid"
handler.msg = MagicMock()
socket_factory = MagicMock()
with patch.object(novncd_mod, "get_connection_infos") as get_info, self.assertLogs(level="WARNING"):
handler._new_client(socket_factory=socket_factory)
get_info.assert_not_called()
socket_factory.assert_not_called()
self.session_user_id.assert_called_once_with("sessionid=another")
row = Logs.objects.get()
self.assertEqual((row.user, row.ip, row.success), ("vnc-user", "192.0.2.9", False))
self.assertEqual(row.message, "VNC console refused: The console page was opened in another session")
def test_the_session_names_its_user(self):
from django.contrib.sessions.backends.db import SessionStore
user = get_user_model().objects.create_user("vnc-session", password="pw")
self.client.force_login(user)
cookie = f"{settings.SESSION_COOKIE_NAME}={self.client.session.session_key}; other=1"
self.assertEqual(tokens.session_user_id(cookie), str(user.pk))
self.assertIsNone(tokens.session_user_id(f"{settings.SESSION_COOKIE_NAME}={SessionStore().session_key}"))
self.assertIsNone(tokens.session_user_id(None))
def test_a_vnc_session_ends_when_its_user_may_no_longer_open_it(self):
tsock, ended = MagicMock(), MagicMock()
ended.wait.return_value = False # ACCESS_CHECK passed, the session still open
vm = MagicMock()
with patch.object(novncd_mod, "may_still_open", side_effect=[True, False]) as may_still_open:
novncd_mod.end_when_refused(vm, 7, tsock, ended)
self.assertEqual(may_still_open.call_count, 2)
may_still_open.assert_called_with(7, vm)
tsock.shutdown.assert_called_once_with(novncd_mod.socket.SHUT_RDWR)
def test_a_token_of_a_serial_console_does_not_open_a_vnc_console(self):
user = get_user_model().objects.create_user("vnc-owner", password="pw")
compute = Compute.objects.create(name="novnc-type-compute", hostname="127.0.0.1", login="root", password="", type=1)
instance = Instance.objects.create(compute=compute, name="novnc-type-vm", uuid="77777777-7777-7777-7777-777777777777")
UserInstance.objects.create(instance=instance, user=user)
self.assertEqual(tokens.authorize(tokens.sign(compute.id, instance.uuid, user.id, "vnc"), "vnc"), (instance, user))
with self.assertRaises(PermissionError):
tokens.authorize(tokens.sign(compute.id, instance.uuid, user.id, "pty"), "vnc")
@patch("sys.argv", ["novncd"])
@override_settings(DEBUG=False)
def test_vnc_token_invalid_token_in_production(self):
with self.assertRaises(PermissionError):
tokens.authorize("invalid-raw-token", "vnc")
def test_vnc_token_unauthorized_user(self):
User = get_user_model()
unauthorized_user = User.objects.create_user(
username="unauthorized_console_user",
password="password",
)
compute = Compute.objects.create(
name="novnc-unauth-compute",
hostname="127.0.0.1",
login="root",
password="",
type=1,
)
instance = Instance.objects.create(
compute=compute,
name="novnc-unauth-vm",
uuid="33333333-3333-3333-3333-333333333333",
)
token = tokens.sign(compute.id, instance.uuid, unauthorized_user.id, "vnc")
with self.assertRaises(PermissionError):
tokens.authorize(token, "vnc")
def test_vnc_token_rejects_signed_token_without_user(self):
# The console view always signs host:uuid:user_id; a token without the
# user part would skip the per-user access check entirely.
compute = Compute.objects.create(
name="novnc-2part-compute", hostname="127.0.0.1", login="root", password="", type=1
)
instance = Instance.objects.create(
compute=compute,
name="novnc-2part-vm",
uuid="66666666-6666-6666-6666-666666666666",
)
signer = TimestampSigner(salt="console.novnc")
token = signer.sign(f"{compute.id}:{instance.uuid}")
with self.assertRaises(PermissionError):
tokens.authorize(token, "vnc")
def test_vnc_token_rejects_unsigned_token_even_under_test_runner(self):
# sys.argv contains "test" while this suite runs; production code must
# not change behaviour based on that.
compute = Compute.objects.create(
name="novnc-unsigned-compute", hostname="127.0.0.1", login="root", password="", type=1
)
instance = Instance.objects.create(
compute=compute,
name="novnc-unsigned-vm",
uuid="88888888-8888-8888-8888-888888888888",
)
with self.assertRaises(PermissionError):
tokens.authorize(f"{compute.id}-{instance.uuid}", "vnc")
def test_vnc_token_view_instances_user_is_not_enough(self):
User = get_user_model()
viewer = User.objects.create_user(username="novnc_viewer", password="password")
viewer.user_permissions.add(Permission.objects.get(codename="view_instances"))
compute = Compute.objects.create(
name="novnc-viewer-compute", hostname="127.0.0.1", login="root", password="", type=1
)
instance = Instance.objects.create(
compute=compute,
name="novnc-viewer-vm",
uuid="55555555-5555-5555-5555-555555555555",
)
token = tokens.sign(compute.id, instance.uuid, viewer.id, "vnc")
with self.assertRaises(PermissionError):
tokens.authorize(token, "vnc")
def test_vnc_token_inactive_user(self):
User = get_user_model()
inactive_user = User.objects.create_user(
username="inactive_console_user",
password="password",
is_active=False,
)
compute = Compute.objects.create(
name="novnc-inactive-compute",
hostname="127.0.0.1",
login="root",
password="",
type=1,
)
instance = Instance.objects.create(
compute=compute,
name="novnc-inactive-vm",
uuid="44444444-4444-4444-4444-444444444444",
)
UserInstance.objects.create(instance=instance, user=inactive_user)
token = tokens.sign(compute.id, instance.uuid, inactive_user.id, "vnc")
with self.assertRaises(PermissionError):
tokens.authorize(token, "vnc")
@patch("sys.argv", ["novncd"])
@override_settings(DEBUG=True)
def test_vnc_token_debug_true_does_not_bypass(self):
with self.assertRaises(PermissionError):
tokens.authorize("invalid-token-in-debug", "vnc")
@patch("sys.argv", ["novncd"])
def test_vnc_token_expired_token(self):
User = get_user_model()
user = User.objects.filter(is_superuser=True).first()
compute = Compute.objects.create(
name="novnc-expired-compute",
hostname="127.0.0.1",
login="root",
password="",
type=1,
)
instance = Instance.objects.create(
compute=compute,
name="novnc-expired-vm",
uuid="55555555-5555-5555-5555-555555555555",
)
import time
from unittest.mock import patch
# Sign token 300 seconds in the past (> 120s TTL)
with patch("time.time", return_value=time.time() - 300):
expired_token = tokens.sign(compute.id, instance.uuid, user.id, "vnc")
with self.assertRaises(PermissionError):
tokens.authorize(expired_token, "vnc")

View file

@ -1,22 +0,0 @@
"""novncd that cannot listen ends with a failure status, which systemd restarts."""
import socket
import subprocess
import sys
from pathlib import Path
from django.test import SimpleTestCase
ROOT = Path(__file__).resolve().parent.parent.parent
class NovncdExitTestCase(SimpleTestCase):
def test_a_taken_port_ends_novncd_with_a_failure_status(self):
with socket.socket() as taken:
taken.bind(("127.0.0.1", 0))
taken.listen()
port = taken.getsockname()[1]
novncd = subprocess.run([sys.executable, str(ROOT / "console" / "novncd"), "-v", "-H", "127.0.0.1",
"-p", str(port)], cwd=ROOT, capture_output=True, timeout=60)
self.assertNotEqual(novncd.returncode, 0)
self.assertIn(b"Openening socket failed", novncd.stderr)

View file

@ -1,14 +1,11 @@
# pylint: disable=no-member
import importlib.machinery
import json
import importlib.util
from pathlib import Path
from unittest.mock import MagicMock, patch
from accounts.models import UserInstance
from appsettings.models import AppSettings
from computes.models import Compute
from console import tokens
from console.views import LAYOUTS_DIR
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
@ -20,17 +17,6 @@ from instances.models import Instance
from libvirt import libvirtError
from lxml import html
# Dynamically load console/novncd script for testing
NOVNCD_PATH = str(Path(__file__).resolve().parent.parent / "novncd")
_loader = importlib.machinery.SourceFileLoader("novncd_mod", NOVNCD_PATH)
_spec = importlib.util.spec_from_file_location(
"novncd_mod", NOVNCD_PATH, loader=_loader
)
assert _spec is not None
assert _spec.loader is not None
novncd_mod = importlib.util.module_from_spec(_spec)
_loader.exec_module(novncd_mod)
def vnc_config(response):
"""What the VNC page's script starts from."""
@ -400,363 +386,3 @@ class ConsoleViewsTestCase(TestCase):
content,
f"Expected key element id '{key_id}' in console menu",
)
class NovncdDaemonLogicTestCase(TestCase):
def setUp(self):
self.get_parser = novncd_mod.get_parser
self.ConsoleClient = novncd_mod.ConsoleClient
user = MagicMock(pk=7)
user.username = "vnc-user"
for name, value in (("authorize", (MagicMock(), user)), ("session_user_id", "7")):
patcher = patch.object(novncd_mod, name, return_value=value)
setattr(self, name, patcher.start())
self.addCleanup(patcher.stop)
patcher = patch.object(novncd_mod.ConsoleClient, "headers", {}, create=True)
patcher.start()
self.addCleanup(patcher.stop)
def test_novncd_parser_defaults(self):
parser = self.get_parser()
opts = parser.parse_args([])
self.assertFalse(opts.verbose)
self.assertFalse(opts.debug)
self.assertEqual(opts.port, 6080)
def test_novncd_parser_custom_arguments(self):
parser = self.get_parser()
opts = parser.parse_args(["-v", "-d", "-H", "192.168.1.100", "-p", "7070"])
self.assertTrue(opts.verbose)
self.assertTrue(opts.debug)
self.assertEqual(opts.host, "192.168.1.100")
self.assertEqual(opts.port, 7070)
@patch.object(novncd_mod, "get_connection_infos")
def test_a_cookie_does_not_choose_the_console(self, mock_get_info):
# another tab's console in a cookie: the URL's own token is used
mock_get_info.return_value = ("host1", "root", 1, "192.168.1.50", 5900, None)
handler = self.ConsoleClient()
handler.headers = {"cookie": "token=1-uuid-of-another-tab"}
handler.path = "/novncd/?token=1-uuid-of-this-tab"
handler.msg = MagicMock()
handler.do_proxy = MagicMock()
handler._new_client(socket_factory=MagicMock())
self.authorize.assert_called_once_with("1-uuid-of-this-tab", "vnc")
@patch.object(novncd_mod, "get_connection_infos")
def test_compatibility_mixin_token_from_query_params(self, mock_get_info):
mock_get_info.return_value = (
"host2",
"root",
1,
"192.168.1.50",
5901,
None,
)
handler = self.ConsoleClient()
handler.headers = {}
handler.path = "/novncd/?token=1-uuid-from-query"
handler.msg = MagicMock()
handler.do_proxy = MagicMock()
socket_factory = MagicMock()
handler._new_client(socket_factory=socket_factory)
self.authorize.assert_called_once_with("1-uuid-from-query", "vnc")
# the address the console listens on
socket_factory.assert_called_once_with("192.168.1.50", 5901, connect=True)
@patch.object(novncd_mod, "get_connection_infos")
def test_a_local_socket_host_reaches_a_console_on_its_loopback(self, mock_get_info):
# libvirt on the panel's host, the VM's console listening on 127.0.0.1
mock_get_info.return_value = ("localhost", "", novncd_mod.CONN_SOCKET, "127.0.0.1", 5902, None)
handler = self.ConsoleClient()
handler.headers = {}
handler.path = "/novncd/?token=1-uuid"
handler.msg = MagicMock()
handler.do_proxy = MagicMock()
socket_factory = MagicMock()
handler._new_client(socket_factory=socket_factory)
socket_factory.assert_called_once_with("127.0.0.1", 5902, connect=True)
def test_an_ssh_hosts_console_always_goes_through_the_tunnel(self):
# whatever address the console listens on, the VNC stream is not sent unencrypted
for console_host, console_socket, target in (
("192.0.2.40", None, "192.0.2.40:5900"),
("0.0.0.0", None, "127.0.0.1:5900"),
("::", None, "[::1]:5900"),
("127.0.0.1", None, "127.0.0.1:5900"),
(None, "/run/libvirt/qemu/vnc.sock", "/run/libvirt/qemu/vnc.sock"),
):
with self.subTest(console_host=console_host), patch.object(
novncd_mod, "get_connection_infos",
return_value=("kvm1.example.com", "root", novncd_mod.CONN_SSH, console_host, 5900, console_socket),
), patch.object(novncd_mod, "open_tunnel", return_value=(MagicMock(), MagicMock())) as open_tunnel:
handler = self.ConsoleClient()
handler.path = "/novncd/?token=1-uuid"
handler.msg = MagicMock()
handler.do_proxy = MagicMock()
socket_factory = MagicMock()
handler._new_client(socket_factory=socket_factory)
open_tunnel.assert_called_once_with("kvm1.example.com", "root", target)
socket_factory.assert_not_called()
# the tunnel ends with the session
open_tunnel.return_value[1].close.assert_called_once_with()
def test_a_tcp_hosts_console_is_reached_where_it_listens(self):
for console_host, reached in (("0.0.0.0", "kvm1.example.com"), ("::", "kvm1.example.com"),
(None, "kvm1.example.com"), ("192.0.2.40", "192.0.2.40")):
with self.subTest(console_host=console_host), patch.object(
novncd_mod, "get_connection_infos",
return_value=("kvm1.example.com", "", 1,
console_host, 5900, None),
):
handler = self.ConsoleClient()
handler.path = "/novncd/?token=1-uuid"
handler.msg = MagicMock()
handler.do_proxy = MagicMock()
socket_factory = MagicMock()
handler._new_client(socket_factory=socket_factory)
socket_factory.assert_called_once_with(reached, 5900, connect=True)
def test_a_tcp_hosts_loopback_console_is_refused(self):
for console_host in ("127.0.0.1", "::1", "localhost"):
with self.subTest(console_host=console_host), patch.object(
novncd_mod, "get_connection_infos", return_value=("kvm1.example.com", "", 1, console_host, 5900, None),
):
handler = self.ConsoleClient()
handler.path = "/novncd/?token=1-uuid"
handler.msg = MagicMock()
socket_factory = MagicMock()
with self.assertRaises(Exception):
handler._new_client(socket_factory=socket_factory)
socket_factory.assert_not_called()
@patch.object(novncd_mod, "get_connection_infos")
def test_compatibility_mixin_missing_token_safe_exit(self, mock_get_info):
handler = self.ConsoleClient()
handler.headers = {}
handler.path = "/novncd/"
handler.msg = MagicMock()
socket_factory = MagicMock()
# a refusal is a warning: it shows in the log without -v
with self.assertLogs(level="WARNING") as log:
handler._new_client(socket_factory=socket_factory)
mock_get_info.assert_not_called()
socket_factory.assert_not_called()
self.assertIn("without a token", log.output[0])
@patch("vrtManager.instance.wvmInstance")
def test_get_connection_infos_reads_the_vm_it_was_given(self, mock_wvm):
mock_instance = MagicMock()
mock_instance.compute.hostname = "127.0.0.1"
mock_instance.compute.login = "root"
mock_instance.compute.password = ""
mock_instance.compute.type = 1
mock_instance.name = "test-vm"
mock_conn = MagicMock()
mock_conn.get_console_listener_addr.return_value = "127.0.0.1"
mock_conn.get_console_port.return_value = 5900
mock_conn.get_console_socket.return_value = None
mock_wvm.return_value = mock_conn
infos = novncd_mod.get_connection_infos(mock_instance)
self.assertEqual(infos[0], "127.0.0.1")
self.assertEqual(infos[4], 5900)
# the VM access was checked for, not whichever VM has its name now
self.assertEqual(mock_wvm.call_args.kwargs["uuid"], mock_instance.uuid)
def test_the_vnc_server_is_waited_for_a_bounded_time(self):
with patch.object(novncd_mod.socket, "create_connection") as create_connection:
tsock = novncd_mod.bounded_connect("192.0.2.40", 5900, connect=True)
create_connection.assert_called_once_with(("192.0.2.40", 5900), timeout=novncd_mod.CONNECT_TIMEOUT)
tsock.settimeout.assert_called_once_with(None)
def test_a_token_without_its_users_session_is_refused_and_logged(self):
from logs.models import Logs
self.session_user_id.return_value = None # read from a log, used in another browser
handler = self.ConsoleClient()
handler.headers = {"Cookie": "sessionid=another", "X-Real-IP": "192.0.2.9"}
handler.path = "/novncd/?token=1-uuid"
handler.msg = MagicMock()
socket_factory = MagicMock()
with patch.object(novncd_mod, "get_connection_infos") as get_info, self.assertLogs(level="WARNING"):
handler._new_client(socket_factory=socket_factory)
get_info.assert_not_called()
socket_factory.assert_not_called()
self.session_user_id.assert_called_once_with("sessionid=another")
row = Logs.objects.get()
self.assertEqual((row.user, row.ip, row.success), ("vnc-user", "192.0.2.9", False))
self.assertEqual(row.message, "VNC console refused: The console page was opened in another session")
def test_the_session_names_its_user(self):
from django.contrib.sessions.backends.db import SessionStore
user = get_user_model().objects.create_user("vnc-session", password="pw")
self.client.force_login(user)
cookie = f"{settings.SESSION_COOKIE_NAME}={self.client.session.session_key}; other=1"
self.assertEqual(tokens.session_user_id(cookie), str(user.pk))
self.assertIsNone(tokens.session_user_id(f"{settings.SESSION_COOKIE_NAME}={SessionStore().session_key}"))
self.assertIsNone(tokens.session_user_id(None))
def test_a_vnc_session_ends_when_its_user_may_no_longer_open_it(self):
tsock, ended = MagicMock(), MagicMock()
ended.wait.return_value = False # ACCESS_CHECK passed, the session still open
vm = MagicMock()
with patch.object(novncd_mod, "may_still_open", side_effect=[True, False]) as may_still_open:
novncd_mod.end_when_refused(vm, 7, tsock, ended)
self.assertEqual(may_still_open.call_count, 2)
may_still_open.assert_called_with(7, vm)
tsock.shutdown.assert_called_once_with(novncd_mod.socket.SHUT_RDWR)
def test_a_token_of_a_serial_console_does_not_open_a_vnc_console(self):
user = get_user_model().objects.create_user("vnc-owner", password="pw")
compute = Compute.objects.create(name="novnc-type-compute", hostname="127.0.0.1", login="root", password="", type=1)
instance = Instance.objects.create(compute=compute, name="novnc-type-vm", uuid="77777777-7777-7777-7777-777777777777")
UserInstance.objects.create(instance=instance, user=user)
self.assertEqual(tokens.authorize(tokens.sign(compute.id, instance.uuid, user.id, "vnc"), "vnc"), (instance, user))
with self.assertRaises(PermissionError):
tokens.authorize(tokens.sign(compute.id, instance.uuid, user.id, "pty"), "vnc")
@patch("sys.argv", ["novncd"])
@override_settings(DEBUG=False)
def test_vnc_token_invalid_token_in_production(self):
with self.assertRaises(PermissionError):
tokens.authorize("invalid-raw-token", "vnc")
def test_vnc_token_unauthorized_user(self):
User = get_user_model()
unauthorized_user = User.objects.create_user(
username="unauthorized_console_user",
password="password",
)
compute = Compute.objects.create(
name="novnc-unauth-compute",
hostname="127.0.0.1",
login="root",
password="",
type=1,
)
instance = Instance.objects.create(
compute=compute,
name="novnc-unauth-vm",
uuid="33333333-3333-3333-3333-333333333333",
)
token = tokens.sign(compute.id, instance.uuid, unauthorized_user.id, "vnc")
with self.assertRaises(PermissionError):
tokens.authorize(token, "vnc")
def test_vnc_token_rejects_signed_token_without_user(self):
# The console view always signs host:uuid:user_id; a token without the
# user part would skip the per-user access check entirely.
compute = Compute.objects.create(
name="novnc-2part-compute", hostname="127.0.0.1", login="root", password="", type=1
)
instance = Instance.objects.create(
compute=compute,
name="novnc-2part-vm",
uuid="66666666-6666-6666-6666-666666666666",
)
signer = TimestampSigner(salt="console.novnc")
token = signer.sign(f"{compute.id}:{instance.uuid}")
with self.assertRaises(PermissionError):
tokens.authorize(token, "vnc")
def test_vnc_token_rejects_unsigned_token_even_under_test_runner(self):
# sys.argv contains "test" while this suite runs; production code must
# not change behaviour based on that.
compute = Compute.objects.create(
name="novnc-unsigned-compute", hostname="127.0.0.1", login="root", password="", type=1
)
instance = Instance.objects.create(
compute=compute,
name="novnc-unsigned-vm",
uuid="88888888-8888-8888-8888-888888888888",
)
with self.assertRaises(PermissionError):
tokens.authorize(f"{compute.id}-{instance.uuid}", "vnc")
def test_vnc_token_view_instances_user_is_not_enough(self):
User = get_user_model()
viewer = User.objects.create_user(username="novnc_viewer", password="password")
viewer.user_permissions.add(Permission.objects.get(codename="view_instances"))
compute = Compute.objects.create(
name="novnc-viewer-compute", hostname="127.0.0.1", login="root", password="", type=1
)
instance = Instance.objects.create(
compute=compute,
name="novnc-viewer-vm",
uuid="55555555-5555-5555-5555-555555555555",
)
token = tokens.sign(compute.id, instance.uuid, viewer.id, "vnc")
with self.assertRaises(PermissionError):
tokens.authorize(token, "vnc")
def test_vnc_token_inactive_user(self):
User = get_user_model()
inactive_user = User.objects.create_user(
username="inactive_console_user",
password="password",
is_active=False,
)
compute = Compute.objects.create(
name="novnc-inactive-compute",
hostname="127.0.0.1",
login="root",
password="",
type=1,
)
instance = Instance.objects.create(
compute=compute,
name="novnc-inactive-vm",
uuid="44444444-4444-4444-4444-444444444444",
)
UserInstance.objects.create(instance=instance, user=inactive_user)
token = tokens.sign(compute.id, instance.uuid, inactive_user.id, "vnc")
with self.assertRaises(PermissionError):
tokens.authorize(token, "vnc")
@patch("sys.argv", ["novncd"])
@override_settings(DEBUG=True)
def test_vnc_token_debug_true_does_not_bypass(self):
with self.assertRaises(PermissionError):
tokens.authorize("invalid-token-in-debug", "vnc")
@patch("sys.argv", ["novncd"])
def test_vnc_token_expired_token(self):
User = get_user_model()
user = User.objects.filter(is_superuser=True).first()
compute = Compute.objects.create(
name="novnc-expired-compute",
hostname="127.0.0.1",
login="root",
password="",
type=1,
)
instance = Instance.objects.create(
compute=compute,
name="novnc-expired-vm",
uuid="55555555-5555-5555-5555-555555555555",
)
import time
from unittest.mock import patch
# Sign token 300 seconds in the past (> 120s TTL)
with patch("time.time", return_value=time.time() - 300):
expired_token = tokens.sign(compute.id, instance.uuid, user.id, "vnc")
with self.assertRaises(PermissionError):
tokens.authorize(expired_token, "vnc")

View file

@ -28,3 +28,9 @@ def render_create_page(testcase, data=None):
else:
response = testcase.client.post(url, {"create": "", **data})
return compute, response
# an API create request (CreateInstanceSerializer) that the job takes
BODY = {"name": "api-vm", "vcpu": 1, "vcpu_mode": "host-model", "memory": 512, "networks": "default", "mac": "",
"nwfilter": "", "storage": "default", "hdd_size": 1, "cache_mode": "none", "meta_prealloc": False,
"virtio": True, "qemu_ga": True, "video": "vga", "listener_addr": "0.0.0.0", "submission_key": "key-api00001"}

View file

@ -16,7 +16,7 @@ from operations.jobs import Context
from operations.models import Operation, acquire
from vrtManager.util import OperationError
from instances.tests.test_create_job import BODY
from instances.tests.create_page import BODY
SEED = "/srv/default/cidata-x.iso"

View file

@ -11,7 +11,6 @@ from django.contrib.messages import get_messages
from django.test import TestCase, override_settings
from django.urls import reverse
from instances.models import Instance
from vrtManager.instance import PERSISTENT_XML, wvmInstance
class ConsoleListenerTests(TestCase):
@ -83,29 +82,3 @@ class ConsoleListenerTests(TestCase):
response = self.client.get(reverse("instances:instance", args=[self.vm.id]))
form = response.context["console_form"]
self.assertTrue(form.fields["listen_on"].disabled)
class ConsoleListensLocallyTests(TestCase):
"""Only a binding the persistent XML pins to loopback (or a Unix socket) is local."""
def listens_locally(self, graphics):
proxy = wvmInstance.__new__(wvmInstance) # no connection: only the XML is read
xml = f"<domain><devices>{graphics}</devices></domain>"
proxy._XMLDesc = lambda flags: xml if flags == PERSISTENT_XML else "<wrong-flags/>"
return proxy.console_listens_locally()
def test_cases(self):
cases = {
"<graphics type='vnc' listen='127.0.0.1'><listen type='address' address='127.0.0.1'/></graphics>": True,
"<graphics type='vnc'><listen type='address' address='::1'/></graphics>": True,
"<graphics type='vnc'><listen type='socket' socket='/run/vnc.sock'/></graphics>": True,
"": True, # no console
"<graphics type='vnc' listen='0.0.0.0'><listen type='address' address='0.0.0.0'/></graphics>": False,
"<graphics type='vnc'><listen type='network' network='default'/></graphics>": False,
"<graphics type='vnc' autoport='yes'/>": False, # the host default decides
"<graphics type='vnc'><listen type='address' address='localhost'/></graphics>": False,
"<graphics type='vnc' listen='127.0.0.1'><listen type='address' address='10.0.0.5'/></graphics>": False,
}
for graphics, expected in cases.items():
with self.subTest(graphics=graphics):
self.assertIs(self.listens_locally(graphics), expected)

View file

@ -2,43 +2,12 @@
model, checked by libvirt before the start. That the host offers it is
checked when the job is submitted (test_create_job)."""
from unittest.mock import MagicMock, patch
from appsettings.models import AppSettings
from appsettings.settings import get_settings
from django.test import SimpleTestCase, TestCase
from django.test import TestCase
from instances.tests.create_page import render_create_page
from lxml import etree, html
from vrtManager.create import wvmCreate
DOM_CAPS = {"domain": "kvm", "os_support": "no", "cpu_modes": ["host-passthrough", "maximum", "host-model", "custom"],
"cpu_custom_models": ["Skylake-Server-noTSX-IBRS", "SandyBridge-IBRS"]}
def defined_cpu(vcpu_mode):
conn = wvmCreate.__new__(wvmCreate)
conn.wvm = MagicMock()
with patch.object(wvmCreate, "get_capabilities", return_value={"features": []}), \
patch.object(wvmCreate, "get_dom_capabilities", return_value=DOM_CAPS):
conn.create_instance(name="vm-1", memory=512, vcpu=1, vcpu_mode=vcpu_mode,
uuid="00000000-0000-4000-8000-0000000000c1", arch="x86_64", machine="q35",
firmware="BIOS", volumes=[], networks="", nwfilter="", virtio=True, listener_addr="0.0.0.0",
add_cdrom="None", add_input="None")
return etree.fromstring(conn.wvm.defineXML.call_args.args[0]).find("cpu")
class CpuModeTests(SimpleTestCase):
def test_a_mode_is_written_as_the_mode(self):
for mode in ("host-model", "host-passthrough", "maximum"):
with self.subTest(mode=mode):
cpu = defined_cpu(mode)
self.assertEqual((dict(cpu.attrib), len(cpu)), ({"mode": mode}, 0))
def test_a_named_model_is_that_model_checked_by_libvirt(self):
# a name longer than 20 characters too
cpu = defined_cpu("Skylake-Server-noTSX-IBRS")
self.assertEqual(dict(cpu.attrib), {"mode": "custom", "match": "exact"}) # no check='none'
self.assertEqual((cpu.findtext("model"), cpu.find("model").get("fallback")), ("Skylake-Server-noTSX-IBRS", "forbid"))
from lxml import html
class CpuModeSelectTests(TestCase):

View file

@ -13,13 +13,10 @@ from appsettings.settings import get_settings
from computes.models import Compute
from instances.jobs import CreateInstance
from instances.models import Instance
from instances.tests.create_page import BODY
from operations.jobs import Context
from operations.models import Operation, acquire
BODY = {"name": "api-vm", "vcpu": 1, "vcpu_mode": "host-model", "memory": 512, "networks": "default", "mac": "",
"nwfilter": "", "storage": "default", "hdd_size": 1, "cache_mode": "none", "meta_prealloc": False,
"virtio": True, "qemu_ga": True, "video": "vga", "listener_addr": "0.0.0.0", "submission_key": "key-api00001"}
class CreateJobTestCase(TransactionTestCase):
def setUp(self):

View file

@ -3,274 +3,22 @@ named once whether the running VM or its next start has it; the checks that
keep a device to one VM and away from what the host uses itself, also for a
snapshot revert; clones get none, and a VM with any is not migrated."""
import unittest
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.test import TestCase
from django.urls import reverse
from instances.models import Instance
from lxml import etree, html
from nodedevs.tests.test_nodedevs import NODES, node
from vrtManager.instance import wvmInstance
from vrtManager.util import OperationError
from vrtManager.tests.test_migration import FakeSource, destination, with_disks
MIGRATABLE = libvirt.VIR_DOMAIN_XML_MIGRATABLE
CONTROLLER = "<controller type='usb' model='qemu-xhci'/>"
PCI_05 = ("<hostdev mode='subsystem' type='pci' managed='yes'><source>"
"<address domain='0x0000' bus='0x05' slot='0x00' function='0x0'/></source></hostdev>")
BY_MODEL = "<hostdev mode='subsystem' type='usb' managed='yes'><source{auto}><vendor id='0x0627'/><product id='0x0001'/>{address}</source></hostdev>"
def by_model(address="", auto=""):
return BY_MODEL.format(address=address, auto=auto)
def domain(*devices):
return f"<domain><name>kiosk-1</name><devices>{''.join(devices)}</devices></domain>"
def vm(running, saved, active=True, migratable=None):
"""A wvmInstance whose running VM is running, as XMLDesc gives it (and as
the migratable XML marks it), and whose next start is saved."""
proxy = wvmInstance.__new__(wvmInstance)
proxy._read_cache = None
proxy.instance = MagicMock(**{"isActive.return_value": active, "hasManagedSaveImage.return_value": 0,
"info.return_value": [libvirt.VIR_DOMAIN_RUNNING if active else libvirt.VIR_DOMAIN_SHUTOFF]})
def describe(flags=0):
if flags & libvirt.VIR_DOMAIN_XML_INACTIVE:
return saved
return migratable if flags & MIGRATABLE and migratable else running
proxy.instance.XMLDesc.side_effect = describe
return proxy
class UsbHostdevTests(SimpleTestCase):
def test_a_device_chosen_by_model_is_one_row_with_the_device_it_holds(self):
# the running VM writes the address libvirt resolved; only its migratable XML marks it
proxy = vm(domain(CONTROLLER, by_model("<address bus='1' device='3'/>")), domain(CONTROLLER, by_model()),
migratable=domain(CONTROLLER, by_model("<address bus='1' device='3'/>", " autoAddress='yes'")))
usb, pci, others = proxy.get_hostdevs()
self.assertEqual(usb, [{"vendor": "0x0627", "product": "0x0001", "address": None, "now": (1, 3), "next": True}])
self.assertEqual((pci, others), ([], []))
def test_a_device_is_taken_from_each_definition_as_it_describes_it(self):
proxy = vm(domain(CONTROLLER, by_model("<address bus='1' device='3'/>")), domain(CONTROLLER, by_model()),
migratable=domain(CONTROLLER, by_model("<address bus='1' device='3'/>", " autoAddress='yes'")))
proxy.delete_usb_hostdev(("0x0627", "0x0001", None))
live, config = proxy.instance.detachDeviceFlags.call_args_list
self.assertEqual(live.args[1], libvirt.VIR_DOMAIN_AFFECT_LIVE)
self.assertEqual(etree.fromstring(live.args[0]).xpath("string(source/address/@device)"), "3")
self.assertEqual((config.args[1], etree.fromstring(config.args[0]).find("source/address")),
(libvirt.VIR_DOMAIN_AFFECT_CONFIG, None))
with self.assertRaisesRegex(OperationError, "no such USB device"):
proxy.delete_usb_hostdev(("0x0627", "0x0001", (1, 2)))
def test_a_device_is_given_by_model_or_by_address_with_a_usb_controller(self):
proxy = vm(domain(CONTROLLER), domain(CONTROLLER))
proxy.add_usb_hostdev("0x0627", "0x0001", (1, 3))
added = etree.fromstring(proxy.instance.attachDeviceFlags.call_args.args[0])
self.assertEqual((added.get("type"), added.xpath("string(source/vendor/@id)"), added.xpath("string(source/address/@device)")),
("usb", "0x0627", "3"))
for xml in (domain(), domain("<controller type='usb' model='none'/>")):
proxy = vm(xml, xml, active=False)
with self.subTest(xml=xml), self.assertRaisesRegex(OperationError, "no USB controller"):
proxy.add_usb_hostdev("0x0627", "0x0001")
proxy.instance.attachDeviceFlags.assert_not_called()
def test_a_device_the_host_has_one_of_is_given_by_model(self):
proxy = vm(domain(CONTROLLER), domain(CONTROLLER))
proxy.add_usb_hostdev("0x0627", "0x0001")
self.assertIsNone(etree.fromstring(proxy.instance.attachDeviceFlags.call_args.args[0]).find("source/address"))
def test_other_host_devices_as_the_running_vm_and_its_next_start_have_them(self):
pci = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x0a' function='0x0'/></source></hostdev>"
nic = "<interface type='hostdev'><source><address type='pci' domain='0x0000' bus='0x00' slot='0x0b' function='0x0'/></source></interface>"
network_nic = ("<interface type='network'><source network='sriov'/><actual type='hostdev'><source>"
"<address type='pci' domain='0x0000' bus='0x00' slot='0x0c' function='0x1'/></source></actual></interface>")
mdev = "<hostdev mode='subsystem' type='mdev'><source><address uuid='{}'/></source></hostdev>"
running = domain(pci, nic, network_nic, mdev.format("00000000-0000-4000-8000-00000000a001"))
_, pci_rows, others = vm(running, domain(nic, mdev.format("00000000-0000-4000-8000-00000000a002"))).get_hostdevs()
self.assertEqual(pci_rows, [{"address": "0000:00:0a.0", "now": True, "next": False, "managed": False}])
self.assertEqual(others, [{"type": "nic", "address": "0000:00:0b.0", "now": True, "next": True},
{"type": "nic", "address": "0000:00:0c.1", "now": True, "next": False},
{"type": "mdev", "address": "00000000-0000-4000-8000-00000000a001", "now": True, "next": False},
{"type": "mdev", "address": "00000000-0000-4000-8000-00000000a002", "now": False, "next": True}])
def test_a_shut_off_vm_gets_a_pci_device_for_its_next_start(self):
proxy = vm("", domain(), active=False)
proxy.add_pci_hostdev("0000:05:00.0")
xml, flags = proxy.instance.attachDeviceFlags.call_args.args
self.assertEqual(flags, libvirt.VIR_DOMAIN_AFFECT_CONFIG)
self.assertEqual((etree.fromstring(xml).get("managed"), dict(etree.fromstring(xml).find("source/address").attrib)),
("yes", {"domain": "0x0000", "bus": "0x05", "slot": "0x00", "function": "0x0"}))
proxy = vm("", domain(PCI_05), active=False)
self.assertFalse(proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual([c.args[1] for c in proxy.instance.detachDeviceFlags.call_args_list], [libvirt.VIR_DOMAIN_AFFECT_CONFIG])
with self.assertRaisesRegex(OperationError, "no PCI device 0000:06:00.0"):
proxy.delete_pci_hostdev("0000:06:00.0")
def test_a_paused_vm_or_one_with_a_saved_state_keeps_its_pci_devices(self):
paused = vm(domain(PCI_05), domain(PCI_05))
paused.instance.info.return_value = [libvirt.VIR_DOMAIN_PAUSED]
saved = vm("", domain(PCI_05), active=False)
saved.instance.hasManagedSaveImage.return_value = 1
for name, proxy in (("paused", paused), ("saved", saved)):
for change in (proxy.add_pci_hostdev, proxy.delete_pci_hostdev):
with self.subTest(vm=name, change=change.__name__), self.assertRaisesRegex(OperationError, "runs, or is shut off"):
change("0000:05:00.0")
proxy.instance.attachDeviceFlags.assert_not_called()
proxy.instance.detachDeviceFlags.assert_not_called()
def test_a_nic_that_is_a_host_device_is_left_to_the_xml_editor(self):
nic = "<interface type='hostdev'><mac address='52:54:00:00:00:0a'/><source><address type='pci' bus='0x00' slot='0x0a'/></source></interface>"
proxy = vm(domain(nic), domain(nic))
with self.assertRaisesRegex(OperationError, "XML editor"):
proxy.delete_network("52:54:00:00:00:0a")
with self.assertRaisesRegex(OperationError, "XML editor"):
proxy.change_network("52:54:00:00:00:0a", "52:54:00:00:00:0b", "default", "net", "virtio", "")
proxy.instance.detachDeviceFlags.assert_not_called()
def test_a_clone_gets_no_host_device_and_its_nics_keep_their_numbers(self):
proxy = vm("", domain(by_model(),
"<interface type='hostdev'><mac address='52:54:00:00:00:0a'/></interface>",
"<interface type='network'><mac address='52:54:00:00:00:0b'/><source network='default'/></interface>"))
proxy.get_instances = lambda: []
tree, _ = proxy._clone_definition({"name": "kiosk-2", "clone-net-mac-1": "52:54:00:00:00:1b"},
"99999999-2222-3333-4444-555555555555")
self.assertEqual(tree.xpath("devices/hostdev"), [])
self.assertEqual(tree.xpath("devices/interface/mac/@address"), ["52:54:00:00:00:1b"])
class RunningVM:
"""A running VM whose definitions change as libvirt changes them: the
running one and the next start's, each with the PCI device or not.
fail: the affect flag whose change libvirt fails, after doing it or not;
keeps: the guest does not let the device go."""
def __init__(self, now=True, later=True, fail=None, done=False, keeps=False, machine="pc-i440fx-10.1"):
self.has = {libvirt.VIR_DOMAIN_AFFECT_LIVE: now, libvirt.VIR_DOMAIN_AFFECT_CONFIG: later}
self.fail, self.done, self.keeps, self.machine = fail, done, keeps, machine
self.proxy = vm("", "")
self.proxy.instance.XMLDesc.side_effect = self.describe
self.proxy.instance.attachDeviceFlags.side_effect = lambda xml, flags: self.change(flags, True)
self.proxy.instance.detachDeviceFlags.side_effect = lambda xml, flags: self.change(flags, False)
def describe(self, flags=0):
saved = flags & libvirt.VIR_DOMAIN_XML_INACTIVE
has = self.has[libvirt.VIR_DOMAIN_AFFECT_CONFIG if saved else libvirt.VIR_DOMAIN_AFFECT_LIVE]
return f"<domain><os><type machine='{self.machine}'>hvm</type></os><devices>{PCI_05 if has else ''}</devices></domain>"
def change(self, flags, adding):
for affect in (libvirt.VIR_DOMAIN_AFFECT_LIVE, libvirt.VIR_DOMAIN_AFFECT_CONFIG):
if flags & affect:
if affect == self.fail:
if self.done:
self.has[affect] = adding
raise libvirt.libvirtError("refused")
if adding or not (self.keeps and affect == libvirt.VIR_DOMAIN_AFFECT_LIVE):
self.has[affect] = adding
class PciHotplugTests(SimpleTestCase):
def test_a_running_vm_gets_it_at_once_and_for_its_next_start(self):
running = RunningVM(now=False, later=False)
running.proxy.add_pci_hostdev("0000:05:00.0")
self.assertEqual(running.proxy.instance.attachDeviceFlags.call_args.args[1],
libvirt.VIR_DOMAIN_AFFECT_LIVE | libvirt.VIR_DOMAIN_AFFECT_CONFIG)
self.assertEqual(list(running.has.values()), [True, True])
def test_a_hot_plug_that_fails_says_where_the_device_is(self):
cases = ((libvirt.VIR_DOMAIN_AFFECT_CONFIG, False, "the running VM has it, its next start does not have it"),
(libvirt.VIR_DOMAIN_AFFECT_CONFIG, True, "the running VM has it, its next start has it"),
(libvirt.VIR_DOMAIN_AFFECT_LIVE, False, "the running VM does not have it, its next start does not have it"))
for fail, done, where in cases:
# libvirt plugs into the running VM first, then saves the next start
running = RunningVM(now=False, later=False, fail=fail, done=done)
if fail == libvirt.VIR_DOMAIN_AFFECT_CONFIG:
running.has[libvirt.VIR_DOMAIN_AFFECT_LIVE] = True
with self.subTest(where=where), self.assertRaisesRegex(OperationError, f"refused; PCI device 0000:05:00.0: {where}"):
running.proxy.add_pci_hostdev("0000:05:00.0")
def test_a_hot_plug_whose_outcome_cannot_be_read_says_so(self):
running = RunningVM(now=False, later=False)
running.proxy.instance.attachDeviceFlags.side_effect = libvirt.libvirtError("connection reset")
running.proxy.instance.XMLDesc.side_effect = libvirt.libvirtError("connection reset")
with self.assertRaisesRegex(OperationError, "connection reset; whether the VM has PCI device 0000:05:00.0 now is not known"):
running.proxy.add_pci_hostdev("0000:05:00.0")
def test_a_vm_that_stops_meanwhile_is_said_to_be_not_running(self):
running = RunningVM(now=False, later=False, fail=libvirt.VIR_DOMAIN_AFFECT_CONFIG)
running.proxy.instance.isActive.return_value = False # its live XML is then the inactive one
with self.assertRaisesRegex(OperationError, "the VM is not running, its next start does not have it"):
running.proxy.add_pci_hostdev("0000:05:00.0")
def test_a_running_vm_lets_it_go_as_its_guest_does(self):
running = RunningVM()
self.assertFalse(running.proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual(list(running.has.values()), [False, False])
# not yet: the request stands, the next start no longer has it
running = RunningVM(keeps=True)
self.assertTrue(running.proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual(list(running.has.values()), [True, False])
def test_a_guest_that_refuses_keeps_it_for_its_next_start_too(self):
running = RunningVM(fail=libvirt.VIR_DOMAIN_AFFECT_LIVE)
with self.assertRaisesRegex(OperationError, "refused; .*the running VM has it, its next start has it"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
self.assertEqual(list(running.has.values()), [True, True])
# removed from the running VM, then an error: said, and the next start keeps it
running = RunningVM(fail=libvirt.VIR_DOMAIN_AFFECT_LIVE, done=True)
with self.assertRaisesRegex(OperationError, "the running VM does not have it, its next start has it"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_failed_removal_from_the_next_start_of_a_running_vm_is_said(self):
running = RunningVM(now=False, later=True, fail=libvirt.VIR_DOMAIN_AFFECT_CONFIG)
with self.assertRaisesRegex(OperationError, "the running VM does not have it, its next start has it"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_removal_whose_outcome_cannot_be_read_says_so(self):
running = RunningVM()
reads = []
def describe(flags=0): # the reads before the removal work, the one after it does not
reads.append(flags)
if len(reads) > 2:
raise libvirt.libvirtError("connection reset")
return running.describe(flags)
running.proxy.instance.XMLDesc.side_effect = describe
with self.assertRaisesRegex(OperationError, "connection reset; whether the VM has PCI device 0000:05:00.0 now is not known"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_next_start_that_keeps_it_after_the_running_vm_let_it_go_is_said(self):
for keeps, where in ((False, "the running VM does not have it, its next start has it"),
(True, "the running VM has it, its next start has it")):
running = RunningVM(fail=libvirt.VIR_DOMAIN_AFFECT_CONFIG, keeps=keeps)
with self.subTest(keeps=keeps), self.assertRaisesRegex(OperationError, where):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_device_in_one_definition_only_is_taken_from_it(self):
for now, later in ((True, False), (False, True)):
running = RunningVM(now=now, later=later)
with self.subTest(now=now, later=later):
self.assertFalse(running.proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual(list(running.has.values()), [False, False])
self.assertEqual(running.proxy.instance.detachDeviceFlags.call_count, 1)
class MigrationTests(unittest.TestCase):
def test_a_vm_with_host_devices_is_not_migrated(self):
for devices, state in ((by_model(), 1), (by_model(), 5), ("<hostdev mode='subsystem' type='pci'/>", 1)):
source = FakeSource(state, xml=with_disks(devices))
with self.subTest(state=state), self.assertRaisesRegex(OperationError, "host devices cannot be migrated"):
destination().moveto(source, "vm", live=state == 1, unsafe=False, offline=state == 5)
self.assertEqual(source.instance.calls, [])
from vrtManager.tests.fakes import by_model, domain
from vrtManager.tests.fakes import IOMMU_NODES, NODES, node
class HostUsbViewTests(TestCase):
@ -352,10 +100,9 @@ class HostUsbViewTests(TestCase):
"VM kiosk-1: request the removal of PCI device 0000:00:0a.0"])
def test_a_running_vm_is_given_a_device_alone_a_shut_off_one_with_its_group(self):
from nodedevs.tests.test_pci_rules import NODES as GROUPS
# the next start has the GPU's audio function managed: it goes along when the VM starts, not at a hot plug
self.proxy.wvm.listAllDevices.return_value = [node(name, etree.tostring(n)) for name, n in GROUPS.items()]
self.proxy.wvm.listAllDevices.return_value = [node(name, etree.tostring(n)) for name, n in IOMMU_NODES.items()]
self.proxy.domain_trees.side_effect = lambda: []
self.proxy.get_hostdevs.return_value = ([], [{"address": "0000:00:0e.0", "now": False, "next": True, "managed": True}], [])
url = reverse("instances:host_devices", args=[self.instance.id])

View file

@ -15,7 +15,7 @@ class InstanceTabsTestCase(unittest.TestCase):
@unittest.skipUnless(shutil.which("node"), "node is not installed")
def test_instance_tabs_js(self):
result = subprocess.run(
["node", str(BASE / "webvirtcloud/tests/js/instance_tabs.test.js"), str(BASE / "static/js/instance_tabs.js")],
["node", str(BASE / "instances/tests/js/instance_tabs.test.js"), str(BASE / "static/js/instance_tabs.js")],
capture_output=True,
text=True,
timeout=60,

View file

@ -4,13 +4,11 @@ import hashlib
import os
import time
import unittest
from xml.etree import ElementTree
import libvirt
from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TransactionTestCase
from django.test import TransactionTestCase
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from appsettings.settings import get_settings
from computes.models import Compute
@ -20,115 +18,12 @@ from operations.models import Operation
from operations.tests import fakes
from operations.worker import Worker
from . import livetest
from .test_live_jobs import wait_for
from vrtManager.tests import livetest
from vrtManager.tests.livetest import wait_for
P = livetest.PREFIX
class LiveCloneTestCase(SimpleTestCase):
@classmethod
def setUpClass(cls):
if not livetest.enabled():
raise unittest.SkipTest("Set TEST_LIBVIRT_HOST to run the live libvirt tests")
cls.args = (
os.environ["TEST_LIBVIRT_HOST"],
os.environ.get("TEST_LIBVIRT_LOGIN", ""),
os.environ.get("TEST_LIBVIRT_PASSWORD", ""),
int(os.environ.get("TEST_LIBVIRT_TYPE", 4)),
)
cls.conn = connection_manager.get_connection(*cls.args)
livetest.cleanup(cls.conn)
livetest.ensure_pool(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.remove_pool(cls.conn)
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def tearDown(self):
livetest.cleanup(self.conn)
def pool_volumes(self):
pool = self.conn.storagePoolLookupByName(livetest.POOL)
pool.refresh(0)
return set(pool.listVolumes())
def source(self, name):
disks = [livetest.create_volume(self.conn, P + f"{name}-{d}") for d in ("a", "b")]
dom = livetest.define_vm(self.conn, P + name, disks)
return dom, wvmInstance(*self.args, None, uuid=dom.UUIDString())
def clone_data(self, name):
return {
"name": P + name,
"disk-vda": P + name + "-a.qcow2",
"disk-vdb": P + name + "-b.qcow2",
"disk_owner_uid": 0,
"disk_owner_gid": 0,
}
def test_failed_clone_removes_only_its_own_copies(self):
_, vm = self.source("clone-src")
taken = livetest.create_volume(self.conn, P + "clone-dst-b") # destination of vdb exists
before = self.pool_volumes()
with self.assertRaises(Exception):
vm.clone_instance(self.clone_data("clone-dst"), "abababab-0000-1111-2222-333333333333")
self.assertEqual(self.pool_volumes(), before, "copies left behind or a volume deleted")
self.assertTrue(livetest.volume_exists(self.conn, taken))
with self.assertRaises(libvirt.libvirtError):
self.conn.lookupByName(P + "clone-dst")
def test_running_vm_is_not_cloned(self):
dom, vm = self.source("clone-run")
dom.create()
before = self.pool_volumes()
with self.assertRaises(Exception):
vm.clone_instance(self.clone_data("clone-run-copy"), "abababab-0000-1111-2222-333333333333")
self.assertEqual(self.pool_volumes(), before)
def test_clone_of_a_uefi_vm_has_its_own_copy_of_the_nvram(self):
disk = livetest.create_volume(self.conn, P + "clone-uefi-a")
dom = livetest.define_vm(self.conn, P + "clone-uefi", [disk], uefi=True)
dom.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED) # libvirt makes the NVRAM file at the first start
dom.destroy()
vm = wvmInstance(*self.args, None, uuid=dom.UUIDString())
source = vm.get_nvram()
uuid = vm.clone_instance(
{"name": P + "clone-uefi-copy", "disk-vda": P + "clone-uefi-copy-a.qcow2", "disk_owner_uid": 0, "disk_owner_gid": 0}, "abababab-0000-1111-2222-333333333333"
)
clone = self.conn.lookupByUUIDString(uuid)
path = ElementTree.fromstring(clone.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)).findtext("os/nvram")
self.assertEqual(path, os.path.join(os.path.dirname(source), P + "clone-uefi-copy_VARS.fd"))
with vm._nvram_pool() as pool:
copy = pool.storageVolLookupByName(os.path.basename(path))
self.assertEqual(copy.info()[1], pool.storageVolLookupByName(os.path.basename(source)).info()[1])
clone.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED) # qemu opens the copy
self.assertEqual(
ElementTree.fromstring(clone.XMLDesc(0)).findtext("os/nvram"), path, "libvirt replaced the copy"
)
clone.destroy()
def test_clone_of_a_stopped_vm(self):
_, vm = self.source("clone-ok")
uuid = vm.clone_instance(self.clone_data("clone-ok-copy"), "abababab-0000-1111-2222-333333333333")
clone = self.conn.lookupByUUIDString(uuid)
self.assertEqual(clone.name(), P + "clone-ok-copy")
self.assertTrue({P + "clone-ok-copy-a.qcow2", P + "clone-ok-copy-b.qcow2"} <= self.pool_volumes())
MIB = 1024 * 1024

View file

@ -1,7 +1,7 @@
"""Live checks of VM creation: the queued job defines the VM, and a failed
one leaves no volumes behind.
Runs only with TEST_LIBVIRT_HOST set (see instances/tests/livetest.py).
Runs only with TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py).
"""
import os
@ -19,8 +19,8 @@ from operations.models import Operation
from operations.tests import fakes
from operations.worker import Worker
from . import livetest
from .test_live_jobs import wait_for
from vrtManager.tests import livetest
from vrtManager.tests.livetest import wait_for
P = livetest.PREFIX
@ -214,4 +214,3 @@ class LiveCreateTestCase(TransactionTestCase):
self.work()
self.assertEqual(self.volumes(), set(), "a volume was left behind")
self.assertFalse(self.domain_exists(P + "cr-api"))

View file

@ -3,7 +3,7 @@
Each test asserts the safe behavior. Known unfixed cases are marked
expectedFailure; a fix turns them into unexpected successes, which fail
the run until the marker is removed. Runs only with
TEST_LIBVIRT_HOST set (see instances/tests/livetest.py).
TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py).
"""
import os
@ -19,7 +19,7 @@ from django.test import TestCase
from django.urls import reverse
from vrtManager.connection import connection_manager
from . import livetest
from vrtManager.tests import livetest
from instances.jobs import DeleteInstance
from instances.models import Instance
from operations.jobs import Context
@ -671,4 +671,3 @@ class LiveDataLossTestCase(TestCase):
inbound = etree.fromstring(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)).find("devices/interface/bandwidth/inbound")
self.assertEqual((inbound.get("average"), inbound.get("peak")), ("1000", "600"))
self.assertEqual(dom.XMLDesc(0), live)

View file

@ -6,7 +6,7 @@ with qemu-guest-agent enabled.
The tests boot wvc-test VMs from qcow2 overlays in the test pool, so the
image itself is only read. They are skipped while a running VM uses the
image, since its content would change under the overlays. Runs only with TEST_LIBVIRT_HOST set as well (see
instances/tests/livetest.py).
vrtManager/tests/livetest.py).
"""
import json
@ -26,7 +26,7 @@ from vrtManager.connection import connection_manager
from operations.models import Operation
from . import livetest
from vrtManager.tests import livetest
from .test_create_instance import run
from instances.models import Instance
from instances.utils import refr

View file

@ -19,7 +19,7 @@ from django.urls import reverse
from lxml import etree
from vrtManager.connection import connection_manager
from . import livetest
from vrtManager.tests import livetest
from instances.models import Instance
from instances.utils import refr

View file

@ -1,13 +1,12 @@
"""Live checks for the instance jobs: the worker runs them against libvirt
and recovery reads the same outcome back. Runs only with TEST_LIBVIRT_HOST
set (see instances/tests/livetest.py). The migration needs a second host:
set (see vrtManager/tests/livetest.py). The migration needs a second host:
TEST_LIBVIRT_SSH_HOST (and _LOGIN), with TEST_LIBVIRT_MIGRATE_ADDRESS as its
migration address. The ISO upload runs on both hosts: TCP writes through a
libvirt stream, SSH through SFTP."""
import os
import subprocess
import time
import unittest
import libvirt
@ -27,20 +26,12 @@ from operations.models import Operation
from operations.tests import fakes
from operations.worker import Worker
from . import livetest
from vrtManager.tests import livetest
from vrtManager.tests.livetest import wait_for
P = livetest.PREFIX
def wait_for(predicate, seconds):
end = time.monotonic() + seconds
while time.monotonic() < end:
if predicate():
return True
time.sleep(0.5)
return False
class LiveJobsTestCase(TransactionTestCase):
@classmethod
def setUpClass(cls):

View file

@ -19,7 +19,7 @@ from django.urls import reverse
from lxml import etree
from vrtManager.connection import connection_manager
from . import livetest
from vrtManager.tests import livetest
from instances.models import Instance
from instances.utils import refr

View file

@ -15,7 +15,7 @@ from vrtManager.util import randomUUID
from operations.models import Operation
from . import livetest
from vrtManager.tests import livetest
from .test_create_instance import run
from instances.models import Flavor, Instance
from instances.utils import refr

View file

@ -14,35 +14,10 @@ from instances.models import Instance
from logs.models import Logs
from libvirt import libvirtError
from lxml import etree
from vrtManager.create import PCIE_ROOT_PORTS, wvmCreate
DOM_CAPS = {"domain": "kvm", "os_support": "no", "cpu_modes": ["host-model"], "cpu_custom_models": [],
"disk_bus": ["virtio", "sata", "scsi"]}
NO_SLOT = libvirtError("internal error: No more available PCI slots")
def defined_controllers(machine):
conn = wvmCreate.__new__(wvmCreate)
conn.wvm = MagicMock()
with patch.object(wvmCreate, "get_capabilities", return_value={"features": []}), \
patch.object(wvmCreate, "get_dom_capabilities", return_value=DOM_CAPS):
conn.create_instance(name="vm-1", memory=512, vcpu=1, vcpu_mode="host-model",
uuid="00000000-0000-4000-8000-0000000000c2", arch="x86_64", machine=machine,
firmware="BIOS", volumes=[], networks="default", nwfilter="", virtio=True,
listener_addr="0.0.0.0", add_cdrom="None", add_input="None")
domain = etree.fromstring(conn.wvm.defineXML.call_args.args[0])
return [c.get("model") for c in domain.findall("devices/controller[@type='pci']")]
class NewVmPortsTests(SimpleTestCase):
def test_a_new_q35_vm_has_spare_root_ports(self):
# libvirt puts the VM's devices into these and adds none: the rest are free
self.assertEqual(defined_controllers("pc-q35-rhel9.8.0"), ["pcie-root"] + ["pcie-root-port"] * PCIE_ROOT_PORTS)
def test_a_pc_vm_has_none_its_pci_bus_has_slots(self):
self.assertEqual(defined_controllers("pc-i440fx-rhel7.6.0"), [])
class NoPciSlotTests(TestCase):
def setUp(self):
self.client.force_login(get_user_model().objects.create_superuser("ps-admin", "ps@example.com", "x"))

View file

@ -17,6 +17,7 @@ from vrtManager.util import render
from libvirt import VIR_DOMAIN_PAUSED, VIR_DOMAIN_RUNNING
from operations.models import Operation
from vrtManager.util import OperationError
import socket
def vm_reader(vcpu=2, memory=1024, disk_gib=10, pending=None):
@ -274,3 +275,20 @@ class DetailPageQuotaTests(TestCase):
proxy.get_status.return_value = 5
response = self.client.get(reverse("instances:instance", args=[vm.id]))
self.assertEqual(response.status_code, 200)
class QuotaWithDownHostTestCase(TestCase):
def test_vm_on_an_unreachable_host_makes_the_quota_unverified(self):
# its usage is unknown, so an increase is refused (fail closed), without an error page
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
user = get_user_model().objects.create_user("quota_down", password="pw")
UserAttributes.objects.create(user=user, max_instances=5, max_cpus=8, max_memory=8192, max_disk_size=100)
compute = Compute.objects.create(name="down", hostname=f"127.0.0.1:{port}", login="", password="", type=1)
vm = Instance.objects.create(compute=compute, name="vm", uuid="55555555-5555-5555-5555-555555555555")
UserInstance.objects.create(user=user, instance=vm)
get_settings() # app settings are normally loaded by middleware
self.assertEqual(check_user_quota(user, 1, 1, 128, 1), QUOTA_UNVERIFIED)

View file

@ -0,0 +1,8 @@
"""Test data shared by the interface tests: a valid bridge form."""
BASE = {
"name": "br1", "itype": "bridge", "start_mode": "onboot", "netdev": "eth1",
"ipv4_type": "static", "ipv4_addr": "192.0.2.10/24", "ipv4_gw": "192.0.2.1",
"ipv6_type": "static", "stp": "on", "delay": 0,
}

View file

@ -1,24 +1,10 @@
"""Host interface form: every value is validated before it goes into the interface XML."""
from unittest.mock import MagicMock, patch
import libvirt
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase
from django.urls import reverse
from lxml import etree
from vrtManager.interface import wvmInterfaces
from django.test import SimpleTestCase
from interfaces.forms import AddInterface
BASE = {
"name": "br1", "itype": "bridge", "start_mode": "onboot", "netdev": "eth1",
"ipv4_type": "static", "ipv4_addr": "192.0.2.10/24", "ipv4_gw": "192.0.2.1",
"ipv6_type": "static", "stp": "on", "delay": 0,
}
from interfaces.tests.fakes import BASE
# the host's network devices, as interfaces/views.py passes them
NETDEVS = ["eth0", "eth1", "enp129s0f0np0", "eth0.100", "bond-lan"]
@ -71,25 +57,6 @@ class Ipv6FormTestCase(SimpleTestCase):
self.assertTrue(form(ipv6_type="dhcp", ipv6_addr="", ipv6_gw="").is_valid())
class Ipv6XmlTestCase(SimpleTestCase):
def xml(self, gw):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
conn.get_iface = MagicMock()
conn.get_iface.return_value.XMLDesc.return_value = "<interface type='ethernet' name='eth1'/>"
conn.create_iface("br1", "bridge", "onboot", "eth1", "none", "", "", "static", "2001:db8::10/64", gw, "on", 0)
return etree.fromstring(conn.define_iface.call_args[0][0])
def test_static_address_with_gateway(self):
proto = self.xml("2001:db8::1").find("protocol[@family='ipv6']")
self.assertEqual(proto.find("ip").attrib, {"address": "2001:db8::10", "prefix": "64"})
self.assertEqual(proto.find("route").get("gateway"), "2001:db8::1")
def test_no_empty_route_without_gateway(self):
self.assertIsNone(self.xml("").find("protocol[@family='ipv6']/route"))
class Ipv4FormTestCase(SimpleTestCase):
def test_dhcp_and_none_need_no_address(self):
for ipv4_type in ("dhcp", "none"):
@ -156,25 +123,6 @@ class InterfaceNameTestCase(SimpleTestCase):
self.assertIn("name", f.errors)
class Ipv4XmlTestCase(SimpleTestCase):
def xml(self, addr, gw):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
conn.get_iface = MagicMock()
conn.get_iface.return_value.XMLDesc.return_value = "<interface type='ethernet' name='eth1'/>"
conn.create_iface("br1", "bridge", "onboot", "eth1", "static", addr, gw, "none", "", "", "on", 0)
return etree.fromstring(conn.define_iface.call_args[0][0])
def test_static_address_with_gateway(self):
proto = self.xml("192.0.2.10/24", "192.0.2.1").find("protocol[@family='ipv4']")
self.assertEqual(proto.find("ip").attrib, {"address": "192.0.2.10", "prefix": "24"})
self.assertEqual(proto.find("route").get("gateway"), "192.0.2.1")
def test_no_empty_route_without_gateway(self):
self.assertIsNone(self.xml("192.0.2.10/24", "").find("protocol[@family='ipv4']/route"))
class HostDeviceTestCase(SimpleTestCase):
def test_a_bridge_member_must_be_a_host_device(self):
for netdev in ("eth9", "eth0:1"):
@ -202,89 +150,6 @@ class HostDeviceTestCase(SimpleTestCase):
self.assertIn("name", f.errors)
def no_interface(name):
err = libvirt.libvirtError("Interface not found: no interface with matching name '%s'" % name)
err.err = (libvirt.VIR_ERR_NO_INTERFACE, libvirt.VIR_FROM_INTERFACE, str(err), 2, name, None, None, 0, 0)
return err
class BridgeMemberXmlTestCase(SimpleTestCase):
"""A bridge member is declared as the host defines it: a bond keeps its
mode and devices, a vlan its tag and device."""
def member(self, host_xml):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
def get_iface(name):
if host_xml is None:
raise no_interface(name)
iface = MagicMock()
iface.XMLDesc.return_value = host_xml
return iface
conn.get_iface = MagicMock(side_effect=get_iface)
conn.create_iface("br1", "bridge", "onboot", "bond0", "none", "", "", "none", "", "", "on", 0)
self.assertTrue(conn.define_iface.return_value.create.called)
members = etree.fromstring(conn.define_iface.call_args[0][0]).findall("bridge/interface")
self.assertEqual(len(members), 1)
return members[0]
def test_a_bond_keeps_its_mode_and_devices(self):
member = self.member(
"<interface type='bond' name='bond0'><start mode='onboot'/><mtu size='9000'/>"
"<protocol family='ipv4'><dhcp/></protocol>"
"<bond mode='active-backup'><miimon freq='100'/>"
"<interface type='ethernet' name='eth0'><mac address='52:54:00:00:00:01'/></interface>"
"<interface type='ethernet' name='eth1'/></bond></interface>"
)
self.assertEqual((member.get("type"), member.get("name")), ("bond", "bond0"))
self.assertEqual(member.find("bond").get("mode"), "active-backup")
self.assertEqual([i.get("name") for i in member.findall("bond/interface")], ["eth0", "eth1"])
self.assertEqual(member.find("bond/miimon").get("freq"), "100")
# what only a top-level interface has: the bridge holds the addresses
for tag in ("start", "mtu", "protocol"):
self.assertIsNone(member.find(tag), tag)
def test_a_vlan_keeps_its_tag_and_device(self):
member = self.member(
"<interface type='vlan' name='bond0'><start mode='onboot'/>"
"<vlan tag='42'><interface name='eth0'/></vlan></interface>"
)
self.assertEqual(member.get("type"), "vlan")
self.assertEqual(member.find("vlan").get("tag"), "42")
self.assertEqual(member.find("vlan/interface").get("name"), "eth0")
self.assertIsNone(member.find("start"))
def test_a_device_the_host_does_not_define_is_ethernet(self):
member = self.member(None)
self.assertEqual(member.attrib, {"type": "ethernet", "name": "bond0"})
def test_other_lookup_errors_are_not_hidden(self):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
err = libvirt.libvirtError("cannot recv data")
err.err = (libvirt.VIR_ERR_SYSTEM_ERROR, libvirt.VIR_FROM_RPC, str(err), 2, None, None, None, 0, 0)
conn.get_iface = MagicMock(side_effect=err)
with self.assertRaises(libvirt.libvirtError):
conn.create_iface("br1", "bridge", "onboot", "bond0", "none", "", "", "none", "", "", "on", 0)
self.assertFalse(conn.define_iface.called)
def test_an_interface_that_does_not_start_is_rolled_back(self):
# the definition, and the member's own configuration it changed, as they were
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
iface = conn.define_iface.return_value
iface.create.side_effect = libvirt.libvirtError("failed to bring up br1")
with self.assertRaises(libvirt.libvirtError):
conn.create_iface("br1", "ethernet", "onboot", "", "none", "", "", "none", "", "", "on", 0)
conn.wvm.changeRollback.assert_called_once_with(0)
conn.wvm.changeCommit.assert_not_called()
class BridgeSettingsTestCase(SimpleTestCase):
def test_a_bridge_needs_stp_and_a_delay(self):
for field in ("stp", "delay"):
@ -313,152 +178,6 @@ class AddressNotationTestCase(SimpleTestCase):
self.assertIn(field, f.errors)
class CreateViewTestCase(TestCase):
"""The page validates the device against the host's devices before it
defines an interface."""
def test_only_a_host_device_becomes_a_bridge_member(self):
admin = get_user_model().objects.create_superuser("iface_admin", "a@example.com", "x")
compute = Compute.objects.create(name="iface-c", hostname="127.0.0.1:1", login="root", password="", type=1)
self.client.force_login(admin)
for netdev, created in (("eth9", False), ("eth0", True)):
with self.subTest(netdev=netdev), patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces_info.return_value = []
conn.can_change_interfaces.return_value = True
conn.get_net_devices.return_value = ["eth0", "eth1"]
conn.get_member_candidates.return_value = ["eth0", "eth1"]
self.client.post(
reverse("interfaces", args=[compute.id]),
{**BASE, "netdev": netdev, "ipv6_type": "none", "create": ""},
)
self.assertEqual(conn.create_iface.called, created)
def test_a_device_taken_meanwhile_is_chosen_again(self):
# not swapped for the first device the dialog still offers
admin = get_user_model().objects.create_superuser("iface_admin3", "c@example.com", "x")
compute = Compute.objects.create(name="iface-c3", hostname="127.0.0.1:1", login="root", password="", type=1)
self.client.force_login(admin)
for select, values in (("iface_netdev", {"netdev": "eth1"}), ("iface_device", {"itype": "ethernet", "name": "eth1", "netdev": "eth0"})):
with self.subTest(**values), patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces_info.return_value = []
conn.can_change_interfaces.return_value = True
conn.get_net_devices.return_value = ["eth0", "eth1"]
conn.get_member_candidates.return_value = ["eth0"]
page = self.client.post(reverse("interfaces", args=[compute.id]),
{**BASE, **values, "ipv6_type": "none", "create": ""}).content.decode()
self.assertFalse(conn.create_iface.called)
chosen = etree.HTML(page).xpath(f"//select[@id='{select}']/option[@selected]/@value")
self.assertEqual(chosen, [""])
def test_unreadable_host_devices_are_reported_not_blamed_on_the_input(self):
admin = get_user_model().objects.create_superuser("iface_admin2", "b@example.com", "x")
compute = Compute.objects.create(name="iface-c2", hostname="127.0.0.1:1", login="root", password="", type=1)
self.client.force_login(admin)
with patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces_info.return_value = []
conn.can_change_interfaces.return_value = True
conn.get_net_devices.side_effect = RuntimeError("nodedev driver not running")
response = self.client.post(
reverse("interfaces", args=[compute.id]),
{**BASE, "netdev": "eth0", "ipv6_type": "none", "create": ""},
)
self.assertFalse(conn.create_iface.called)
messages = [str(m) for m in response.context["messages"]]
self.assertTrue(any("nodedev driver not running" in m for m in messages), messages)
self.assertFalse(any("not on this host" in m for m in messages), messages)
class InterfaceListTestCase(TestCase):
def test_the_interfaces_are_grouped_by_type(self):
self.client.force_login(get_user_model().objects.create_superuser("iface_list", "l@example.com", "x"))
compute = Compute.objects.create(name="iface-list", hostname="127.0.0.1:1", login="root", password="", type=1)
ifaces = [{"name": name, "type": kind, "mac": None, "link": "up", "members": []}
for name, kind in (("br0", "bridge"), ("eth0", "ethernet"), ("eth1", "ethernet"))]
with patch("interfaces.views.wvmInterfaces") as conn_cls:
conn_cls.return_value.get_ifaces_info.return_value = ifaces
conn_cls.return_value.can_change_interfaces.return_value = False
page = etree.HTML(self.client.get(reverse("interfaces", args=[compute.id])).content.decode())
# no heading for a type the host has none of
self.assertEqual([" ".join(h.itertext()).split() for h in page.xpath("//h2")], [["Bridges", "1"], ["Ethernet", "2"]])
class UnchangeableHostTestCase(TestCase):
"""A host whose libvirt cannot change interfaces (udev backend) gets no
create form and no start/stop/delete, only a note; posts change nothing."""
NOTE = "only reads network interfaces"
def setUp(self):
self.client.force_login(get_user_model().objects.create_superuser("iface_ro", "ro@example.com", "x"))
self.compute = Compute.objects.create(name="iface-ro", hostname="127.0.0.1:1", login="root", password="", type=1)
def interfaces_page(self, changeable, post=None):
with patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces_info.return_value = []
conn.can_change_interfaces.return_value = changeable
conn.get_net_devices.return_value = ["eth0", "eth1"]
url = reverse("interfaces", args=[self.compute.id])
response = self.client.post(url, post) if post else self.client.get(url)
return conn, response.content.decode()
def test_the_interfaces_page_offers_create_only_where_it_works(self):
for changeable in (True, False):
with self.subTest(changeable=changeable):
conn, page = self.interfaces_page(changeable)
self.assertEqual("#AddInterface" in page, changeable)
self.assertEqual(self.NOTE in page, not changeable)
# the devices are only the form's choices
self.assertEqual(conn.get_net_devices.called, changeable)
def test_a_create_post_creates_nothing(self):
conn, page = self.interfaces_page(False, {**BASE, "netdev": "eth1", "ipv6_type": "none", "create": ""})
conn.create_iface.assert_not_called()
self.assertIn(self.NOTE, page)
def interface_page(self, changeable, state, post=None):
with patch("interfaces.views.wvmInterface") as conn_cls:
conn = conn_cls.return_value
conn.can_change_interfaces.return_value = changeable
conn.is_active.return_value = state
url = reverse("interface", args=[self.compute.id, "br0"])
response = self.client.post(url, post) if post else self.client.get(url)
return conn, response
def test_the_interface_page_shows_the_state_without_actions(self):
# libvirt's own state is told where it manages the interface only; the link always
for state in (1, 0):
with self.subTest(state=state):
conn, response = self.interface_page(False, state)
page = response.content.decode()
self.assertNotIn('name="start"', page)
self.assertNotIn('name="stop"', page)
self.assertNotIn(">Active<", page)
self.assertIn(self.NOTE, page)
_conn, response = self.interface_page(True, 0)
page = response.content.decode()
self.assertIn('name="start"', page)
self.assertIn(">Inactive<", page)
def test_a_host_that_could_not_be_read_offers_nothing(self):
# not knowing whether the host can change interfaces is not a yes
with patch("interfaces.views.wvmInterface", side_effect=libvirt.libvirtError("unreachable")):
page = self.client.get(reverse("interface", args=[self.compute.id, "br0"])).content.decode()
self.assertNotIn(self.NOTE, page)
self.assertNotIn("Interface start/stop/delete form", page)
def test_start_stop_and_delete_posts_change_nothing(self):
for action in ("start", "stop", "delete"):
with self.subTest(action=action):
conn, response = self.interface_page(False, 1, {action: ""})
self.assertEqual(response.status_code, 200)
for method in (conn.start_iface, conn.stop_iface, conn.delete_iface):
method.assert_not_called()
class EthernetDeviceTestCase(SimpleTestCase):
def test_an_ethernet_interface_needs_no_bridge_member(self):
self.assertTrue(form(itype="ethernet", name="eth0", netdev="", ipv6_type="none").is_valid())

View file

@ -10,9 +10,10 @@ from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from vrtManager.connection import wvmConnect
from vrtManager.tests import livetest
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_HOST"), "Set TEST_LIBVIRT_HOST to run the live libvirt tests")
@unittest.skipUnless(livetest.enabled(), "Set TEST_LIBVIRT_HOST to run the live libvirt tests")
class LiveInterfacesTestCase(TestCase):
def hosts(self):
yield "tcp", (

View file

@ -0,0 +1,159 @@
"""The host interface pages: creating an interface, the list, and a host
whose libvirt cannot change interfaces."""
from unittest.mock import patch
import libvirt
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from lxml import etree
from computes.models import Compute
from interfaces.tests.fakes import BASE
class CreateViewTestCase(TestCase):
"""The page validates the device against the host's devices before it
defines an interface."""
def test_only_a_host_device_becomes_a_bridge_member(self):
admin = get_user_model().objects.create_superuser("iface_admin", "a@example.com", "x")
compute = Compute.objects.create(name="iface-c", hostname="127.0.0.1:1", login="root", password="", type=1)
self.client.force_login(admin)
for netdev, created in (("eth9", False), ("eth0", True)):
with self.subTest(netdev=netdev), patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces_info.return_value = []
conn.can_change_interfaces.return_value = True
conn.get_net_devices.return_value = ["eth0", "eth1"]
conn.get_member_candidates.return_value = ["eth0", "eth1"]
self.client.post(
reverse("interfaces", args=[compute.id]),
{**BASE, "netdev": netdev, "ipv6_type": "none", "create": ""},
)
self.assertEqual(conn.create_iface.called, created)
def test_a_device_taken_meanwhile_is_chosen_again(self):
# not swapped for the first device the dialog still offers
admin = get_user_model().objects.create_superuser("iface_admin3", "c@example.com", "x")
compute = Compute.objects.create(name="iface-c3", hostname="127.0.0.1:1", login="root", password="", type=1)
self.client.force_login(admin)
for select, values in (("iface_netdev", {"netdev": "eth1"}), ("iface_device", {"itype": "ethernet", "name": "eth1", "netdev": "eth0"})):
with self.subTest(**values), patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces_info.return_value = []
conn.can_change_interfaces.return_value = True
conn.get_net_devices.return_value = ["eth0", "eth1"]
conn.get_member_candidates.return_value = ["eth0"]
page = self.client.post(reverse("interfaces", args=[compute.id]),
{**BASE, **values, "ipv6_type": "none", "create": ""}).content.decode()
self.assertFalse(conn.create_iface.called)
chosen = etree.HTML(page).xpath(f"//select[@id='{select}']/option[@selected]/@value")
self.assertEqual(chosen, [""])
def test_unreadable_host_devices_are_reported_not_blamed_on_the_input(self):
admin = get_user_model().objects.create_superuser("iface_admin2", "b@example.com", "x")
compute = Compute.objects.create(name="iface-c2", hostname="127.0.0.1:1", login="root", password="", type=1)
self.client.force_login(admin)
with patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces_info.return_value = []
conn.can_change_interfaces.return_value = True
conn.get_net_devices.side_effect = RuntimeError("nodedev driver not running")
response = self.client.post(
reverse("interfaces", args=[compute.id]),
{**BASE, "netdev": "eth0", "ipv6_type": "none", "create": ""},
)
self.assertFalse(conn.create_iface.called)
messages = [str(m) for m in response.context["messages"]]
self.assertTrue(any("nodedev driver not running" in m for m in messages), messages)
self.assertFalse(any("not on this host" in m for m in messages), messages)
class InterfaceListTestCase(TestCase):
def test_the_interfaces_are_grouped_by_type(self):
self.client.force_login(get_user_model().objects.create_superuser("iface_list", "l@example.com", "x"))
compute = Compute.objects.create(name="iface-list", hostname="127.0.0.1:1", login="root", password="", type=1)
ifaces = [{"name": name, "type": kind, "mac": None, "link": "up", "members": []}
for name, kind in (("br0", "bridge"), ("eth0", "ethernet"), ("eth1", "ethernet"))]
with patch("interfaces.views.wvmInterfaces") as conn_cls:
conn_cls.return_value.get_ifaces_info.return_value = ifaces
conn_cls.return_value.can_change_interfaces.return_value = False
page = etree.HTML(self.client.get(reverse("interfaces", args=[compute.id])).content.decode())
# no heading for a type the host has none of
self.assertEqual([" ".join(h.itertext()).split() for h in page.xpath("//h2")], [["Bridges", "1"], ["Ethernet", "2"]])
class UnchangeableHostTestCase(TestCase):
"""A host whose libvirt cannot change interfaces (udev backend) gets no
create form and no start/stop/delete, only a note; posts change nothing."""
NOTE = "only reads network interfaces"
def setUp(self):
self.client.force_login(get_user_model().objects.create_superuser("iface_ro", "ro@example.com", "x"))
self.compute = Compute.objects.create(name="iface-ro", hostname="127.0.0.1:1", login="root", password="", type=1)
def interfaces_page(self, changeable, post=None):
with patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces_info.return_value = []
conn.can_change_interfaces.return_value = changeable
conn.get_net_devices.return_value = ["eth0", "eth1"]
url = reverse("interfaces", args=[self.compute.id])
response = self.client.post(url, post) if post else self.client.get(url)
return conn, response.content.decode()
def test_the_interfaces_page_offers_create_only_where_it_works(self):
for changeable in (True, False):
with self.subTest(changeable=changeable):
conn, page = self.interfaces_page(changeable)
self.assertEqual("#AddInterface" in page, changeable)
self.assertEqual(self.NOTE in page, not changeable)
# the devices are only the form's choices
self.assertEqual(conn.get_net_devices.called, changeable)
def test_a_create_post_creates_nothing(self):
conn, page = self.interfaces_page(False, {**BASE, "netdev": "eth1", "ipv6_type": "none", "create": ""})
conn.create_iface.assert_not_called()
self.assertIn(self.NOTE, page)
def interface_page(self, changeable, state, post=None):
with patch("interfaces.views.wvmInterface") as conn_cls:
conn = conn_cls.return_value
conn.can_change_interfaces.return_value = changeable
conn.is_active.return_value = state
url = reverse("interface", args=[self.compute.id, "br0"])
response = self.client.post(url, post) if post else self.client.get(url)
return conn, response
def test_the_interface_page_shows_the_state_without_actions(self):
# libvirt's own state is told where it manages the interface only; the link always
for state in (1, 0):
with self.subTest(state=state):
conn, response = self.interface_page(False, state)
page = response.content.decode()
self.assertNotIn('name="start"', page)
self.assertNotIn('name="stop"', page)
self.assertNotIn(">Active<", page)
self.assertIn(self.NOTE, page)
_conn, response = self.interface_page(True, 0)
page = response.content.decode()
self.assertIn('name="start"', page)
self.assertIn(">Inactive<", page)
def test_a_host_that_could_not_be_read_offers_nothing(self):
# not knowing whether the host can change interfaces is not a yes
with patch("interfaces.views.wvmInterface", side_effect=libvirt.libvirtError("unreachable")):
page = self.client.get(reverse("interface", args=[self.compute.id, "br0"])).content.decode()
self.assertNotIn(self.NOTE, page)
self.assertNotIn("Interface start/stop/delete form", page)
def test_start_stop_and_delete_posts_change_nothing(self):
for action in ("start", "stop", "delete"):
with self.subTest(action=action):
conn, response = self.interface_page(False, 1, {action: ""})
self.assertEqual(response.status_code, 200)
for method in (conn.start_iface, conn.stop_iface, conn.delete_iface):
method.assert_not_called()

View file

@ -8,7 +8,7 @@ from computes.models import Compute
from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.test import TestCase
from networks.tests.driver import without_inactive_flag
from vrtManager.tests.driver import without_inactive_flag
from django.urls import reverse
from logs.models import Logs

View file

@ -12,7 +12,7 @@ from computes.models import Compute
from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.test import TestCase
from networks.tests.driver import without_inactive_flag
from vrtManager.tests.driver import without_inactive_flag
from django.urls import reverse
from lxml import etree

View file

@ -12,7 +12,7 @@ from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from lxml import html
from networks.tests.driver import without_inactive_flag
from vrtManager.tests.driver import without_inactive_flag
NODE = """<node><network><name>lab</name><bridge name='virbr9'/>
<ip address='192.0.2.1' netmask='255.255.255.0'><dhcp><range start='192.0.2.10' end='192.0.2.20'/></dhcp></ip>

View file

@ -11,7 +11,7 @@ from computes.models import Compute
from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.test import TestCase
from networks.tests.driver import without_inactive_flag
from vrtManager.tests.driver import without_inactive_flag
from django.urls import reverse
NODE = """<node>

View file

@ -5,56 +5,24 @@ that cannot be read leave the rest of the page. Runs on a private libvirt
test driver, which has no leases: they are given."""
import tempfile
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import libvirt
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase
from django.test import TestCase
from django.urls import reverse
from instances.models import Instance
from lxml import html
from networks.tests.driver import without_inactive_flag
from networks.tests.test_network_pages import NODE
from vrtManager.network import wvmNetwork
DUID = "00:01:00:01:2e:8b:3c:41:52:54:00:00:00:40"
def lease(ip, mac=None, expiry=1793265489, hostname=None, clientid=None, family=0):
return {"iface": "virbr9", "expirytime": expiry, "type": family, "mac": mac, "ipaddr": ip, "prefix": 24,
"hostname": hostname, "clientid": clientid, "iaid": None}
class LeasesTests(SimpleTestCase):
def network(self, leases, active=True):
net = wvmNetwork.__new__(wvmNetwork)
net.net = MagicMock(**{"isActive.return_value": active, "DHCPLeases.return_value": leases})
return net
def test_leases_by_address_ipv4_first(self):
net = self.network([lease("2001:db8:1::40", family=1, clientid=DUID, expiry=0),
lease("192.0.2.100", "52:54:00:AA:00:01", hostname="db"),
lease("192.0.2.9", "52:54:00:aa:00:02")])
ipv4_9, ipv4_100, ipv6 = net.get_leases()
self.assertEqual([str(ipv4_9["ip"]), str(ipv4_100["ip"]), str(ipv6["ip"])], ["192.0.2.9", "192.0.2.100", "2001:db8:1::40"])
self.assertEqual((ipv4_100["mac"], ipv4_100["hostname"], ipv4_100["expires"]),
("52:54:00:aa:00:01", "db", datetime.fromtimestamp(1793265489, timezone.utc)))
# an IPv6 lease: no MAC, its client's DUID; one that never ends
self.assertEqual((ipv6["mac"], ipv6["client_id"], ipv6["expires"]), ("", DUID, None))
def test_a_network_that_does_not_run_has_none(self):
net = self.network([lease("192.0.2.9")], active=False)
self.assertEqual(net.get_leases(), [])
net.net.DHCPLeases.assert_not_called()
from vrtManager.tests.driver import without_inactive_flag
from vrtManager.tests.fakes import DUID, NAT_NETWORK_NODE, lease
class LeasesCardTests(TestCase):
def setUp(self):
node = tempfile.NamedTemporaryFile("w", suffix=".xml")
self.addCleanup(node.close)
node.write(NODE)
node.write(NAT_NETWORK_NODE)
node.flush()
self.conn = libvirt.open(f"test://{node.name}")
self.addCleanup(self.conn.close)

View file

@ -14,26 +14,15 @@ from django.test import TestCase
from django.urls import reverse
from instances.models import Instance
from lxml import html
from networks.tests.driver import without_inactive_flag
NODE = """<node>
<network><name>lab</name><forward mode='nat'/><bridge name='virbr9'/>
<ip address='192.0.2.1' netmask='255.255.255.0'><dhcp><range start='192.0.2.10' end='192.0.2.20'/>
<host mac='52:54:00:00:00:31' name='web' ip='192.0.2.31'/></dhcp></ip>
<ip family='ipv6' address='2001:db8:1::1' prefix='64'><dhcp><host id='0:1:0:1' ip='2001:db8:1::31'/></dhcp></ip>
</network>
<network><name>lan</name><forward mode='bridge'/><bridge name='br0'/></network>
<network><name>direct</name><forward mode='bridge'><interface dev='eth0'/></forward></network>
<domain type='test'><name>web</name><memory>1024</memory><os><type>hvm</type></os>
<devices><interface type='network'><source network='lab'/></interface></devices></domain>
</node>"""
from vrtManager.tests.driver import without_inactive_flag
from vrtManager.tests.fakes import NAT_NETWORK_NODE
class NetworkPageTests(TestCase):
def setUp(self):
node = tempfile.NamedTemporaryFile("w", suffix=".xml")
self.addCleanup(node.close)
node.write(NODE)
node.write(NAT_NETWORK_NODE)
node.flush()
self.conn = libvirt.open(f"test://{node.name}")
self.addCleanup(self.conn.close)

View file

@ -9,7 +9,7 @@ from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.test import TestCase
from django.urls import reverse
from networks.tests.driver import without_inactive_flag
from vrtManager.tests.driver import without_inactive_flag
class NetworkQosTests(TestCase):

View file

@ -4,169 +4,17 @@ and the VMs that name it, by PCI address, by USB address or by USB model
only. Node device XML as libvirt 11.10 writes it; the connection is a mock,
libvirt's test driver has no node devices."""
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import libvirt
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase
from django.test import TestCase
from django.urls import reverse
from instances.models import Instance
from lxml import html
from vrtManager.connection import wvmConnect
from vrtManager import util
from vrtManager.nodedevs import hostdev_problems, node_devices, pci_address
def pci(slot, product, code, driver=None, group=None):
iommu = f"<iommuGroup number='{group}'><address domain='0x0000' bus='0x00' slot='{slot}' function='0x0'/></iommuGroup>" \
if group is not None else ""
driver = f"<driver><name>{driver}</name></driver>" if driver else ""
return (f"pci_0000_00_{int(slot, 16):02x}_0", f"""<device><name>pci_0000_00_{int(slot, 16):02x}_0</name><parent>computer</parent>{driver}
<capability type='pci'><class>{code}</class><domain>0</domain><bus>0</bus><slot>{int(slot, 16)}</slot><function>0</function>
<product id='0x1000'>{product}</product><vendor id='0x1af4'>Red Hat, Inc.</vendor>{iommu}</capability></device>""")
def child(name, parent, capability):
return name, f"<device><name>{name}</name><parent>{parent}</parent>{capability}</device>"
def usb(name, parent, bus, device, product="QEMU Tablet", vendor_id="0x0627", product_id="0x0001"):
return name, f"""<device><name>{name}</name><parent>{parent}</parent><capability type='usb_device'><bus>{bus}</bus>
<device>{device}</device><product id='{product_id}'>{product}</product><vendor id='{vendor_id}'>Adomax</vendor>
</capability></device>"""
def interface(name, parent, code):
return child(name, parent, f"<capability type='usb'><number>0</number><class>{code}</class></capability>")
NODES = [
pci("0x06", "Virtio network device", "0x020000", "virtio-pci", group=3),
child("net_eth0", "pci_0000_00_06_0", "<capability type='net'><interface>eth0</interface></capability>"),
pci("0x03", "Virtio block device", "0x010000", "virtio-pci", group=2),
child("virtio1", "pci_0000_00_03_0", "<capability type='system'/>"), # a level between, not a resource
child("block_vda", "virtio1", "<capability type='storage'><block>/dev/vda</block></capability>"),
pci("0x02", "Virtio 1.0 GPU", "0x030000", "virtio-pci", group=2),
child("drm_card0", "pci_0000_00_02_0", "<devnode type='dev'>/dev/dri/card0</devnode><capability type='drm'><type>primary</type></capability>"),
child("drm_render", "pci_0000_00_02_0", "<devnode type='dev'>/dev/dri/renderD128</devnode><capability type='drm'><type>render</type></capability>"),
pci("0x0a", "Ethernet controller", "0x020000", "vfio-pci", group=4),
pci("0x01", "PIIX3 USB", "0x0c0300", "uhci_hcd"),
usb("usb_usb1", "pci_0000_00_01_0", 1, 1, "1.1 root hub", "0x1d6b", "0x0001"),
interface("usb_1_0_1_0", "usb_usb1", 9),
usb("usb_1_1", "usb_usb1", 1, 2),
interface("usb_1_1_1_0", "usb_1_1", 3),
usb("usb_1_2", "usb_usb1", 1, 3),
interface("usb_1_2_1_0", "usb_1_2", 3),
]
def node(name, xml):
return MagicMock(**{"name.return_value": name, "XMLDesc.return_value": xml})
def vm(name, hostdevs, saved=None):
"""A VM with hostdevs as it runs and, given saved, persistent with those for its next start."""
def describe(flags=0):
return f"<domain><devices>{saved if flags else hostdevs}</devices></domain>"
return MagicMock(**{"name.return_value": name, "XMLDesc.side_effect": describe, "isPersistent.return_value": saved is not None})
PCI_01 = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x01' function='0x0'/></source></hostdev>"
PCI_0A = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x0a' function='0x0'/></source></hostdev>"
USB_1_3 = "<hostdev mode='subsystem' type='usb'><source><vendor id='0x0627'/><product id='0x0001'/><address bus='1' device='3'/></source></hostdev>"
USB_MODEL = "<hostdev mode='subsystem' type='usb'><source><vendor id='0x0627'/><product id='0x0001'/></source></hostdev>"
def devices(nodes=NODES, domains=()):
conn = wvmConnect.__new__(wvmConnect)
conn.wvm = MagicMock(**{"listAllDevices.return_value": [node(*n) for n in nodes],
"listAllDomains.return_value": list(domains)})
return node_devices(conn)
class NodeDevicesTests(SimpleTestCase):
def test_pci_devices_by_group_with_what_the_host_has_on_them(self):
pci_devices, _ = devices()
self.assertEqual([(d["address"], d["group"]) for d in pci_devices],
[("0000:00:02.0", 2), ("0000:00:03.0", 2), ("0000:00:06.0", 3), ("0000:00:0a.0", 4), ("0000:00:01.0", None)])
gpu, disk, nic, spare, _ = pci_devices
# a graphics card's primary node is the host's; its render node is not a resource of its own
self.assertEqual((gpu["resources"], disk["resources"], nic["resources"], spare["resources"]),
(["/dev/dri/card0"], ["/dev/vda"], ["eth0"], []))
self.assertEqual((nic["kind"], nic["driver"], nic["ids"]), ("Network controller", "virtio-pci", "1af4:1000"))
def test_usb_devices_with_their_classes_hubs_and_same_models(self):
_, (hub, tablet_2, tablet_3) = devices()
self.assertEqual((hub["kinds"], hub["same_model"]), (["Hub"], False))
self.assertEqual((tablet_2["kinds"], tablet_2["same_model"], tablet_3["same_model"]), (["Human interface"], True, True))
def test_the_vms_that_name_a_device(self):
pci_devices, (_, tablet_2, tablet_3) = devices(domains=[vm("db-1", PCI_0A + USB_1_3), vm("kiosk-1", USB_MODEL)])
self.assertEqual([d["vms"] for d in pci_devices], [[], [], [], ["db-1"], []])
# by address: that device; by model only: any device of the model
self.assertEqual((tablet_3["vms"], tablet_3["by_model"]), (["db-1"], ["kiosk-1"]))
self.assertEqual((tablet_2["vms"], tablet_2["by_model"]), ([], ["kiosk-1"]))
def test_a_usb_controller_holds_the_usb_devices_below_it(self):
pci_devices, (hub, tablet_2, _) = devices(domains=[vm("kiosk-1", USB_1_3)])
controller = [d for d in pci_devices if d["address"] == "0000:00:01.0"][0]
# its root hub is no device of the host's; the tablets are, one given to a VM
self.assertEqual(controller["resources"], ["USB device 1/2 (QEMU Tablet)", "USB device 1/3 (QEMU Tablet)"])
self.assertEqual((hub["controller"], tablet_2["controller"]), ("0000:00:01.0", "0000:00:01.0"))
def test_a_usb_device_on_a_controller_given_to_a_vm_is_not_given_to_another(self):
pci_devices, usb_devices = devices(domains=[vm("db-1", PCI_01)])
reasons = [util.render(m["text"], m["values"], str) for m in hostdev_problems(
"kiosk-1", pci_devices, usb_devices, set(), [("0x0627", "0x0001", (1, 2))])]
self.assertIn("USB device 1/2 (QEMU Tablet) is on the PCI controller 0000:00:01.0, given to db-1", reasons)
def test_a_usb_device_is_named_as_libvirt_picks_one(self):
def named(hostdev):
_, (_, tablet_2, tablet_3) = devices(domains=[vm("v", hostdev)])
return [(d["vms"], d["by_model"]) for d in (tablet_2, tablet_3)]
# an address with other IDs: another device now at that address
other = USB_1_3.replace("0x0627", "0x046d")
address_only = "<hostdev mode='subsystem' type='usb'><source><address bus='1' device='2'/></source></hostdev>"
self.assertEqual(named(other), [([], []), ([], [])])
self.assertEqual(named(address_only), [(["v"], []), ([], [])])
self.assertEqual(named(USB_MODEL), [([], ["v"]), ([], ["v"])])
def test_a_pci_device_named_as_a_nic_or_only_for_the_next_start(self):
nic = ("<interface type='hostdev'><source><address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>"
"</source></interface>")
pci_devices, _ = devices(domains=[vm("nic-1", nic), vm("next-1", "", saved=PCI_0A)])
self.assertEqual({d["address"]: d["vms"] for d in pci_devices if d["vms"]},
{"0000:00:06.0": ["nic-1"], "0000:00:0a.0": ["next-1"]})
def test_sr_iov_functions_and_unnamed_hardware(self):
vf = pci("0x0b", "", "0x020000", group=5)[1].replace(
"<product id='0x1000'></product>", "<product id='0x10ed'/>").replace(
"</capability></device>", "<capability type='phys_function'><address domain='0x0000' bus='0x00' slot='0x0a'"
" function='0x0'/></capability></capability></device>")
pf = NODES[8][1].replace("</capability></device>", "<capability type='virt_functions' maxCount='7'><address domain='0x0000'"
" bus='0x00' slot='0x0b' function='0x0'/></capability></capability></device>")
pci_devices, _ = devices(nodes=[("pci_0000_00_0a_0", pf), ("pci_0000_00_0b_0", vf)])
self.assertEqual([(d["physfn"], d["vfs"], d["product"]) for d in pci_devices],
[("", 1, "Ethernet controller"), ("0000:00:0a.0", 0, "0x10ed")])
def test_a_device_gone_meanwhile_is_left_out_other_errors_are_raised(self):
for code, raised in ((libvirt.VIR_ERR_NO_NODE_DEVICE, False), (libvirt.VIR_ERR_OPERATION_FAILED, True)):
err = libvirt.libvirtError("gone")
err.get_error_code = lambda code=code: code
gone = node("pci_0000_00_0b_0", "")
gone.XMLDesc.side_effect = err
conn = wvmConnect.__new__(wvmConnect)
conn.wvm = MagicMock(**{"listAllDevices.return_value": [gone, node(*NODES[0])], "listAllDomains.return_value": []})
with self.subTest(code=code):
if raised:
self.assertRaises(libvirt.libvirtError, node_devices, conn)
else:
self.assertEqual([d["address"] for d in node_devices(conn)[0]], ["0000:00:06.0"])
def test_a_pci_address_from_libvirts_hex_or_numbers(self):
self.assertEqual(pci_address("0x0000", "0x00", "0x1f", "0x3"), pci_address(0, 0, 31, 3))
self.assertEqual(pci_address(0, 0, 31, 3), "0000:00:1f.3")
from vrtManager.tests.fakes import NODES, node
from vrtManager.tests.fakes import PCI_0A, USB_1_3, vm
class NodeDevicesPageTests(TestCase):

View file

@ -11,7 +11,7 @@ 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.tests import livetest
from vrtManager.tests import livetest
from vrtManager.connection import connection_manager
P = livetest.PREFIX

View file

@ -5,79 +5,12 @@ value refused leaves no secret behind."""
from unittest.mock import patch
import libvirt
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase
from django.urls import reverse
from lxml import etree, html
from lxml import html
from virtsecrets.forms import AddSecret
from vrtManager.util import OperationError
from vrtManager.virtsecrets import wvmSecrets
def connection():
with patch("vrtManager.connection.connection_manager.get_connection") as get:
conn = wvmSecrets("host", "login", "pw", 1)
return conn, get.return_value
class CreateSecretTests(SimpleTestCase):
def test_the_usage_is_text_of_its_element(self):
for usage_type, element, usage in (
("volume", "volume", "/var/lib/libvirt/images/vm-1.qcow2"),
("iscsi", "target", "iqn.2026-10.test.wvc:lun"),
("ceph", "name", "client.libvirt secret"),
("tls", "name", "migration"),
("vtpm", "name", "VM web-1's vTPM"),
("volume", "volume", "/a'b<c>&d\""),
):
with self.subTest(usage_type=usage_type, usage=usage):
conn, wvm = connection()
conn.create_secret(usage_type, usage, private=True)
secret = etree.fromstring(wvm.secretDefineXML.call_args.args[0])
self.assertEqual((secret.get("ephemeral"), secret.get("private")), ("no", "yes"))
self.assertEqual(secret.find("usage").get("type"), usage_type)
self.assertEqual(secret.findtext(f"usage/{element}"), usage)
wvm.secretDefineXML.return_value.setValue.assert_not_called()
def test_a_new_secret_takes_its_value_at_once(self):
for usage_type, value, stored in (("ceph", "cHcA/w==", b"pw\x00\xff"), ("iscsi", " pass word ", b" pass word ")):
with self.subTest(usage_type=usage_type):
conn, wvm = connection()
conn.create_secret(usage_type, "lun", value)
wvm.secretDefineXML.return_value.setValue.assert_called_once_with(stored)
def test_a_value_refused_leaves_no_secret(self):
conn, wvm = connection()
with self.assertRaisesRegex(OperationError, "base64"):
conn.create_secret("ceph", "client.libvirt", "not base64!")
wvm.secretDefineXML.assert_not_called()
wvm.secretDefineXML.return_value.setValue.side_effect = libvirt.libvirtError("refused")
with self.assertRaises(libvirt.libvirtError):
conn.create_secret("iscsi", "lun", "pw")
wvm.secretDefineXML.return_value.undefine.assert_called_once_with()
class SecretValueTests(SimpleTestCase):
def set_value(self, usage_type, value):
conn, wvm = connection()
secret = wvm.secretLookupByUUIDString.return_value
secret.XMLDesc.return_value = f"<secret><usage type='{usage_type}'><name>x</name></usage></secret>"
conn.set_secret_value("uuid", value)
return secret.setValue
def test_a_ceph_key_is_stored_as_its_bytes_a_password_as_typed(self):
self.set_value("ceph", "cHcA/w==").assert_called_once_with(b"pw\x00\xff")
# a password that happens to be base64 is not decoded
self.set_value("iscsi", "cHcA/w==").assert_called_once_with(b"cHcA/w==")
self.set_value("vtpm", " pässword ").assert_called_once_with(" pässword ".encode())
def test_a_ceph_key_that_is_not_base64_is_refused_without_the_value(self):
for value in ("b'cHc='", "not base64!", "kéy"):
with self.subTest(value=value), self.assertRaises(OperationError) as refused:
self.set_value("ceph", value)
self.assertNotIn(value, str(refused.exception))
class CreateSecretFormTests(SimpleTestCase):

View file

@ -10,7 +10,7 @@ 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.tests import livetest
from vrtManager.tests import livetest
from vrtManager.connection import connection_manager
P = livetest.PREFIX

View file

@ -4,125 +4,16 @@ 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
from unittest.mock import 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.test import 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"<secret><usage type='{usage_type}'><name>{usage}</name></usage></secret>"})
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"<domain><devices>{devices}</devices></domain>", **kwargs)
def pool(name, auth, **kwargs):
return owner(name, f"<pool type='rbd'><source>{auth}</source></pool>", **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"<disk type='network'><auth username='libvirt'><secret type='ceph' uuid='{CEPH}'/></auth></disk>"),
vm("lun-1", "<hostdev mode='subsystem' type='scsi'><source protocol='iscsi'><auth username='u'>"
"<secret type='iscsi' usage='client.libvirt'/></auth></source></hostdev>"),
vm("tpm-1", f"<tpm model='tpm-crb'><backend type='emulator'><encryption secret='{VTPM}'/></backend></tpm>"),
])
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", "<auth type='ceph' username='libvirt'><secret usage='client.libvirt'/></auth>"),
# chap is iSCSI; named only for its next start
pool("lun", "", saved="<pool><source><auth type='chap' username='u'><secret usage='client.libvirt'/>"
"</auth></source></pool>", 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 = "<disk><auth username='libvirt'><secret type='ceph' usage='client.libvirt'/></auth></disk>"
# saved only: read whether it runs or not, as it may stop between the reads
for kwargs in ({"active": True, "saved": f"<domain><devices>{disk}</devices></domain>"},
{"active": False, "saved": f"<domain><devices>{disk}</devices></domain>"},
{"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"<disk><auth><secret type='ceph' uuid='{CEPH}'/></auth></disk>")])
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"<disk><auth><secret type='ceph' uuid='{CEPH}'/></auth></disk>")],
pools=[pool("ceph-rbd", "<auth type='ceph'><secret usage='client.libvirt'/></auth>")])
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"<disk><auth><secret type='ceph' uuid='{CEPH}'/></auth></disk>")])
conn.delete_secret(ISCSI)
conn.wvm.secretLookupByUUIDString.assert_called_once_with(ISCSI)
conn.wvm.secretLookupByUUIDString.return_value.undefine.assert_called_once_with()
from vrtManager.tests.fakes import CEPH
class SecretInUsePageTests(TestCase):

View file

@ -1,4 +1,4 @@
"""libvirt's test driver for the network tests."""
"""libvirt's test driver as the network tests need it."""
from unittest.mock import patch

318
vrtManager/tests/fakes.py Normal file
View file

@ -0,0 +1,318 @@
"""Fake libvirt hosts, domains and node devices that tests of several
modules share: the migration source and destination, the host devices of
a q35 host, and the IOMMU groups of the PCI passthrough rules."""
from unittest.mock import MagicMock
import libvirt
from lxml import etree
from vrtManager.instance import wvmInstances
def no_domain():
error = libvirt.libvirtError("Domain not found")
error.err = (libvirt.VIR_ERR_NO_DOMAIN,)
return error
class FakeDomain:
def __init__(self, xml="<domain/>", snapshots=0, saved=False, undefine_fails=False, undefine_lost=False,
started=False):
self.calls = []
self.xml = xml
self.snapshots = snapshots
self.saved = saved
self.undefine_fails = undefine_fails
self.undefine_lost = undefine_lost # undefined, but the reply is lost
self.started = started # started by another client during the move
self.undefined = None
self.redefined = []
self.restored_autostart = None
def migrate(self, dconn, flags, dname, uri, bandwidth):
self.calls.append((flags, uri))
def name(self):
return "vm"
def UUIDString(self):
return "u-1"
inactive_xml = None # the persistent definition, when it differs
def XMLDesc(self, flags):
if flags & libvirt.VIR_DOMAIN_XML_INACTIVE and self.inactive_xml:
return self.inactive_xml
return self.xml
def snapshotNum(self, flags=0):
return self.snapshots
def hasManagedSaveImage(self, flags=0):
return self.saved
def undefineFlags(self, flags):
if self.undefine_fails:
raise libvirt.libvirtError("undefine failed")
self.undefined = flags
if self.undefine_lost:
raise libvirt.libvirtError("Cannot recv data")
def isPersistent(self):
return self.undefined is None or bool(self.redefined)
def isActive(self):
if not self.started and self.undefined is not None:
raise no_domain() # libvirt: an undefined shut-off VM is gone
return self.started
def autostart(self):
return 1
def setAutostart(self, value):
self.restored_autostart = value
def connect(self):
return self
def defineXML(self, xml):
self.redefined.append(xml)
return self
def no_volume():
error = libvirt.libvirtError("Storage volume not found")
error.err = (libvirt.VIR_ERR_NO_STORAGE_VOL,)
return error
class FakeVolume:
def __init__(self, backing):
self.backing = backing
def XMLDesc(self, flags=0):
if self.backing is None:
return "<volume><target><path>x</path></target></volume>"
return f"<volume><backingStore><path>{self.backing}</path></backingStore></volume>"
class FakePool:
def __init__(self, host, active=True):
self.host = host
self.active = active
def isActive(self):
return self.active
def XMLDesc(self, flags=0):
return "<pool><target><path>/elsewhere</path></target></pool>"
def refresh(self, flags=0):
self.host.refreshes += 1
self.host.volumes |= self.host.unlisted
self.host.unlisted = set()
def storageVolLookupByName(self, name):
return self.host.storageVolLookupByPath(name)
class FakeHost:
"""The destination's libvirt connection. volumes: the paths its pools
list; unlisted: files its pools show after a refresh."""
def __init__(self, existing=(), volumes=(), unlisted=(), backing=None, inactive_pools=()):
self.existing = set(existing)
self.defined = []
self.new = []
self.volumes = set(volumes)
self.unlisted = set(unlisted)
self.backing = backing or {} # path: the path of its backing file
self.inactive_pools = set(inactive_pools)
self.refreshes = 0
def storageVolLookupByPath(self, path):
if path not in self.volumes:
raise no_volume()
return FakeVolume(self.backing.get(path))
def storagePoolLookupByName(self, name):
return FakePool(self, active=name not in self.inactive_pools)
def listAllStoragePools(self, flags=0):
return [FakePool(self)]
def lookup(self, key):
if key in self.existing:
return FakeDomain()
raise no_domain()
lookupByName = lookupByUUIDString = lookup
def defineXML(self, xml):
self.defined.append(xml)
self.new.append(FakeDomain())
return self.new[-1]
class FakeSource:
def __init__(self, state, nvram=None, **domain):
self.state = state
self.nvram = nvram
self.instance = FakeDomain(**domain)
def get_nvram(self):
return self.nvram
reads = 0
def get_status(self):
self.reads += 1
return self.state
def get_arch(self):
return "x86_64"
def get_dom_emulator(self):
return "/usr/bin/qemu-system-x86_64"
def destination(host=None):
dest = wvmInstances.__new__(wvmInstances)
dest.wvm = host or FakeHost()
dest.get_emulator = lambda arch: "/usr/bin/qemu-system-x86_64"
return dest
def with_disks(*disks):
return "<domain><devices>%s</devices></domain>" % "".join(disks)
def pci(slot, product, code, driver=None, group=None):
iommu = f"<iommuGroup number='{group}'><address domain='0x0000' bus='0x00' slot='{slot}' function='0x0'/></iommuGroup>" \
if group is not None else ""
driver = f"<driver><name>{driver}</name></driver>" if driver else ""
return (f"pci_0000_00_{int(slot, 16):02x}_0", f"""<device><name>pci_0000_00_{int(slot, 16):02x}_0</name><parent>computer</parent>{driver}
<capability type='pci'><class>{code}</class><domain>0</domain><bus>0</bus><slot>{int(slot, 16)}</slot><function>0</function>
<product id='0x1000'>{product}</product><vendor id='0x1af4'>Red Hat, Inc.</vendor>{iommu}</capability></device>""")
def child(name, parent, capability):
return name, f"<device><name>{name}</name><parent>{parent}</parent>{capability}</device>"
def usb(name, parent, bus, device, product="QEMU Tablet", vendor_id="0x0627", product_id="0x0001"):
return name, f"""<device><name>{name}</name><parent>{parent}</parent><capability type='usb_device'><bus>{bus}</bus>
<device>{device}</device><product id='{product_id}'>{product}</product><vendor id='{vendor_id}'>Adomax</vendor>
</capability></device>"""
def interface(name, parent, code):
return child(name, parent, f"<capability type='usb'><number>0</number><class>{code}</class></capability>")
NODES = [
pci("0x06", "Virtio network device", "0x020000", "virtio-pci", group=3),
child("net_eth0", "pci_0000_00_06_0", "<capability type='net'><interface>eth0</interface></capability>"),
pci("0x03", "Virtio block device", "0x010000", "virtio-pci", group=2),
child("virtio1", "pci_0000_00_03_0", "<capability type='system'/>"), # a level between, not a resource
child("block_vda", "virtio1", "<capability type='storage'><block>/dev/vda</block></capability>"),
pci("0x02", "Virtio 1.0 GPU", "0x030000", "virtio-pci", group=2),
child("drm_card0", "pci_0000_00_02_0", "<devnode type='dev'>/dev/dri/card0</devnode><capability type='drm'><type>primary</type></capability>"),
child("drm_render", "pci_0000_00_02_0", "<devnode type='dev'>/dev/dri/renderD128</devnode><capability type='drm'><type>render</type></capability>"),
pci("0x0a", "Ethernet controller", "0x020000", "vfio-pci", group=4),
pci("0x01", "PIIX3 USB", "0x0c0300", "uhci_hcd"),
usb("usb_usb1", "pci_0000_00_01_0", 1, 1, "1.1 root hub", "0x1d6b", "0x0001"),
interface("usb_1_0_1_0", "usb_usb1", 9),
usb("usb_1_1", "usb_usb1", 1, 2),
interface("usb_1_1_1_0", "usb_1_1", 3),
usb("usb_1_2", "usb_usb1", 1, 3),
interface("usb_1_2_1_0", "usb_1_2", 3),
]
def node(name, xml):
return MagicMock(**{"name.return_value": name, "XMLDesc.return_value": xml})
BY_MODEL = "<hostdev mode='subsystem' type='usb' managed='yes'><source{auto}><vendor id='0x0627'/><product id='0x0001'/>{address}</source></hostdev>"
def by_model(address="", auto=""):
return BY_MODEL.format(address=address, auto=auto)
def domain(*devices):
return f"<domain><name>kiosk-1</name><devices>{''.join(devices)}</devices></domain>"
def device(slot, product, code, driver, group, members=None, bridge=False):
"""A PCI node of an IOMMU test host: a device on bus 0 at slot, in IOMMU group with the slots members (its own by default)."""
members = members or [slot]
addresses = "".join(f"<address domain='0x0000' bus='0x00' slot='0x{m:02x}' function='0x0'/>" for m in members)
driver_xml = f"<driver><name>{driver}</name></driver>" if driver else ""
bridge_xml = "<capability type='pci-bridge'/>" if bridge else ""
xml = (f"<device><name>pci_0000_00_{slot:02x}_0</name><parent>computer</parent>{driver_xml}"
f"<capability type='pci'><class>{code}</class><domain>0</domain><bus>0</bus><slot>{slot}</slot><function>0</function>"
f"<product id='0x0001'>{product}</product><vendor id='0x8086'>Intel</vendor>{bridge_xml}"
f"<iommuGroup number='{group}'>{addresses}</iommuGroup></capability></device>")
return f"pci_0000_00_{slot:02x}_0", etree.fromstring(xml)
IOMMU_NODES = dict([
device(0x02, "PCIe Root port", "0x060400", "pcieport", 2, bridge=True),
device(0x03, "SATA controller", "0x010601", "ahci", 7, [0x03, 0x04, 0x05]),
device(0x04, "SMBus controller", "0x0c0500", "i801_smbus", 7, [0x03, 0x04, 0x05]),
device(0x05, "LPC controller", "0x060100", "lpc_ich", 7, [0x03, 0x04, 0x05]), # class 0x06, not a bridge
device(0x06, "Gigabit NIC", "0x020000", "e1000e", 12),
device(0x07, "Spare NIC", "0x020000", "vfio-pci", 13),
device(0x08, "NIC port 0", "0x020000", "vfio-pci", 14, [0x08, 0x09, 0x0a]),
device(0x09, "NIC port 1", "0x020000", None, 14, [0x08, 0x09, 0x0a]),
device(0x0a, "Downstream port", "0x060400", "pcieport", 14, [0x08, 0x09, 0x0a], bridge=True),
device(0x0b, "Accelerator", "0x120000", "vfio-pci", 15, [0x0b, 0x0c]), # 0x0c cannot be read
device(0x0d, "GPU", "0x030000", "nouveau", 16, [0x0d, 0x0e]),
device(0x0e, "GPU audio", "0x040300", "snd_hda_intel", 16, [0x0d, 0x0e]),
])
# what the host has below: a disk on the SATA controller, its interface on the e1000e
IOMMU_NODES["block_sda"] = etree.fromstring("<device><name>block_sda</name><parent>pci_0000_00_03_0</parent>"
"<capability type='storage'><block>/dev/sda</block></capability></device>")
IOMMU_NODES["net_eth1"] = etree.fromstring("<device><name>net_eth1</name><parent>pci_0000_00_06_0</parent>"
"<capability type='net'><interface>eth1</interface></capability></device>")
def vm(name, hostdevs, saved=None):
"""A VM with hostdevs as it runs and, given saved, persistent with those for its next start."""
def describe(flags=0):
return f"<domain><devices>{saved if flags else hostdevs}</devices></domain>"
return MagicMock(**{"name.return_value": name, "XMLDesc.side_effect": describe, "isPersistent.return_value": saved is not None})
PCI_0A = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x0a' function='0x0'/></source></hostdev>"
USB_1_3 = "<hostdev mode='subsystem' type='usb'><source><vendor id='0x0627'/><product id='0x0001'/><address bus='1' device='3'/></source></hostdev>"
DUID = "00:01:00:01:2e:8b:3c:41:52:54:00:00:00:40"
def lease(ip, mac=None, expiry=1793265489, hostname=None, clientid=None, family=0):
return {"iface": "virbr9", "expirytime": expiry, "type": family, "mac": mac, "ipaddr": ip, "prefix": 24,
"hostname": hostname, "clientid": clientid, "iaid": None}
# a host with a NAT network that hands out addresses (libvirt's test driver node file)
NAT_NETWORK_NODE = """<node>
<network><name>lab</name><forward mode='nat'/><bridge name='virbr9'/>
<ip address='192.0.2.1' netmask='255.255.255.0'><dhcp><range start='192.0.2.10' end='192.0.2.20'/>
<host mac='52:54:00:00:00:31' name='web' ip='192.0.2.31'/></dhcp></ip>
<ip family='ipv6' address='2001:db8:1::1' prefix='64'><dhcp><host id='0:1:0:1' ip='2001:db8:1::31'/></dhcp></ip>
</network>
<network><name>lan</name><forward mode='bridge'/><bridge name='br0'/></network>
<network><name>direct</name><forward mode='bridge'><interface dev='eth0'/></forward></network>
<domain type='test'><name>web</name><memory>1024</memory><os><type>hvm</type></os>
<devices><interface type='network'><source network='lab'/></interface></devices></domain>
</node>"""
CEPH = "8f2c0000-0000-4000-8000-000000000001"

View file

@ -10,6 +10,7 @@ changed.
import os
import libvirt
import time
PREFIX = "wvc-test-"
POOL = "wvc-test"
@ -125,3 +126,12 @@ def volume_exists(conn, path):
pool = conn.storagePoolLookupByName(POOL)
pool.refresh(0)
return os.path.basename(path) in pool.listVolumes()
def wait_for(predicate, seconds):
end = time.monotonic() + seconds
while time.monotonic() < end:
if predicate():
return True
time.sleep(0.5)
return False

View file

@ -3,6 +3,7 @@ from unittest.mock import MagicMock
from lxml import etree
from vrtManager.instance import PERSISTENT_XML, wvmInstance
from django.test import TestCase
LIVE = "<domain><devices><graphics type='vnc' passwd='live' keymap='en-us'><listen type='address'/></graphics></devices></domain>"
PERSISTENT = "<domain><devices><graphics type='vnc' passwd='old'><listen type='address'/></graphics></devices></domain>"
@ -39,3 +40,29 @@ class ConsoleSettersTestCase(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class ConsoleListensLocallyTests(TestCase):
"""Only a binding the persistent XML pins to loopback (or a Unix socket) is local."""
def listens_locally(self, graphics):
proxy = wvmInstance.__new__(wvmInstance) # no connection: only the XML is read
xml = f"<domain><devices>{graphics}</devices></domain>"
proxy._XMLDesc = lambda flags: xml if flags == PERSISTENT_XML else "<wrong-flags/>"
return proxy.console_listens_locally()
def test_cases(self):
cases = {
"<graphics type='vnc' listen='127.0.0.1'><listen type='address' address='127.0.0.1'/></graphics>": True,
"<graphics type='vnc'><listen type='address' address='::1'/></graphics>": True,
"<graphics type='vnc'><listen type='socket' socket='/run/vnc.sock'/></graphics>": True,
"": True, # no console
"<graphics type='vnc' listen='0.0.0.0'><listen type='address' address='0.0.0.0'/></graphics>": False,
"<graphics type='vnc'><listen type='network' network='default'/></graphics>": False,
"<graphics type='vnc' autoport='yes'/>": False, # the host default decides
"<graphics type='vnc'><listen type='address' address='localhost'/></graphics>": False,
"<graphics type='vnc' listen='127.0.0.1'><listen type='address' address='10.0.0.5'/></graphics>": False,
}
for graphics, expected in cases.items():
with self.subTest(graphics=graphics):
self.assertIs(self.listens_locally(graphics), expected)

View file

@ -9,6 +9,7 @@ from django.test import SimpleTestCase
from lxml import etree
from vrtManager.connection import wvmConnect
from vrtManager.create import wvmCreate
from vrtManager.create import PCIE_ROOT_PORTS
FULL = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
@ -124,3 +125,59 @@ class DefinitionTests(SimpleTestCase):
tpm = defined("UEFI Secure Boot", tpm_model="tpm-crb").find("devices/tpm")
self.assertEqual((tpm.get("model"), tpm.find("backend").get("type"), tpm.find("backend").get("version")),
("tpm-crb", "emulator", "2.0"))
CPU_DOM_CAPS = {"domain": "kvm", "os_support": "no", "cpu_modes": ["host-passthrough", "maximum", "host-model", "custom"],
"cpu_custom_models": ["Skylake-Server-noTSX-IBRS", "SandyBridge-IBRS"]}
def defined_cpu(vcpu_mode):
conn = wvmCreate.__new__(wvmCreate)
conn.wvm = MagicMock()
with patch.object(wvmCreate, "get_capabilities", return_value={"features": []}), \
patch.object(wvmCreate, "get_dom_capabilities", return_value=CPU_DOM_CAPS):
conn.create_instance(name="vm-1", memory=512, vcpu=1, vcpu_mode=vcpu_mode,
uuid="00000000-0000-4000-8000-0000000000c1", arch="x86_64", machine="q35",
firmware="BIOS", volumes=[], networks="", nwfilter="", virtio=True, listener_addr="0.0.0.0",
add_cdrom="None", add_input="None")
return etree.fromstring(conn.wvm.defineXML.call_args.args[0]).find("cpu")
class CpuModeTests(SimpleTestCase):
def test_a_mode_is_written_as_the_mode(self):
for mode in ("host-model", "host-passthrough", "maximum"):
with self.subTest(mode=mode):
cpu = defined_cpu(mode)
self.assertEqual((dict(cpu.attrib), len(cpu)), ({"mode": mode}, 0))
def test_a_named_model_is_that_model_checked_by_libvirt(self):
# a name longer than 20 characters too
cpu = defined_cpu("Skylake-Server-noTSX-IBRS")
self.assertEqual(dict(cpu.attrib), {"mode": "custom", "match": "exact"}) # no check='none'
self.assertEqual((cpu.findtext("model"), cpu.find("model").get("fallback")), ("Skylake-Server-noTSX-IBRS", "forbid"))
PORTS_DOM_CAPS = {"domain": "kvm", "os_support": "no", "cpu_modes": ["host-model"], "cpu_custom_models": [],
"disk_bus": ["virtio", "sata", "scsi"]}
def defined_controllers(machine):
conn = wvmCreate.__new__(wvmCreate)
conn.wvm = MagicMock()
with patch.object(wvmCreate, "get_capabilities", return_value={"features": []}), \
patch.object(wvmCreate, "get_dom_capabilities", return_value=PORTS_DOM_CAPS):
conn.create_instance(name="vm-1", memory=512, vcpu=1, vcpu_mode="host-model",
uuid="00000000-0000-4000-8000-0000000000c2", arch="x86_64", machine=machine,
firmware="BIOS", volumes=[], networks="default", nwfilter="", virtio=True,
listener_addr="0.0.0.0", add_cdrom="None", add_input="None")
domain = etree.fromstring(conn.wvm.defineXML.call_args.args[0])
return [c.get("model") for c in domain.findall("devices/controller[@type='pci']")]
class NewVmPortsTests(SimpleTestCase):
def test_a_new_q35_vm_has_spare_root_ports(self):
# libvirt puts the VM's devices into these and adds none: the rest are free
self.assertEqual(defined_controllers("pc-q35-rhel9.8.0"), ["pcie-root"] + ["pcie-root-port"] * PCIE_ROOT_PORTS)
def test_a_pc_vm_has_none_its_pci_bus_has_slots(self):
self.assertEqual(defined_controllers("pc-i440fx-rhel7.6.0"), [])

View file

@ -1,6 +1,6 @@
"""External snapshots go through libvirt's snapshot API; WebVirtCloud only
checks preconditions and picks the flags. libvirt is mocked here; the live
cases are in instances/tests/test_live_snapshots.py."""
cases are in vrtManager/tests/test_live_snapshots.py."""
import json
import unittest
@ -714,6 +714,7 @@ class PoolRefreshTestCase(unittest.TestCase):
p.get_volume_by_path = MagicMock()
self.assertEqual(getattr(p, action)("one"), [])
class KindTestCase(unittest.TestCase):
def test_internal_endpoints_refuse_external_names(self):
snap = fake_snap("e")

View file

@ -0,0 +1,261 @@
"""A VM's host devices in vrtManager: USB devices given and taken back,
each named once whether the running VM or its next start has it, PCI
devices plugged into a running VM, and a VM with any is not migrated."""
import unittest
from unittest.mock import MagicMock
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.instance import wvmInstance
from vrtManager.tests.fakes import FakeSource, by_model, destination, domain, with_disks
from vrtManager.util import OperationError
MIGRATABLE = libvirt.VIR_DOMAIN_XML_MIGRATABLE
CONTROLLER = "<controller type='usb' model='qemu-xhci'/>"
PCI_05 = ("<hostdev mode='subsystem' type='pci' managed='yes'><source>"
"<address domain='0x0000' bus='0x05' slot='0x00' function='0x0'/></source></hostdev>")
def vm(running, saved, active=True, migratable=None):
"""A wvmInstance whose running VM is running, as XMLDesc gives it (and as
the migratable XML marks it), and whose next start is saved."""
proxy = wvmInstance.__new__(wvmInstance)
proxy._read_cache = None
proxy.instance = MagicMock(**{"isActive.return_value": active, "hasManagedSaveImage.return_value": 0,
"info.return_value": [libvirt.VIR_DOMAIN_RUNNING if active else libvirt.VIR_DOMAIN_SHUTOFF]})
def describe(flags=0):
if flags & libvirt.VIR_DOMAIN_XML_INACTIVE:
return saved
return migratable if flags & MIGRATABLE and migratable else running
proxy.instance.XMLDesc.side_effect = describe
return proxy
class UsbHostdevTests(SimpleTestCase):
def test_a_device_chosen_by_model_is_one_row_with_the_device_it_holds(self):
# the running VM writes the address libvirt resolved; only its migratable XML marks it
proxy = vm(domain(CONTROLLER, by_model("<address bus='1' device='3'/>")), domain(CONTROLLER, by_model()),
migratable=domain(CONTROLLER, by_model("<address bus='1' device='3'/>", " autoAddress='yes'")))
usb, pci, others = proxy.get_hostdevs()
self.assertEqual(usb, [{"vendor": "0x0627", "product": "0x0001", "address": None, "now": (1, 3), "next": True}])
self.assertEqual((pci, others), ([], []))
def test_a_device_is_taken_from_each_definition_as_it_describes_it(self):
proxy = vm(domain(CONTROLLER, by_model("<address bus='1' device='3'/>")), domain(CONTROLLER, by_model()),
migratable=domain(CONTROLLER, by_model("<address bus='1' device='3'/>", " autoAddress='yes'")))
proxy.delete_usb_hostdev(("0x0627", "0x0001", None))
live, config = proxy.instance.detachDeviceFlags.call_args_list
self.assertEqual(live.args[1], libvirt.VIR_DOMAIN_AFFECT_LIVE)
self.assertEqual(etree.fromstring(live.args[0]).xpath("string(source/address/@device)"), "3")
self.assertEqual((config.args[1], etree.fromstring(config.args[0]).find("source/address")),
(libvirt.VIR_DOMAIN_AFFECT_CONFIG, None))
with self.assertRaisesRegex(OperationError, "no such USB device"):
proxy.delete_usb_hostdev(("0x0627", "0x0001", (1, 2)))
def test_a_device_is_given_by_model_or_by_address_with_a_usb_controller(self):
proxy = vm(domain(CONTROLLER), domain(CONTROLLER))
proxy.add_usb_hostdev("0x0627", "0x0001", (1, 3))
added = etree.fromstring(proxy.instance.attachDeviceFlags.call_args.args[0])
self.assertEqual((added.get("type"), added.xpath("string(source/vendor/@id)"), added.xpath("string(source/address/@device)")),
("usb", "0x0627", "3"))
for xml in (domain(), domain("<controller type='usb' model='none'/>")):
proxy = vm(xml, xml, active=False)
with self.subTest(xml=xml), self.assertRaisesRegex(OperationError, "no USB controller"):
proxy.add_usb_hostdev("0x0627", "0x0001")
proxy.instance.attachDeviceFlags.assert_not_called()
def test_a_device_the_host_has_one_of_is_given_by_model(self):
proxy = vm(domain(CONTROLLER), domain(CONTROLLER))
proxy.add_usb_hostdev("0x0627", "0x0001")
self.assertIsNone(etree.fromstring(proxy.instance.attachDeviceFlags.call_args.args[0]).find("source/address"))
def test_other_host_devices_as_the_running_vm_and_its_next_start_have_them(self):
pci = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x0a' function='0x0'/></source></hostdev>"
nic = "<interface type='hostdev'><source><address type='pci' domain='0x0000' bus='0x00' slot='0x0b' function='0x0'/></source></interface>"
network_nic = ("<interface type='network'><source network='sriov'/><actual type='hostdev'><source>"
"<address type='pci' domain='0x0000' bus='0x00' slot='0x0c' function='0x1'/></source></actual></interface>")
mdev = "<hostdev mode='subsystem' type='mdev'><source><address uuid='{}'/></source></hostdev>"
running = domain(pci, nic, network_nic, mdev.format("00000000-0000-4000-8000-00000000a001"))
_, pci_rows, others = vm(running, domain(nic, mdev.format("00000000-0000-4000-8000-00000000a002"))).get_hostdevs()
self.assertEqual(pci_rows, [{"address": "0000:00:0a.0", "now": True, "next": False, "managed": False}])
self.assertEqual(others, [{"type": "nic", "address": "0000:00:0b.0", "now": True, "next": True},
{"type": "nic", "address": "0000:00:0c.1", "now": True, "next": False},
{"type": "mdev", "address": "00000000-0000-4000-8000-00000000a001", "now": True, "next": False},
{"type": "mdev", "address": "00000000-0000-4000-8000-00000000a002", "now": False, "next": True}])
def test_a_shut_off_vm_gets_a_pci_device_for_its_next_start(self):
proxy = vm("", domain(), active=False)
proxy.add_pci_hostdev("0000:05:00.0")
xml, flags = proxy.instance.attachDeviceFlags.call_args.args
self.assertEqual(flags, libvirt.VIR_DOMAIN_AFFECT_CONFIG)
self.assertEqual((etree.fromstring(xml).get("managed"), dict(etree.fromstring(xml).find("source/address").attrib)),
("yes", {"domain": "0x0000", "bus": "0x05", "slot": "0x00", "function": "0x0"}))
proxy = vm("", domain(PCI_05), active=False)
self.assertFalse(proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual([c.args[1] for c in proxy.instance.detachDeviceFlags.call_args_list], [libvirt.VIR_DOMAIN_AFFECT_CONFIG])
with self.assertRaisesRegex(OperationError, "no PCI device 0000:06:00.0"):
proxy.delete_pci_hostdev("0000:06:00.0")
def test_a_paused_vm_or_one_with_a_saved_state_keeps_its_pci_devices(self):
paused = vm(domain(PCI_05), domain(PCI_05))
paused.instance.info.return_value = [libvirt.VIR_DOMAIN_PAUSED]
saved = vm("", domain(PCI_05), active=False)
saved.instance.hasManagedSaveImage.return_value = 1
for name, proxy in (("paused", paused), ("saved", saved)):
for change in (proxy.add_pci_hostdev, proxy.delete_pci_hostdev):
with self.subTest(vm=name, change=change.__name__), self.assertRaisesRegex(OperationError, "runs, or is shut off"):
change("0000:05:00.0")
proxy.instance.attachDeviceFlags.assert_not_called()
proxy.instance.detachDeviceFlags.assert_not_called()
def test_a_nic_that_is_a_host_device_is_left_to_the_xml_editor(self):
nic = "<interface type='hostdev'><mac address='52:54:00:00:00:0a'/><source><address type='pci' bus='0x00' slot='0x0a'/></source></interface>"
proxy = vm(domain(nic), domain(nic))
with self.assertRaisesRegex(OperationError, "XML editor"):
proxy.delete_network("52:54:00:00:00:0a")
with self.assertRaisesRegex(OperationError, "XML editor"):
proxy.change_network("52:54:00:00:00:0a", "52:54:00:00:00:0b", "default", "net", "virtio", "")
proxy.instance.detachDeviceFlags.assert_not_called()
def test_a_clone_gets_no_host_device_and_its_nics_keep_their_numbers(self):
proxy = vm("", domain(by_model(),
"<interface type='hostdev'><mac address='52:54:00:00:00:0a'/></interface>",
"<interface type='network'><mac address='52:54:00:00:00:0b'/><source network='default'/></interface>"))
proxy.get_instances = lambda: []
tree, _ = proxy._clone_definition({"name": "kiosk-2", "clone-net-mac-1": "52:54:00:00:00:1b"},
"99999999-2222-3333-4444-555555555555")
self.assertEqual(tree.xpath("devices/hostdev"), [])
self.assertEqual(tree.xpath("devices/interface/mac/@address"), ["52:54:00:00:00:1b"])
class RunningVM:
"""A running VM whose definitions change as libvirt changes them: the
running one and the next start's, each with the PCI device or not.
fail: the affect flag whose change libvirt fails, after doing it or not;
keeps: the guest does not let the device go."""
def __init__(self, now=True, later=True, fail=None, done=False, keeps=False, machine="pc-i440fx-10.1"):
self.has = {libvirt.VIR_DOMAIN_AFFECT_LIVE: now, libvirt.VIR_DOMAIN_AFFECT_CONFIG: later}
self.fail, self.done, self.keeps, self.machine = fail, done, keeps, machine
self.proxy = vm("", "")
self.proxy.instance.XMLDesc.side_effect = self.describe
self.proxy.instance.attachDeviceFlags.side_effect = lambda xml, flags: self.change(flags, True)
self.proxy.instance.detachDeviceFlags.side_effect = lambda xml, flags: self.change(flags, False)
def describe(self, flags=0):
saved = flags & libvirt.VIR_DOMAIN_XML_INACTIVE
has = self.has[libvirt.VIR_DOMAIN_AFFECT_CONFIG if saved else libvirt.VIR_DOMAIN_AFFECT_LIVE]
return f"<domain><os><type machine='{self.machine}'>hvm</type></os><devices>{PCI_05 if has else ''}</devices></domain>"
def change(self, flags, adding):
for affect in (libvirt.VIR_DOMAIN_AFFECT_LIVE, libvirt.VIR_DOMAIN_AFFECT_CONFIG):
if flags & affect:
if affect == self.fail:
if self.done:
self.has[affect] = adding
raise libvirt.libvirtError("refused")
if adding or not (self.keeps and affect == libvirt.VIR_DOMAIN_AFFECT_LIVE):
self.has[affect] = adding
class PciHotplugTests(SimpleTestCase):
def test_a_running_vm_gets_it_at_once_and_for_its_next_start(self):
running = RunningVM(now=False, later=False)
running.proxy.add_pci_hostdev("0000:05:00.0")
self.assertEqual(running.proxy.instance.attachDeviceFlags.call_args.args[1],
libvirt.VIR_DOMAIN_AFFECT_LIVE | libvirt.VIR_DOMAIN_AFFECT_CONFIG)
self.assertEqual(list(running.has.values()), [True, True])
def test_a_hot_plug_that_fails_says_where_the_device_is(self):
cases = ((libvirt.VIR_DOMAIN_AFFECT_CONFIG, False, "the running VM has it, its next start does not have it"),
(libvirt.VIR_DOMAIN_AFFECT_CONFIG, True, "the running VM has it, its next start has it"),
(libvirt.VIR_DOMAIN_AFFECT_LIVE, False, "the running VM does not have it, its next start does not have it"))
for fail, done, where in cases:
# libvirt plugs into the running VM first, then saves the next start
running = RunningVM(now=False, later=False, fail=fail, done=done)
if fail == libvirt.VIR_DOMAIN_AFFECT_CONFIG:
running.has[libvirt.VIR_DOMAIN_AFFECT_LIVE] = True
with self.subTest(where=where), self.assertRaisesRegex(OperationError, f"refused; PCI device 0000:05:00.0: {where}"):
running.proxy.add_pci_hostdev("0000:05:00.0")
def test_a_hot_plug_whose_outcome_cannot_be_read_says_so(self):
running = RunningVM(now=False, later=False)
running.proxy.instance.attachDeviceFlags.side_effect = libvirt.libvirtError("connection reset")
running.proxy.instance.XMLDesc.side_effect = libvirt.libvirtError("connection reset")
with self.assertRaisesRegex(OperationError, "connection reset; whether the VM has PCI device 0000:05:00.0 now is not known"):
running.proxy.add_pci_hostdev("0000:05:00.0")
def test_a_vm_that_stops_meanwhile_is_said_to_be_not_running(self):
running = RunningVM(now=False, later=False, fail=libvirt.VIR_DOMAIN_AFFECT_CONFIG)
running.proxy.instance.isActive.return_value = False # its live XML is then the inactive one
with self.assertRaisesRegex(OperationError, "the VM is not running, its next start does not have it"):
running.proxy.add_pci_hostdev("0000:05:00.0")
def test_a_running_vm_lets_it_go_as_its_guest_does(self):
running = RunningVM()
self.assertFalse(running.proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual(list(running.has.values()), [False, False])
# not yet: the request stands, the next start no longer has it
running = RunningVM(keeps=True)
self.assertTrue(running.proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual(list(running.has.values()), [True, False])
def test_a_guest_that_refuses_keeps_it_for_its_next_start_too(self):
running = RunningVM(fail=libvirt.VIR_DOMAIN_AFFECT_LIVE)
with self.assertRaisesRegex(OperationError, "refused; .*the running VM has it, its next start has it"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
self.assertEqual(list(running.has.values()), [True, True])
# removed from the running VM, then an error: said, and the next start keeps it
running = RunningVM(fail=libvirt.VIR_DOMAIN_AFFECT_LIVE, done=True)
with self.assertRaisesRegex(OperationError, "the running VM does not have it, its next start has it"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_failed_removal_from_the_next_start_of_a_running_vm_is_said(self):
running = RunningVM(now=False, later=True, fail=libvirt.VIR_DOMAIN_AFFECT_CONFIG)
with self.assertRaisesRegex(OperationError, "the running VM does not have it, its next start has it"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_removal_whose_outcome_cannot_be_read_says_so(self):
running = RunningVM()
reads = []
def describe(flags=0): # the reads before the removal work, the one after it does not
reads.append(flags)
if len(reads) > 2:
raise libvirt.libvirtError("connection reset")
return running.describe(flags)
running.proxy.instance.XMLDesc.side_effect = describe
with self.assertRaisesRegex(OperationError, "connection reset; whether the VM has PCI device 0000:05:00.0 now is not known"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_next_start_that_keeps_it_after_the_running_vm_let_it_go_is_said(self):
for keeps, where in ((False, "the running VM does not have it, its next start has it"),
(True, "the running VM has it, its next start has it")):
running = RunningVM(fail=libvirt.VIR_DOMAIN_AFFECT_CONFIG, keeps=keeps)
with self.subTest(keeps=keeps), self.assertRaisesRegex(OperationError, where):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_device_in_one_definition_only_is_taken_from_it(self):
for now, later in ((True, False), (False, True)):
running = RunningVM(now=now, later=later)
with self.subTest(now=now, later=later):
self.assertFalse(running.proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual(list(running.has.values()), [False, False])
self.assertEqual(running.proxy.instance.detachDeviceFlags.call_count, 1)
class MigrationTests(unittest.TestCase):
def test_a_vm_with_host_devices_is_not_migrated(self):
for devices, state in ((by_model(), 1), (by_model(), 5), ("<hostdev mode='subsystem' type='pci'/>", 1)):
source = FakeSource(state, xml=with_disks(devices))
with self.subTest(state=state), self.assertRaisesRegex(OperationError, "host devices cannot be migrated"):
destination().moveto(source, "vm", live=state == 1, unsafe=False, offline=state == 5)
self.assertEqual(source.instance.calls, [])

View file

@ -23,6 +23,9 @@ from libvirt import (
from vrtManager.connection import wvmConnect
from vrtManager.hostdetails import wvmHostDetails
from vrtManager.storage import wvmStorage, wvmStorages
from unittest import TestCase as UnitTestCase
from unittest.mock import patch
from vrtManager.hostdetails import cpu_percent
CAPABILITIES = """<capabilities><host><cpu><arch>x86_64</arch></cpu></host>
<guest><os_type>hvm</os_type><arch name='x86_64'><emulator>/usr/bin/qemu-kvm</emulator>
@ -270,3 +273,49 @@ class VolumesTestCase(unittest.TestCase):
stg = self.storage([])
stg.pool.storageVolLookupByName.return_value = volume("a.img", 4096, 0, "raw")
self.assertEqual(stg.get_volume_details("a.img"), {"name": "a.img", "size": 4096, "allocation": 0, "type": "raw"})
def host(*readings):
"""A wvmHostDetails whose getCPUStats returns readings, (idle, busy) each."""
h = wvmHostDetails.__new__(wvmHostDetails)
h.wvm = MagicMock()
h.wvm.getCPUStats.side_effect = [
{"kernel": busy // 2, "user": busy - busy // 2, "idle": idle, "iowait": 0, "guest": busy // 3, "utilization": 99}
for idle, busy in readings
]
return h
class CpuPercentTestCase(UnitTestCase):
def test_busy_share_between_two_samples(self):
self.assertEqual(cpu_percent((100, 200), (130, 300)), 70.0)
self.assertEqual(cpu_percent((0, 0), (1, 3)), 66.7)
def test_samples_that_do_not_follow_on(self):
for before, after in (((100, 200), (100, 200)), ((100, 200), (50, 100)), ((100, 200), (200, 250))):
with self.subTest(before=before, after=after):
self.assertIsNone(cpu_percent(before, after))
@patch("vrtManager.hostdetails.time")
class CpuUsageTestCase(UnitTestCase):
def test_since_a_previous_sample_without_waiting(self, clock):
clock.time.return_value = 105.0
h = host((130, 70))
usage = h.get_cpu_usage(previous=(100, 100, 100.0))
clock.sleep.assert_not_called()
# total = kernel + user + idle + iowait; guest (inside user) and utilization are left out
self.assertEqual(usage, {"usage": 70.0, "sample": (130, 200, 105.0), "window": 5.0})
def test_over_one_second_without_a_sample(self, clock):
clock.time.side_effect = [10.0, 11.0]
h = host((100, 100), (110, 190))
self.assertEqual(h.get_cpu_usage(), {"usage": 90.0, "sample": (110, 300, 11.0), "window": 1.0})
clock.sleep.assert_called_once_with(1)
def test_over_one_second_after_the_host_restarted(self, clock):
clock.time.side_effect = [10.0, 11.0]
h = host((5, 5), (6, 9))
usage = h.get_cpu_usage(previous=(1000, 5000, 5.0))
clock.sleep.assert_called_once_with(1)
self.assertEqual((usage["usage"], usage["window"]), (80.0, 1.0))

View file

@ -0,0 +1,131 @@
"""The interface XML wvmInterfaces writes from the form's values: IPv4,
IPv6 and a bridge's member."""
from unittest.mock import MagicMock
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.interface import wvmInterfaces
class Ipv6XmlTestCase(SimpleTestCase):
def xml(self, gw):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
conn.get_iface = MagicMock()
conn.get_iface.return_value.XMLDesc.return_value = "<interface type='ethernet' name='eth1'/>"
conn.create_iface("br1", "bridge", "onboot", "eth1", "none", "", "", "static", "2001:db8::10/64", gw, "on", 0)
return etree.fromstring(conn.define_iface.call_args[0][0])
def test_static_address_with_gateway(self):
proto = self.xml("2001:db8::1").find("protocol[@family='ipv6']")
self.assertEqual(proto.find("ip").attrib, {"address": "2001:db8::10", "prefix": "64"})
self.assertEqual(proto.find("route").get("gateway"), "2001:db8::1")
def test_no_empty_route_without_gateway(self):
self.assertIsNone(self.xml("").find("protocol[@family='ipv6']/route"))
class Ipv4XmlTestCase(SimpleTestCase):
def xml(self, addr, gw):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
conn.get_iface = MagicMock()
conn.get_iface.return_value.XMLDesc.return_value = "<interface type='ethernet' name='eth1'/>"
conn.create_iface("br1", "bridge", "onboot", "eth1", "static", addr, gw, "none", "", "", "on", 0)
return etree.fromstring(conn.define_iface.call_args[0][0])
def test_static_address_with_gateway(self):
proto = self.xml("192.0.2.10/24", "192.0.2.1").find("protocol[@family='ipv4']")
self.assertEqual(proto.find("ip").attrib, {"address": "192.0.2.10", "prefix": "24"})
self.assertEqual(proto.find("route").get("gateway"), "192.0.2.1")
def test_no_empty_route_without_gateway(self):
self.assertIsNone(self.xml("192.0.2.10/24", "").find("protocol[@family='ipv4']/route"))
def no_interface(name):
err = libvirt.libvirtError("Interface not found: no interface with matching name '%s'" % name)
err.err = (libvirt.VIR_ERR_NO_INTERFACE, libvirt.VIR_FROM_INTERFACE, str(err), 2, name, None, None, 0, 0)
return err
class BridgeMemberXmlTestCase(SimpleTestCase):
"""A bridge member is declared as the host defines it: a bond keeps its
mode and devices, a vlan its tag and device."""
def member(self, host_xml):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
def get_iface(name):
if host_xml is None:
raise no_interface(name)
iface = MagicMock()
iface.XMLDesc.return_value = host_xml
return iface
conn.get_iface = MagicMock(side_effect=get_iface)
conn.create_iface("br1", "bridge", "onboot", "bond0", "none", "", "", "none", "", "", "on", 0)
self.assertTrue(conn.define_iface.return_value.create.called)
members = etree.fromstring(conn.define_iface.call_args[0][0]).findall("bridge/interface")
self.assertEqual(len(members), 1)
return members[0]
def test_a_bond_keeps_its_mode_and_devices(self):
member = self.member(
"<interface type='bond' name='bond0'><start mode='onboot'/><mtu size='9000'/>"
"<protocol family='ipv4'><dhcp/></protocol>"
"<bond mode='active-backup'><miimon freq='100'/>"
"<interface type='ethernet' name='eth0'><mac address='52:54:00:00:00:01'/></interface>"
"<interface type='ethernet' name='eth1'/></bond></interface>"
)
self.assertEqual((member.get("type"), member.get("name")), ("bond", "bond0"))
self.assertEqual(member.find("bond").get("mode"), "active-backup")
self.assertEqual([i.get("name") for i in member.findall("bond/interface")], ["eth0", "eth1"])
self.assertEqual(member.find("bond/miimon").get("freq"), "100")
# what only a top-level interface has: the bridge holds the addresses
for tag in ("start", "mtu", "protocol"):
self.assertIsNone(member.find(tag), tag)
def test_a_vlan_keeps_its_tag_and_device(self):
member = self.member(
"<interface type='vlan' name='bond0'><start mode='onboot'/>"
"<vlan tag='42'><interface name='eth0'/></vlan></interface>"
)
self.assertEqual(member.get("type"), "vlan")
self.assertEqual(member.find("vlan").get("tag"), "42")
self.assertEqual(member.find("vlan/interface").get("name"), "eth0")
self.assertIsNone(member.find("start"))
def test_a_device_the_host_does_not_define_is_ethernet(self):
member = self.member(None)
self.assertEqual(member.attrib, {"type": "ethernet", "name": "bond0"})
def test_other_lookup_errors_are_not_hidden(self):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
err = libvirt.libvirtError("cannot recv data")
err.err = (libvirt.VIR_ERR_SYSTEM_ERROR, libvirt.VIR_FROM_RPC, str(err), 2, None, None, None, 0, 0)
conn.get_iface = MagicMock(side_effect=err)
with self.assertRaises(libvirt.libvirtError):
conn.create_iface("br1", "bridge", "onboot", "bond0", "none", "", "", "none", "", "", "on", 0)
self.assertFalse(conn.define_iface.called)
def test_an_interface_that_does_not_start_is_rolled_back(self):
# the definition, and the member's own configuration it changed, as they were
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
iface = conn.define_iface.return_value
iface.create.side_effect = libvirt.libvirtError("failed to bring up br1")
with self.assertRaises(libvirt.libvirtError):
conn.create_iface("br1", "ethernet", "onboot", "", "none", "", "", "none", "", "", "on", 0)
conn.wvm.changeRollback.assert_called_once_with(0)
conn.wvm.changeCommit.assert_not_called()

View file

@ -15,6 +15,14 @@ from vrtManager.instance import (
VIR_DOMAIN_BACKUP_BEGIN_PRESERVE_SHUTDOWN_DOMAIN,
wvmInstance,
)
import os
from xml.etree import ElementTree
from django.test import SimpleTestCase
from vrtManager.connection import connection_manager
from vrtManager.tests import livetest
P = livetest.PREFIX
GIB = 1024**3
UUID = "abababab-0000-1111-2222-333333333333"
@ -295,3 +303,106 @@ class DefineCloneTestCase(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class LiveCloneTestCase(SimpleTestCase):
@classmethod
def setUpClass(cls):
if not livetest.enabled():
raise unittest.SkipTest("Set TEST_LIBVIRT_HOST to run the live libvirt tests")
cls.args = (
os.environ["TEST_LIBVIRT_HOST"],
os.environ.get("TEST_LIBVIRT_LOGIN", ""),
os.environ.get("TEST_LIBVIRT_PASSWORD", ""),
int(os.environ.get("TEST_LIBVIRT_TYPE", 4)),
)
cls.conn = connection_manager.get_connection(*cls.args)
livetest.cleanup(cls.conn)
livetest.ensure_pool(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.remove_pool(cls.conn)
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def tearDown(self):
livetest.cleanup(self.conn)
def pool_volumes(self):
pool = self.conn.storagePoolLookupByName(livetest.POOL)
pool.refresh(0)
return set(pool.listVolumes())
def source(self, name):
disks = [livetest.create_volume(self.conn, P + f"{name}-{d}") for d in ("a", "b")]
dom = livetest.define_vm(self.conn, P + name, disks)
return dom, wvmInstance(*self.args, None, uuid=dom.UUIDString())
def clone_data(self, name):
return {
"name": P + name,
"disk-vda": P + name + "-a.qcow2",
"disk-vdb": P + name + "-b.qcow2",
"disk_owner_uid": 0,
"disk_owner_gid": 0,
}
def test_failed_clone_removes_only_its_own_copies(self):
_, vm = self.source("clone-src")
taken = livetest.create_volume(self.conn, P + "clone-dst-b") # destination of vdb exists
before = self.pool_volumes()
with self.assertRaises(Exception):
vm.clone_instance(self.clone_data("clone-dst"), "abababab-0000-1111-2222-333333333333")
self.assertEqual(self.pool_volumes(), before, "copies left behind or a volume deleted")
self.assertTrue(livetest.volume_exists(self.conn, taken))
with self.assertRaises(libvirt.libvirtError):
self.conn.lookupByName(P + "clone-dst")
def test_running_vm_is_not_cloned(self):
dom, vm = self.source("clone-run")
dom.create()
before = self.pool_volumes()
with self.assertRaises(Exception):
vm.clone_instance(self.clone_data("clone-run-copy"), "abababab-0000-1111-2222-333333333333")
self.assertEqual(self.pool_volumes(), before)
def test_clone_of_a_uefi_vm_has_its_own_copy_of_the_nvram(self):
disk = livetest.create_volume(self.conn, P + "clone-uefi-a")
dom = livetest.define_vm(self.conn, P + "clone-uefi", [disk], uefi=True)
dom.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED) # libvirt makes the NVRAM file at the first start
dom.destroy()
vm = wvmInstance(*self.args, None, uuid=dom.UUIDString())
source = vm.get_nvram()
uuid = vm.clone_instance(
{"name": P + "clone-uefi-copy", "disk-vda": P + "clone-uefi-copy-a.qcow2", "disk_owner_uid": 0, "disk_owner_gid": 0}, "abababab-0000-1111-2222-333333333333"
)
clone = self.conn.lookupByUUIDString(uuid)
path = ElementTree.fromstring(clone.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)).findtext("os/nvram")
self.assertEqual(path, os.path.join(os.path.dirname(source), P + "clone-uefi-copy_VARS.fd"))
with vm._nvram_pool() as pool:
copy = pool.storageVolLookupByName(os.path.basename(path))
self.assertEqual(copy.info()[1], pool.storageVolLookupByName(os.path.basename(source)).info()[1])
clone.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED) # qemu opens the copy
self.assertEqual(
ElementTree.fromstring(clone.XMLDesc(0)).findtext("os/nvram"), path, "libvirt replaced the copy"
)
clone.destroy()
def test_clone_of_a_stopped_vm(self):
_, vm = self.source("clone-ok")
uuid = vm.clone_instance(self.clone_data("clone-ok-copy"), "abababab-0000-1111-2222-333333333333")
clone = self.conn.lookupByUUIDString(uuid)
self.assertEqual(clone.name(), P + "clone-ok-copy")
self.assertTrue({P + "clone-ok-copy-a.qcow2", P + "clone-ok-copy-b.qcow2"} <= self.pool_volumes())

View file

@ -1,6 +1,6 @@
"""Live checks of bounded connection opens against real libvirt.
Runs only with TEST_LIBVIRT_HOST set (a TCP compute, see instances/tests/livetest.py);
Runs only with TEST_LIBVIRT_HOST set (a TCP compute, see vrtManager/tests/livetest.py);
TEST_LIBVIRT_SSH_HOST/_LOGIN add a reachable ssh compute. 192.0.2.1 is
TEST-NET-1: SYN packets to it are dropped, as for a host that went away
without a trace.
@ -34,6 +34,7 @@ import unittest
import libvirt
from vrtManager.connection import CONN_SSH, CONN_TCP, CONNECT_TIMEOUT, connection_manager
from vrtManager.tests import livetest
UNREACHABLE = "192.0.2.1"
KEEPALIVE_DEATH = 45 # keepalive (5, 5) closes a silent connection after ~30 s
@ -118,7 +119,7 @@ class Relay:
sock.close()
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_HOST"), "Set TEST_LIBVIRT_HOST to run the live libvirt tests")
@unittest.skipUnless(livetest.enabled(), "Set TEST_LIBVIRT_HOST to run the live libvirt tests")
class LiveBoundedOpenTestCase(unittest.TestCase):
def setUp(self):
self.host = os.environ["TEST_LIBVIRT_HOST"]

View file

@ -2,7 +2,7 @@
QEMU holds a running domain's image, so the size goes through blockResize;
the guest and the volume must both see it, and the persistent definition
stays as it was. Runs only with TEST_LIBVIRT_HOST set (see instances/tests/livetest.py).
stays as it was. Runs only with TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py).
"""
import os
@ -13,7 +13,7 @@ from django.test import SimpleTestCase
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from . import livetest
from vrtManager.tests import livetest
P = livetest.PREFIX
PERSISTENT = libvirt.VIR_DOMAIN_XML_INACTIVE | libvirt.VIR_DOMAIN_XML_SECURE

View file

@ -9,7 +9,7 @@ from lxml import etree
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from . import livetest
from vrtManager.tests import livetest
P = livetest.PREFIX
CDROMS = (

View file

@ -1,7 +1,7 @@
"""Live check that the NVRAM file a migrated VM leaves behind is removed,
and kept when the destination sees the same directory.
Runs only with TEST_LIBVIRT_HOST set (see instances/tests/livetest.py). The
Runs only with TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py). The
deletion needs a second host: TEST_LIBVIRT_SSH_HOST (and _LOGIN).
"""
@ -12,7 +12,7 @@ import libvirt
from django.test import SimpleTestCase
from lxml import etree
from instances.tests import livetest
from vrtManager.tests import livetest
from vrtManager.connection import CONN_SSH, NVRAM_DIR, connection_manager, wvmConnect
NAME = livetest.PREFIX + "nvram"

View file

@ -1,5 +1,5 @@
"""Live checks for external snapshots on libvirt's own API. Runs only with
TEST_LIBVIRT_HOST set (see instances/tests/livetest.py)."""
TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py)."""
import os
import unittest
@ -11,7 +11,7 @@ from vrtManager import util
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from . import livetest
from vrtManager.tests import livetest
P = livetest.PREFIX

View file

@ -2,7 +2,7 @@
Every edit that redefines the domain must start from the persistent
definition with secrets (VIR_DOMAIN_XML_INACTIVE | VIR_DOMAIN_XML_SECURE).
Runs only with TEST_LIBVIRT_HOST set (see instances/tests/livetest.py).
Runs only with TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py).
"""
import os
@ -14,7 +14,7 @@ from lxml import etree
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from . import livetest
from vrtManager.tests import livetest
P = livetest.PREFIX
MAC = "52:54:00:aa:01:01"

View file

@ -5,7 +5,7 @@ before and after. The edit must show its effect, and once the nodes it is
allowed to touch are removed from both sides, the rest of the definition
must be identical: unknown elements, secrets and pending changes survive.
On a running VM, edits of the persistent definition must leave the live
one alone. Runs only with TEST_LIBVIRT_HOST set (see instances/tests/livetest.py).
one alone. Runs only with TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py).
"""
import os
@ -17,7 +17,7 @@ from lxml import etree
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from . import livetest
from vrtManager.tests import livetest
P = livetest.PREFIX
MAC = "52:54:00:aa:02:01"

View file

@ -9,180 +9,7 @@ import libvirt
from vrtManager import util
from vrtManager.instance import wvmInstances
def no_domain():
error = libvirt.libvirtError("Domain not found")
error.err = (libvirt.VIR_ERR_NO_DOMAIN,)
return error
class FakeDomain:
def __init__(self, xml="<domain/>", snapshots=0, saved=False, undefine_fails=False, undefine_lost=False,
started=False):
self.calls = []
self.xml = xml
self.snapshots = snapshots
self.saved = saved
self.undefine_fails = undefine_fails
self.undefine_lost = undefine_lost # undefined, but the reply is lost
self.started = started # started by another client during the move
self.undefined = None
self.redefined = []
self.restored_autostart = None
def migrate(self, dconn, flags, dname, uri, bandwidth):
self.calls.append((flags, uri))
def name(self):
return "vm"
def UUIDString(self):
return "u-1"
inactive_xml = None # the persistent definition, when it differs
def XMLDesc(self, flags):
if flags & libvirt.VIR_DOMAIN_XML_INACTIVE and self.inactive_xml:
return self.inactive_xml
return self.xml
def snapshotNum(self, flags=0):
return self.snapshots
def hasManagedSaveImage(self, flags=0):
return self.saved
def undefineFlags(self, flags):
if self.undefine_fails:
raise libvirt.libvirtError("undefine failed")
self.undefined = flags
if self.undefine_lost:
raise libvirt.libvirtError("Cannot recv data")
def isPersistent(self):
return self.undefined is None or bool(self.redefined)
def isActive(self):
if not self.started and self.undefined is not None:
raise no_domain() # libvirt: an undefined shut-off VM is gone
return self.started
def autostart(self):
return 1
def setAutostart(self, value):
self.restored_autostart = value
def connect(self):
return self
def defineXML(self, xml):
self.redefined.append(xml)
return self
def no_volume():
error = libvirt.libvirtError("Storage volume not found")
error.err = (libvirt.VIR_ERR_NO_STORAGE_VOL,)
return error
class FakeVolume:
def __init__(self, backing):
self.backing = backing
def XMLDesc(self, flags=0):
if self.backing is None:
return "<volume><target><path>x</path></target></volume>"
return f"<volume><backingStore><path>{self.backing}</path></backingStore></volume>"
class FakePool:
def __init__(self, host, active=True):
self.host = host
self.active = active
def isActive(self):
return self.active
def XMLDesc(self, flags=0):
return "<pool><target><path>/elsewhere</path></target></pool>"
def refresh(self, flags=0):
self.host.refreshes += 1
self.host.volumes |= self.host.unlisted
self.host.unlisted = set()
def storageVolLookupByName(self, name):
return self.host.storageVolLookupByPath(name)
class FakeHost:
"""The destination's libvirt connection. volumes: the paths its pools
list; unlisted: files its pools show after a refresh."""
def __init__(self, existing=(), volumes=(), unlisted=(), backing=None, inactive_pools=()):
self.existing = set(existing)
self.defined = []
self.new = []
self.volumes = set(volumes)
self.unlisted = set(unlisted)
self.backing = backing or {} # path: the path of its backing file
self.inactive_pools = set(inactive_pools)
self.refreshes = 0
def storageVolLookupByPath(self, path):
if path not in self.volumes:
raise no_volume()
return FakeVolume(self.backing.get(path))
def storagePoolLookupByName(self, name):
return FakePool(self, active=name not in self.inactive_pools)
def listAllStoragePools(self, flags=0):
return [FakePool(self)]
def lookup(self, key):
if key in self.existing:
return FakeDomain()
raise no_domain()
lookupByName = lookupByUUIDString = lookup
def defineXML(self, xml):
self.defined.append(xml)
self.new.append(FakeDomain())
return self.new[-1]
class FakeSource:
def __init__(self, state, nvram=None, **domain):
self.state = state
self.nvram = nvram
self.instance = FakeDomain(**domain)
def get_nvram(self):
return self.nvram
reads = 0
def get_status(self):
self.reads += 1
return self.state
def get_arch(self):
return "x86_64"
def get_dom_emulator(self):
return "/usr/bin/qemu-system-x86_64"
def destination(host=None):
dest = wvmInstances.__new__(wvmInstances)
dest.wvm = host or FakeHost()
dest.get_emulator = lambda arch: "/usr/bin/qemu-system-x86_64"
return dest
from vrtManager.tests.fakes import FakeDomain, FakeHost, FakeSource, destination, with_disks
class MigrationFlagsTestCase(unittest.TestCase):
@ -325,10 +152,6 @@ class MigrationModeTestCase(unittest.TestCase):
self.assertEqual(self.migrate(5, live=True, autoconverge=True, compress=True), ("offline", None))
def with_disks(*disks):
return "<domain><devices>%s</devices></domain>" % "".join(disks)
FILE_DISK = "<disk type='file' device='disk'><source file='/pool/vm.qcow2'/><target dev='vda'/></disk>"

View file

@ -1,19 +1,20 @@
"""A network's addresses are read from its XML however libvirt writes it, each
<ip> by its own index; a page reads each definition once; a change starts
from the saved definition, so two in a row keep each other; a fixed address
is found by the address it had and checked against its subnet; the XML
editor defines only this network; a network that does not start is not left
defined. Runs on a private libvirt test driver: "test:///default" is shared
by the whole process."""
"""wvmNetwork: a change starts from the saved definition, so two in a row
keep each other; a fixed address is found by the address it had; the XML
editor defines only this network; a network that does not start is not
left defined; the DHCP leases are read. Runs on a private libvirt test
driver: "test:///default" is shared by the whole process."""
import tempfile
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
import libvirt
from django.test import SimpleTestCase
from networks.tests.driver import without_inactive_flag
from lxml import etree
from vrtManager.network import ip_networks, network_size, wvmNetwork, wvmNetworks
from vrtManager.network import network_size, wvmNetwork, wvmNetworks
from vrtManager.tests.driver import without_inactive_flag
from vrtManager.tests.fakes import DUID, lease
from vrtManager.util import OperationError
NODE = """<node>
@ -25,23 +26,6 @@ NODE = """<node>
</node>"""
class AddressTests(SimpleTestCase):
def test_an_address_without_a_prefix_takes_libvirts(self):
tree = etree.fromstring("<network><ip address='192.0.2.1'/><ip address='10.1.2.3'/><ip family='ipv6' address='2001:db8::1'/></network>")
self.assertEqual([str(ip["subnet"]) for ip in ip_networks(tree)], ["192.0.2.0/24", "10.0.0.0/8", "2001:db8::/64"])
def test_every_way_libvirt_writes_an_address(self):
compact = "<network><ip address='192.0.2.1' netmask='255.255.255.0'/></network>"
static = ("<network><ip address='192.0.2.1' prefix='24'><dhcp><host mac='52:54:00:00:00:01' ip='192.0.2.5'/>"
"</dhcp></ip><ip family='ipv6' address='2001:db8::1' prefix='64'/></network>")
(ip,) = ip_networks(etree.fromstring(compact))
self.assertEqual((ip["index"], ip["family"], str(ip["subnet"]), ip["dhcp"]), (0, "ipv4", "192.0.2.0/24", False))
v4, v6 = ip_networks(etree.fromstring(static))
# DHCP with fixed addresses only is DHCP too
self.assertEqual((v4["dhcp"], v4["range"], v4["hosts"][0]["ip"]), (True, None, "192.0.2.5"))
self.assertEqual((v6["index"], v6["family"], str(v6["subnet"])), (1, "ipv6", "2001:db8::/64"))
class NetworkChangeTests(SimpleTestCase):
def setUp(self):
node = tempfile.NamedTemporaryFile("w", suffix=".xml")
@ -182,3 +166,26 @@ class NetworkChangeTests(SimpleTestCase):
wvmNetworks("localhost", "", "", 4).create_network(
"n1", "nat", True, gateway, mask, dhcp, False, None, None, None, "", False)
self.assertNotIn("n1", self.conn.listDefinedNetworks() + self.conn.listNetworks())
class LeasesTests(SimpleTestCase):
def network(self, leases, active=True):
net = wvmNetwork.__new__(wvmNetwork)
net.net = MagicMock(**{"isActive.return_value": active, "DHCPLeases.return_value": leases})
return net
def test_leases_by_address_ipv4_first(self):
net = self.network([lease("2001:db8:1::40", family=1, clientid=DUID, expiry=0),
lease("192.0.2.100", "52:54:00:AA:00:01", hostname="db"),
lease("192.0.2.9", "52:54:00:aa:00:02")])
ipv4_9, ipv4_100, ipv6 = net.get_leases()
self.assertEqual([str(ipv4_9["ip"]), str(ipv4_100["ip"]), str(ipv6["ip"])], ["192.0.2.9", "192.0.2.100", "2001:db8:1::40"])
self.assertEqual((ipv4_100["mac"], ipv4_100["hostname"], ipv4_100["expires"]),
("52:54:00:aa:00:01", "db", datetime.fromtimestamp(1793265489, timezone.utc)))
# an IPv6 lease: no MAC, its client's DUID; one that never ends
self.assertEqual((ipv6["mac"], ipv6["client_id"], ipv6["expires"]), ("", DUID, None))
def test_a_network_that_does_not_run_has_none(self):
net = self.network([lease("192.0.2.9")], active=False)
self.assertEqual(net.get_leases(), [])
net.net.DHCPLeases.assert_not_called()

View file

@ -2,6 +2,7 @@ import unittest
from lxml import etree
from vrtManager.network import ip_networks, network_size
from django.test import SimpleTestCase
class TestNetworkSize(unittest.TestCase):
@ -96,3 +97,20 @@ class TestIpNetworks(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class AddressTests(SimpleTestCase):
def test_an_address_without_a_prefix_takes_libvirts(self):
tree = etree.fromstring("<network><ip address='192.0.2.1'/><ip address='10.1.2.3'/><ip family='ipv6' address='2001:db8::1'/></network>")
self.assertEqual([str(ip["subnet"]) for ip in ip_networks(tree)], ["192.0.2.0/24", "10.0.0.0/8", "2001:db8::/64"])
def test_every_way_libvirt_writes_an_address(self):
compact = "<network><ip address='192.0.2.1' netmask='255.255.255.0'/></network>"
static = ("<network><ip address='192.0.2.1' prefix='24'><dhcp><host mac='52:54:00:00:00:01' ip='192.0.2.5'/>"
"</dhcp></ip><ip family='ipv6' address='2001:db8::1' prefix='64'/></network>")
(ip,) = ip_networks(etree.fromstring(compact))
self.assertEqual((ip["index"], ip["family"], str(ip["subnet"]), ip["dhcp"]), (0, "ipv4", "192.0.2.0/24", False))
v4, v6 = ip_networks(etree.fromstring(static))
# DHCP with fixed addresses only is DHCP too
self.assertEqual((v4["dhcp"], v4["range"], v4["hosts"][0]["ip"]), (True, None, "192.0.2.5"))
self.assertEqual((v6["index"], v6["family"], str(v6["subnet"])), (1, "ipv6", "2001:db8::/64"))

View file

@ -0,0 +1,110 @@
"""The host's node devices as vrtManager lists them: PCI devices by IOMMU
group and USB devices, each with what the host has on it and the VMs
that name it. Node device XML as libvirt 11.10 writes it; the connection
is a mock, libvirt's test driver has no node devices."""
from unittest.mock import MagicMock
import libvirt
from django.test import SimpleTestCase
from vrtManager import util
from vrtManager.connection import wvmConnect
from vrtManager.nodedevs import hostdev_problems, node_devices, pci_address
from vrtManager.tests.fakes import NODES, PCI_0A, USB_1_3, node, pci, vm
PCI_01 = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x01' function='0x0'/></source></hostdev>"
USB_MODEL = "<hostdev mode='subsystem' type='usb'><source><vendor id='0x0627'/><product id='0x0001'/></source></hostdev>"
def devices(nodes=NODES, domains=()):
conn = wvmConnect.__new__(wvmConnect)
conn.wvm = MagicMock(**{"listAllDevices.return_value": [node(*n) for n in nodes],
"listAllDomains.return_value": list(domains)})
return node_devices(conn)
class NodeDevicesTests(SimpleTestCase):
def test_pci_devices_by_group_with_what_the_host_has_on_them(self):
pci_devices, _ = devices()
self.assertEqual([(d["address"], d["group"]) for d in pci_devices],
[("0000:00:02.0", 2), ("0000:00:03.0", 2), ("0000:00:06.0", 3), ("0000:00:0a.0", 4), ("0000:00:01.0", None)])
gpu, disk, nic, spare, _ = pci_devices
# a graphics card's primary node is the host's; its render node is not a resource of its own
self.assertEqual((gpu["resources"], disk["resources"], nic["resources"], spare["resources"]),
(["/dev/dri/card0"], ["/dev/vda"], ["eth0"], []))
self.assertEqual((nic["kind"], nic["driver"], nic["ids"]), ("Network controller", "virtio-pci", "1af4:1000"))
def test_usb_devices_with_their_classes_hubs_and_same_models(self):
_, (hub, tablet_2, tablet_3) = devices()
self.assertEqual((hub["kinds"], hub["same_model"]), (["Hub"], False))
self.assertEqual((tablet_2["kinds"], tablet_2["same_model"], tablet_3["same_model"]), (["Human interface"], True, True))
def test_the_vms_that_name_a_device(self):
pci_devices, (_, tablet_2, tablet_3) = devices(domains=[vm("db-1", PCI_0A + USB_1_3), vm("kiosk-1", USB_MODEL)])
self.assertEqual([d["vms"] for d in pci_devices], [[], [], [], ["db-1"], []])
# by address: that device; by model only: any device of the model
self.assertEqual((tablet_3["vms"], tablet_3["by_model"]), (["db-1"], ["kiosk-1"]))
self.assertEqual((tablet_2["vms"], tablet_2["by_model"]), ([], ["kiosk-1"]))
def test_a_usb_controller_holds_the_usb_devices_below_it(self):
pci_devices, (hub, tablet_2, _) = devices(domains=[vm("kiosk-1", USB_1_3)])
controller = [d for d in pci_devices if d["address"] == "0000:00:01.0"][0]
# its root hub is no device of the host's; the tablets are, one given to a VM
self.assertEqual(controller["resources"], ["USB device 1/2 (QEMU Tablet)", "USB device 1/3 (QEMU Tablet)"])
self.assertEqual((hub["controller"], tablet_2["controller"]), ("0000:00:01.0", "0000:00:01.0"))
def test_a_usb_device_on_a_controller_given_to_a_vm_is_not_given_to_another(self):
pci_devices, usb_devices = devices(domains=[vm("db-1", PCI_01)])
reasons = [util.render(m["text"], m["values"], str) for m in hostdev_problems(
"kiosk-1", pci_devices, usb_devices, set(), [("0x0627", "0x0001", (1, 2))])]
self.assertIn("USB device 1/2 (QEMU Tablet) is on the PCI controller 0000:00:01.0, given to db-1", reasons)
def test_a_usb_device_is_named_as_libvirt_picks_one(self):
def named(hostdev):
_, (_, tablet_2, tablet_3) = devices(domains=[vm("v", hostdev)])
return [(d["vms"], d["by_model"]) for d in (tablet_2, tablet_3)]
# an address with other IDs: another device now at that address
other = USB_1_3.replace("0x0627", "0x046d")
address_only = "<hostdev mode='subsystem' type='usb'><source><address bus='1' device='2'/></source></hostdev>"
self.assertEqual(named(other), [([], []), ([], [])])
self.assertEqual(named(address_only), [(["v"], []), ([], [])])
self.assertEqual(named(USB_MODEL), [([], ["v"]), ([], ["v"])])
def test_a_pci_device_named_as_a_nic_or_only_for_the_next_start(self):
nic = ("<interface type='hostdev'><source><address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>"
"</source></interface>")
pci_devices, _ = devices(domains=[vm("nic-1", nic), vm("next-1", "", saved=PCI_0A)])
self.assertEqual({d["address"]: d["vms"] for d in pci_devices if d["vms"]},
{"0000:00:06.0": ["nic-1"], "0000:00:0a.0": ["next-1"]})
def test_sr_iov_functions_and_unnamed_hardware(self):
vf = pci("0x0b", "", "0x020000", group=5)[1].replace(
"<product id='0x1000'></product>", "<product id='0x10ed'/>").replace(
"</capability></device>", "<capability type='phys_function'><address domain='0x0000' bus='0x00' slot='0x0a'"
" function='0x0'/></capability></capability></device>")
pf = NODES[8][1].replace("</capability></device>", "<capability type='virt_functions' maxCount='7'><address domain='0x0000'"
" bus='0x00' slot='0x0b' function='0x0'/></capability></capability></device>")
pci_devices, _ = devices(nodes=[("pci_0000_00_0a_0", pf), ("pci_0000_00_0b_0", vf)])
self.assertEqual([(d["physfn"], d["vfs"], d["product"]) for d in pci_devices],
[("", 1, "Ethernet controller"), ("0000:00:0a.0", 0, "0x10ed")])
def test_a_device_gone_meanwhile_is_left_out_other_errors_are_raised(self):
for code, raised in ((libvirt.VIR_ERR_NO_NODE_DEVICE, False), (libvirt.VIR_ERR_OPERATION_FAILED, True)):
err = libvirt.libvirtError("gone")
err.get_error_code = lambda code=code: code
gone = node("pci_0000_00_0b_0", "")
gone.XMLDesc.side_effect = err
conn = wvmConnect.__new__(wvmConnect)
conn.wvm = MagicMock(**{"listAllDevices.return_value": [gone, node(*NODES[0])], "listAllDomains.return_value": []})
with self.subTest(code=code):
if raised:
self.assertRaises(libvirt.libvirtError, node_devices, conn)
else:
self.assertEqual([d["address"] for d in node_devices(conn)[0]], ["0000:00:06.0"])
def test_a_pci_address_from_libvirts_hex_or_numbers(self):
self.assertEqual(pci_address("0x0000", "0x00", "0x1f", "0x3"), pci_address(0, 0, 31, 3))
self.assertEqual(pci_address(0, 0, 31, 3), "0000:00:1f.3")

View file

@ -12,50 +12,17 @@ from django.test import SimpleTestCase
from lxml import etree
from vrtManager import util
from vrtManager.nodedevs import _pci, check_hostdevs, hostdev_problems, pci_problems
from vrtManager.tests.fakes import IOMMU_NODES
from vrtManager.util import OperationError
def device(slot, product, code, driver, group, members=None, bridge=False):
"""A PCI device on bus 0 at slot, in IOMMU group with the slots members (its own by default)."""
members = members or [slot]
addresses = "".join(f"<address domain='0x0000' bus='0x00' slot='0x{m:02x}' function='0x0'/>" for m in members)
driver_xml = f"<driver><name>{driver}</name></driver>" if driver else ""
bridge_xml = "<capability type='pci-bridge'/>" if bridge else ""
xml = (f"<device><name>pci_0000_00_{slot:02x}_0</name><parent>computer</parent>{driver_xml}"
f"<capability type='pci'><class>{code}</class><domain>0</domain><bus>0</bus><slot>{slot}</slot><function>0</function>"
f"<product id='0x0001'>{product}</product><vendor id='0x8086'>Intel</vendor>{bridge_xml}"
f"<iommuGroup number='{group}'>{addresses}</iommuGroup></capability></device>")
return f"pci_0000_00_{slot:02x}_0", etree.fromstring(xml)
NODES = dict([
device(0x02, "PCIe Root port", "0x060400", "pcieport", 2, bridge=True),
device(0x03, "SATA controller", "0x010601", "ahci", 7, [0x03, 0x04, 0x05]),
device(0x04, "SMBus controller", "0x0c0500", "i801_smbus", 7, [0x03, 0x04, 0x05]),
device(0x05, "LPC controller", "0x060100", "lpc_ich", 7, [0x03, 0x04, 0x05]), # class 0x06, not a bridge
device(0x06, "Gigabit NIC", "0x020000", "e1000e", 12),
device(0x07, "Spare NIC", "0x020000", "vfio-pci", 13),
device(0x08, "NIC port 0", "0x020000", "vfio-pci", 14, [0x08, 0x09, 0x0a]),
device(0x09, "NIC port 1", "0x020000", None, 14, [0x08, 0x09, 0x0a]),
device(0x0a, "Downstream port", "0x060400", "pcieport", 14, [0x08, 0x09, 0x0a], bridge=True),
device(0x0b, "Accelerator", "0x120000", "vfio-pci", 15, [0x0b, 0x0c]), # 0x0c cannot be read
device(0x0d, "GPU", "0x030000", "nouveau", 16, [0x0d, 0x0e]),
device(0x0e, "GPU audio", "0x040300", "snd_hda_intel", 16, [0x0d, 0x0e]),
])
# what the host has below: a disk on the SATA controller, its interface on the e1000e
NODES["block_sda"] = etree.fromstring("<device><name>block_sda</name><parent>pci_0000_00_03_0</parent>"
"<capability type='storage'><block>/dev/sda</block></capability></device>")
NODES["net_eth1"] = etree.fromstring("<device><name>net_eth1</name><parent>pci_0000_00_06_0</parent>"
"<capability type='net'><interface>eth1</interface></capability></device>")
def inventory(vms=()):
"""The host's PCI devices, as a list; vms: (VM, its PCI addresses)."""
children = {}
for name, node in NODES.items():
for name, node in IOMMU_NODES.items():
children.setdefault(node.findtext("parent"), []).append(name)
users = [(vm, set(addresses), []) for vm, addresses in vms]
return [_pci(name, node, NODES, children, users) for name, node in NODES.items()
return [_pci(name, node, IOMMU_NODES, children, users) for name, node in IOMMU_NODES.items()
if node.find("capability").get("type") == "pci"]
@ -121,7 +88,7 @@ class DefinitionsTests(SimpleTestCase):
conn = MagicMock()
conn.wvm.listAllDevices.return_value = [MagicMock(**{"name.return_value": name,
"XMLDesc.return_value": etree.tostring(node)})
for name, node in NODES.items()]
for name, node in IOMMU_NODES.items()]
conn.domain_trees.return_value = []
trees = [etree.fromstring(f"<domain><devices>{''.join(devices)}</devices></domain>") for devices in devices_per_definition]
check_hostdevs(conn, "web-1", trees)

View file

@ -0,0 +1,188 @@
"""wvmSecrets: a secret's usage goes into its XML as text; its value is a
Ceph key in base64 or a password as typed, and a value refused leaves no
secret behind; what uses a secret, and deleting one."""
from unittest.mock import MagicMock, patch
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.tests.fakes import CEPH
from vrtManager.util import OperationError
from vrtManager.virtsecrets import wvmSecrets
def connection_and_libvirt():
with patch("vrtManager.connection.connection_manager.get_connection") as get:
conn = wvmSecrets("host", "login", "pw", 1)
return conn, get.return_value
class CreateSecretTests(SimpleTestCase):
def test_the_usage_is_text_of_its_element(self):
for usage_type, element, usage in (
("volume", "volume", "/var/lib/libvirt/images/vm-1.qcow2"),
("iscsi", "target", "iqn.2026-10.test.wvc:lun"),
("ceph", "name", "client.libvirt secret"),
("tls", "name", "migration"),
("vtpm", "name", "VM web-1's vTPM"),
("volume", "volume", "/a'b<c>&d\""),
):
with self.subTest(usage_type=usage_type, usage=usage):
conn, wvm = connection_and_libvirt()
conn.create_secret(usage_type, usage, private=True)
secret = etree.fromstring(wvm.secretDefineXML.call_args.args[0])
self.assertEqual((secret.get("ephemeral"), secret.get("private")), ("no", "yes"))
self.assertEqual(secret.find("usage").get("type"), usage_type)
self.assertEqual(secret.findtext(f"usage/{element}"), usage)
wvm.secretDefineXML.return_value.setValue.assert_not_called()
def test_a_new_secret_takes_its_value_at_once(self):
for usage_type, value, stored in (("ceph", "cHcA/w==", b"pw\x00\xff"), ("iscsi", " pass word ", b" pass word ")):
with self.subTest(usage_type=usage_type):
conn, wvm = connection_and_libvirt()
conn.create_secret(usage_type, "lun", value)
wvm.secretDefineXML.return_value.setValue.assert_called_once_with(stored)
def test_a_value_refused_leaves_no_secret(self):
conn, wvm = connection_and_libvirt()
with self.assertRaisesRegex(OperationError, "base64"):
conn.create_secret("ceph", "client.libvirt", "not base64!")
wvm.secretDefineXML.assert_not_called()
wvm.secretDefineXML.return_value.setValue.side_effect = libvirt.libvirtError("refused")
with self.assertRaises(libvirt.libvirtError):
conn.create_secret("iscsi", "lun", "pw")
wvm.secretDefineXML.return_value.undefine.assert_called_once_with()
class SecretValueTests(SimpleTestCase):
def set_value(self, usage_type, value):
conn, wvm = connection_and_libvirt()
secret = wvm.secretLookupByUUIDString.return_value
secret.XMLDesc.return_value = f"<secret><usage type='{usage_type}'><name>x</name></usage></secret>"
conn.set_secret_value("uuid", value)
return secret.setValue
def test_a_ceph_key_is_stored_as_its_bytes_a_password_as_typed(self):
self.set_value("ceph", "cHcA/w==").assert_called_once_with(b"pw\x00\xff")
# a password that happens to be base64 is not decoded
self.set_value("iscsi", "cHcA/w==").assert_called_once_with(b"cHcA/w==")
self.set_value("vtpm", " pässword ").assert_called_once_with(" pässword ".encode())
def test_a_ceph_key_that_is_not_base64_is_refused_without_the_value(self):
for value in ("b'cHc='", "not base64!", "kéy"):
with self.subTest(value=value), self.assertRaises(OperationError) as refused:
self.set_value("ceph", value)
self.assertNotIn(value, str(refused.exception))
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"<secret><usage type='{usage_type}'><name>{usage}</name></usage></secret>"})
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"<domain><devices>{devices}</devices></domain>", **kwargs)
def pool(name, auth, **kwargs):
return owner(name, f"<pool type='rbd'><source>{auth}</source></pool>", **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"<disk type='network'><auth username='libvirt'><secret type='ceph' uuid='{CEPH}'/></auth></disk>"),
vm("lun-1", "<hostdev mode='subsystem' type='scsi'><source protocol='iscsi'><auth username='u'>"
"<secret type='iscsi' usage='client.libvirt'/></auth></source></hostdev>"),
vm("tpm-1", f"<tpm model='tpm-crb'><backend type='emulator'><encryption secret='{VTPM}'/></backend></tpm>"),
])
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", "<auth type='ceph' username='libvirt'><secret usage='client.libvirt'/></auth>"),
# chap is iSCSI; named only for its next start
pool("lun", "", saved="<pool><source><auth type='chap' username='u'><secret usage='client.libvirt'/>"
"</auth></source></pool>", 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 = "<disk><auth username='libvirt'><secret type='ceph' usage='client.libvirt'/></auth></disk>"
# saved only: read whether it runs or not, as it may stop between the reads
for kwargs in ({"active": True, "saved": f"<domain><devices>{disk}</devices></domain>"},
{"active": False, "saved": f"<domain><devices>{disk}</devices></domain>"},
{"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"<disk><auth><secret type='ceph' uuid='{CEPH}'/></auth></disk>")])
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"<disk><auth><secret type='ceph' uuid='{CEPH}'/></auth></disk>")],
pools=[pool("ceph-rbd", "<auth type='ceph'><secret usage='client.libvirt'/></auth>")])
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"<disk><auth><secret type='ceph' uuid='{CEPH}'/></auth></disk>")])
conn.delete_secret(ISCSI)
conn.wvm.secretLookupByUUIDString.assert_called_once_with(ISCSI)
conn.wvm.secretLookupByUUIDString.return_value.undefine.assert_called_once_with()

View file

@ -8,7 +8,7 @@ import libvirt
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import TestCase
from networks.tests.driver import without_inactive_flag
from vrtManager.tests.driver import without_inactive_flag
from django.urls import NoReverseMatch, URLPattern, URLResolver, get_resolver, reverse
from instances.models import Flavor, Instance
from lxml import html