summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-06-05 20:51:01 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-06-12 02:58:55 +0000
commit7d6e51b7706e305fa393bbc741c869c3d56f5abe (patch)
treeaa1b7aec13f7ce6a74062a0f6cd2838d18293cfb /tests
parent1feb6132d9b56e92903cca7f0f0d50cbee1c4e02 (diff)
downloadmeta-virtualization-7d6e51b7706e305fa393bbc741c869c3d56f5abe.tar.gz
tests: add libvirt recipe and runtime tests
46 tests across three tiers: Tier 1 (static, no build): Recipe structure, PACKAGECONFIG options, hook_support.py python3 compatibility (text mode, raw string regex, syntax validation), service files, patch files, kvm-image-minimal recipe checks. Tier 2 (build): libvirt and kvm-image-minimal build verification. Tier 3 (boot): libvirtd service status, virtlockd socket, virsh connectivity via monolithic daemon socket, capabilities, nodeinfo, domain listing, default network, hook script installation and permissions, qemu user and libvirt group existence. The boot tests use the explicit monolithic daemon socket path (qemu+unix:///system?socket=/var/run/libvirt/libvirt-sock) because libvirt v12 defaults to modular daemons (virtqemud) but the kvm-image-minimal recipe runs the monolithic libvirtd. Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/test_libvirt.py468
1 files changed, 468 insertions, 0 deletions
diff --git a/tests/test_libvirt.py b/tests/test_libvirt.py
new file mode 100644
index 00000000..442df8c3
--- /dev/null
+++ b/tests/test_libvirt.py
@@ -0,0 +1,468 @@
1# SPDX-FileCopyrightText: Copyright (C) 2026 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4"""
5Libvirt recipe and runtime tests.
6
7Tier 1: Static assertions on recipe files (no build required)
8Tier 2: Build verification (requires bitbake)
9Tier 3: Boot tests on kvm-image-minimal (requires QEMU with KVM)
10
11The tests automatically build kvm-image-minimal with the kvm
12DISTRO_FEATURE before booting. No local.conf changes needed.
13
14Run:
15 # Static tests only
16 pytest tests/test_libvirt.py -v -m "not slow and not boot" --poky-dir /opt/bruce/poky
17
18 # All tests including build and boot
19 pytest tests/test_libvirt.py -v --poky-dir /opt/bruce/poky
20
21Options:
22 --boot-timeout QEMU boot timeout (default: 120s)
23 --no-kvm Disable KVM acceleration
24"""
25
26import os
27import re
28import subprocess
29import tempfile
30import time
31import pytest
32from pathlib import Path
33
34try:
35 import pexpect
36 PEXPECT_AVAILABLE = True
37except ImportError:
38 PEXPECT_AVAILABLE = False
39
40
41def _run_bitbake(build_dir, recipe, extra_vars=None, timeout=3600):
42 """Run bitbake with optional variable overrides via -R conf file."""
43 bb_cmd = "bitbake"
44 conf_file = None
45 if extra_vars:
46 conf_file = tempfile.NamedTemporaryFile(
47 mode='w', suffix='.conf', prefix='pytest-libvirt-',
48 dir=str(build_dir / "conf"), delete=False)
49 for var, val in extra_vars.items():
50 conf_file.write(f'{var} = "{val}"\n')
51 conf_file.close()
52 bb_cmd += f" -R {conf_file.name}"
53 bb_cmd += f" {recipe}"
54 poky_dir = build_dir.parent
55 full_cmd = f"bash -c 'cd {poky_dir} && source oe-init-build-env {build_dir} >/dev/null 2>&1 && {bb_cmd}'"
56 try:
57 return subprocess.run(full_cmd, shell=True, cwd=build_dir,
58 timeout=timeout, capture_output=True, text=True)
59 finally:
60 if conf_file:
61 os.unlink(conf_file.name)
62
63
64# ============================================================================
65# Fixtures
66# ============================================================================
67
68@pytest.fixture(scope="module")
69def poky_dir(request):
70 path = Path(request.config.getoption("--poky-dir"))
71 if not path.exists():
72 pytest.skip(f"Poky directory not found: {path}")
73 return path
74
75
76@pytest.fixture(scope="module")
77def build_dir(request, poky_dir):
78 bd = request.config.getoption("--build-dir")
79 path = Path(bd) if bd else poky_dir / "build"
80 if not path.exists():
81 pytest.skip(f"Build directory not found: {path}")
82 return path
83
84
85@pytest.fixture(scope="module")
86def meta_virt_dir(poky_dir):
87 path = poky_dir / "meta-virtualization"
88 if not path.exists():
89 pytest.skip(f"meta-virtualization not found: {path}")
90 return path
91
92
93@pytest.fixture(scope="module")
94def libvirt_recipe(meta_virt_dir):
95 path = meta_virt_dir / "recipes-extended" / "libvirt" / "libvirt_git.bb"
96 if not path.exists():
97 pytest.skip(f"libvirt recipe not found: {path}")
98 return path
99
100
101@pytest.fixture(scope="module")
102def libvirt_files_dir(meta_virt_dir):
103 path = meta_virt_dir / "recipes-extended" / "libvirt" / "libvirt"
104 if not path.exists():
105 pytest.skip(f"libvirt files dir not found: {path}")
106 return path
107
108
109@pytest.fixture(scope="module")
110def kvm_image(build_dir):
111 """Build kvm-image-minimal with kvm DISTRO_FEATURE."""
112 result = _run_bitbake(
113 build_dir, "kvm-image-minimal",
114 extra_vars={
115 "DISTRO_FEATURES:append": " kvm",
116 },
117 )
118 if result.returncode != 0:
119 pytest.fail(f"KVM image build failed: {result.stderr}")
120
121
122@pytest.fixture(scope="module")
123def libvirt_session(request, poky_dir, build_dir, kvm_image):
124 """Boot kvm-image-minimal and provide a pexpect session."""
125 if not PEXPECT_AVAILABLE:
126 pytest.skip("pexpect not installed")
127
128 machine = request.config.getoption("--machine", default="qemux86-64")
129 timeout = int(request.config.getoption("--boot-timeout", default="120"))
130 no_kvm = request.config.getoption("--no-kvm", default=False)
131
132 kvm_opt = "" if no_kvm else "kvm"
133 cmd = (
134 f"bash -c 'cd {poky_dir} && "
135 f"source oe-init-build-env {build_dir} >/dev/null 2>&1 && "
136 f"runqemu {machine} kvm-image-minimal ext4 nographic slirp "
137 f"{kvm_opt} qemuparams=\"-m 2048\"'"
138 )
139
140 child = pexpect.spawn(cmd, encoding='utf-8', timeout=timeout)
141 child.logfile_read = open('/tmp/runqemu-libvirt-test.log', 'w')
142
143 try:
144 index = child.expect([r'login:', r'root@', pexpect.TIMEOUT, pexpect.EOF],
145 timeout=timeout)
146 if index == 0:
147 child.sendline('root')
148 child.expect([r'root@', r'#'], timeout=30)
149 elif index >= 2:
150 raise RuntimeError("Boot failed")
151
152 child.sendline('export TERM=dumb')
153 child.expect(r'root@[^:]+:[^#]+#', timeout=10)
154
155 yield child
156
157 except RuntimeError as e:
158 pytest.skip(f"Failed to boot: {e}")
159 finally:
160 child.sendline('poweroff')
161 try:
162 child.expect(pexpect.EOF, timeout=30)
163 except pexpect.TIMEOUT:
164 child.terminate(force=True)
165
166
167def run_cmd(child, cmd, timeout=60):
168 """Run a command and return (output, returncode)."""
169 marker = f"__MARKER_{time.monotonic_ns()}__"
170 child.sendline(f"{cmd}; echo {marker} $?")
171 child.expect(marker + r" (\d+)", timeout=timeout)
172 raw = child.before
173 raw = re.sub(r'\x1b\]3008;[^\x07\x1b]*(?:\x07|\x1b\\)', '', raw)
174 raw = re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', raw)
175 output = raw.strip()
176 rc = int(child.match.group(1))
177 child.expect(r'root@[^:]+:[^#]+#', timeout=10)
178 return output, rc
179
180
181# ============================================================================
182# Tier 1: Static assertions (no build required)
183# ============================================================================
184
185class TestLibvirtRecipeStatic:
186 """Static checks on the libvirt recipe file."""
187
188 def test_recipe_exists(self, libvirt_recipe):
189 assert libvirt_recipe.exists()
190
191 def test_has_meson_inherit(self, libvirt_recipe):
192 content = libvirt_recipe.read_text()
193 assert "inherit meson" in content
194
195 def test_has_systemd_support(self, libvirt_recipe):
196 content = libvirt_recipe.read_text()
197 assert "inherit" in content and "systemd" in content
198 assert "SYSTEMD_SERVICE" in content
199
200 def test_systemd_services_defined(self, libvirt_recipe):
201 content = libvirt_recipe.read_text()
202 assert "libvirtd.service" in content
203 assert "virtlockd.service" in content
204
205 def test_has_useradd(self, libvirt_recipe):
206 content = libvirt_recipe.read_text()
207 assert "inherit" in content and "useradd" in content
208 assert "qemu" in content
209 assert "libvirt" in content
210
211 def test_qemu_packageconfig(self, libvirt_recipe):
212 content = libvirt_recipe.read_text()
213 assert "PACKAGECONFIG[qemu]" in content
214 assert "driver_qemu" in content
215
216 def test_lxc_packageconfig(self, libvirt_recipe):
217 content = libvirt_recipe.read_text()
218 assert "PACKAGECONFIG[lxc]" in content
219 assert "driver_lxc" in content
220
221 def test_xen_packageconfig_gated(self, libvirt_recipe):
222 """libxl PACKAGECONFIG should be gated on xen DISTRO_FEATURE."""
223 content = libvirt_recipe.read_text()
224 assert "PACKAGECONFIG[libxl]" in content
225 for line in content.splitlines():
226 if "libxl" in line and "DISTRO_FEATURES" in line:
227 assert "xen" in line
228 break
229 else:
230 pytest.fail("libxl PACKAGECONFIG not gated on xen DISTRO_FEATURE")
231
232 def test_nftables_rdepends(self, libvirt_recipe):
233 """libvirt-libvirtd should RDEPEND on nftables when configured."""
234 content = libvirt_recipe.read_text()
235 assert "nftables" in content
236
237 def test_nftables_or_iptables_rdepends(self, libvirt_recipe):
238 """RDEPENDS should select nftables or iptables based on PACKAGECONFIG."""
239 content = libvirt_recipe.read_text()
240 assert "bb.utils.contains('PACKAGECONFIG', 'nftables'" in content, \
241 "RDEPENDS should conditionally select nftables or iptables"
242
243 def test_packages_split(self, libvirt_recipe):
244 """Recipe should split into libvirt, libvirt-libvirtd, libvirt-virsh."""
245 content = libvirt_recipe.read_text()
246 assert "${PN}-libvirtd" in content
247 assert "${PN}-virsh" in content
248
249 def test_cve_status_entries(self, libvirt_recipe):
250 content = libvirt_recipe.read_text()
251 assert "CVE_STATUS" in content
252 cve_count = content.count("CVE_STATUS[CVE-")
253 assert cve_count >= 5, f"Expected at least 5 CVE entries, found {cve_count}"
254
255
256class TestLibvirtHookSupport:
257 """Tests for hook_support.py (the file from the recent bug report)."""
258
259 def test_hook_script_exists(self, libvirt_files_dir):
260 path = libvirt_files_dir / "hook_support.py"
261 assert path.exists()
262
263 def test_hook_script_python3_shebang(self, libvirt_files_dir):
264 content = (libvirt_files_dir / "hook_support.py").read_text()
265 first_line = content.splitlines()[0]
266 assert "python3" in first_line or "python" in first_line
267
268 def test_hook_script_uses_text_mode(self, libvirt_files_dir):
269 """Popen should use text=True for python3 string compatibility."""
270 content = (libvirt_files_dir / "hook_support.py").read_text()
271 assert "text=True" in content, \
272 "Popen should use text=True for python3 string/bytes compatibility"
273
274 def test_hook_script_regex_raw_string(self, libvirt_files_dir):
275 """Regex should use raw string to avoid SyntaxWarning in python3.12+."""
276 content = (libvirt_files_dir / "hook_support.py").read_text()
277 for line in content.splitlines():
278 if "re.compile" in line and "\\w" in line:
279 assert "rf\"" in line or "r'" in line or 'r"' in line, \
280 f"Regex with \\w should use raw string: {line.strip()}"
281
282 def test_hook_script_syntax_valid(self, libvirt_files_dir):
283 """hook_support.py should parse without syntax errors."""
284 import py_compile
285 path = libvirt_files_dir / "hook_support.py"
286 try:
287 py_compile.compile(str(path), doraise=True)
288 except py_compile.PyCompileError as e:
289 pytest.fail(f"Syntax error in hook_support.py: {e}")
290
291 def test_hook_installs_for_all_domains(self, libvirt_recipe):
292 """hook_support.py should be installed for daemon, lxc, network, qemu."""
293 content = libvirt_recipe.read_text()
294 for hook in ["daemon", "lxc", "network", "qemu"]:
295 assert hook in content, f"Hook not installed for domain: {hook}"
296
297
298class TestLibvirtServiceFiles:
299 """Tests for systemd service and init script files."""
300
301 def test_initscript_exists(self, libvirt_files_dir):
302 assert (libvirt_files_dir / "libvirtd.sh").exists()
303
304 def test_initscript_has_lsb_header(self, libvirt_files_dir):
305 content = (libvirt_files_dir / "libvirtd.sh").read_text()
306 assert "BEGIN INIT INFO" in content
307
308 def test_initscript_start_stop(self, libvirt_files_dir):
309 content = (libvirt_files_dir / "libvirtd.sh").read_text()
310 assert "start)" in content
311 assert "stop)" in content
312 assert "restart)" in content
313
314 def test_dnsmasq_conf_exists(self, libvirt_files_dir):
315 assert (libvirt_files_dir / "dnsmasq.conf").exists()
316
317 def test_libvirtd_conf_exists(self, libvirt_files_dir):
318 assert (libvirt_files_dir / "libvirtd.conf").exists()
319
320 def test_gnutls_helper_exists(self, libvirt_files_dir):
321 assert (libvirt_files_dir / "gnutls-helper.py").exists()
322
323
324class TestLibvirtPatchFiles:
325 """Verify patches exist and reference valid upstream issues."""
326
327 def test_patches_exist(self, libvirt_files_dir):
328 patches = list(libvirt_files_dir.glob("*.patch"))
329 assert len(patches) >= 1, "Expected at least one patch"
330
331 def test_buildpath_patches_present(self, libvirt_files_dir):
332 """Patches for buildpaths should exist."""
333 patches = list(libvirt_files_dir.glob("*.patch"))
334 buildpath_patches = [p for p in patches if "build-path" in p.name
335 or "build_path" in p.name or "gendispatch" in p.name]
336 assert len(buildpath_patches) >= 1, \
337 "Expected at least one buildpath-related patch"
338
339
340class TestKvmImageRecipe:
341 """Static checks on kvm-image-minimal recipe."""
342
343 def test_recipe_exists(self, meta_virt_dir):
344 path = meta_virt_dir / "recipes-extended" / "images" / "kvm-image-minimal.bb"
345 assert path.exists()
346
347 def test_requires_kvm_distro_feature(self, meta_virt_dir):
348 content = (meta_virt_dir / "recipes-extended" / "images" / "kvm-image-minimal.bb").read_text()
349 assert "kvm" in content
350 assert "REQUIRED_DISTRO_FEATURES" in content
351
352 def test_includes_libvirt(self, meta_virt_dir):
353 content = (meta_virt_dir / "recipes-extended" / "images" / "kvm-image-minimal.bb").read_text()
354 assert "libvirt" in content
355 assert "libvirt-libvirtd" in content
356 assert "libvirt-virsh" in content
357
358 def test_includes_qemu(self, meta_virt_dir):
359 content = (meta_virt_dir / "recipes-extended" / "images" / "kvm-image-minimal.bb").read_text()
360 assert "qemu" in content
361
362 def test_includes_kvm_modules(self, meta_virt_dir):
363 content = (meta_virt_dir / "recipes-extended" / "images" / "kvm-image-minimal.bb").read_text()
364 assert "kernel-module-kvm" in content
365
366
367# ============================================================================
368# Tier 2: Build verification (requires bitbake)
369# ============================================================================
370
371@pytest.mark.slow
372class TestLibvirtBuild:
373 """Build tests for libvirt recipe."""
374
375 def test_libvirt_builds(self, build_dir):
376 result = _run_bitbake(build_dir, "libvirt")
377 assert result.returncode == 0, f"libvirt build failed: {result.stderr}"
378
379 def test_kvm_image_builds(self, build_dir, kvm_image):
380 """kvm-image-minimal builds successfully (via kvm_image fixture)."""
381 pass
382
383
384# ============================================================================
385# Tier 3: Boot tests (requires QEMU)
386# ============================================================================
387
388# Libvirt v12 defaults to modular daemons (virtqemud, virtnetworkd, etc.)
389# but kvm-image-minimal runs the monolithic libvirtd. virsh must be pointed
390# at the monolithic socket explicitly.
391_VIRSH = "virsh -c qemu+unix:///system?socket=/var/run/libvirt/libvirt-sock"
392
393
394@pytest.mark.slow
395@pytest.mark.boot
396class TestLibvirtRuntime:
397 """Boot tests verifying libvirt on a running kvm-image-minimal."""
398
399 def test_libvirtd_running(self, libvirt_session):
400 """libvirtd systemd service should be active."""
401 output, rc = run_cmd(libvirt_session, "systemctl is-active libvirtd")
402 assert "active" in output, f"libvirtd not active: {output}"
403
404 def test_virtlockd_running(self, libvirt_session):
405 """virtlockd should be active (started on demand via socket)."""
406 output, rc = run_cmd(libvirt_session,
407 "systemctl is-active virtlockd.socket")
408 assert "active" in output, f"virtlockd.socket not active: {output}"
409
410 def test_virsh_available(self, libvirt_session):
411 """virsh command should be available."""
412 output, rc = run_cmd(libvirt_session, "which virsh")
413 assert rc == 0, f"virsh not found: {output}"
414
415 def test_virsh_version(self, libvirt_session):
416 """virsh version should report libvirt version."""
417 output, rc = run_cmd(libvirt_session, "virsh --version")
418 assert rc == 0, f"virsh version failed: {output}"
419
420 def test_virsh_connect(self, libvirt_session):
421 """virsh should connect to local libvirtd."""
422 output, rc = run_cmd(libvirt_session, f"{_VIRSH} uri")
423 assert rc == 0, f"virsh uri failed: {output}"
424
425 def test_virsh_capabilities(self, libvirt_session):
426 """virsh capabilities should return valid XML."""
427 output, rc = run_cmd(libvirt_session, f"{_VIRSH} capabilities")
428 assert rc == 0, f"virsh capabilities failed: {output}"
429 assert "<capabilities>" in output or "capabilities" in output
430
431 def test_virsh_nodeinfo(self, libvirt_session):
432 """virsh nodeinfo should report system info."""
433 output, rc = run_cmd(libvirt_session, f"{_VIRSH} nodeinfo")
434 assert rc == 0, f"virsh nodeinfo failed: {output}"
435 assert "CPU model" in output or "cpu" in output.lower()
436
437 def test_virsh_list(self, libvirt_session):
438 """virsh list should work (even with no domains)."""
439 output, rc = run_cmd(libvirt_session, f"{_VIRSH} list --all")
440 assert rc == 0, f"virsh list failed: {output}"
441
442 def test_default_network(self, libvirt_session):
443 """Default network should be defined."""
444 output, rc = run_cmd(libvirt_session, f"{_VIRSH} net-list --all")
445 assert rc == 0, f"virsh net-list failed: {output}"
446
447 def test_hook_scripts_installed(self, libvirt_session):
448 """Hook scripts should be installed for all domains."""
449 output, rc = run_cmd(libvirt_session, "ls /etc/libvirt/hooks/")
450 assert rc == 0
451 for hook in ["daemon", "lxc", "network", "qemu"]:
452 assert hook in output, f"Hook script missing: {hook}"
453
454 def test_hook_scripts_executable(self, libvirt_session):
455 """Hook scripts should be executable."""
456 output, rc = run_cmd(libvirt_session,
457 "test -x /etc/libvirt/hooks/qemu && echo OK")
458 assert "OK" in output, "qemu hook not executable"
459
460 def test_qemu_user_exists(self, libvirt_session):
461 """qemu user should exist (created by useradd in recipe)."""
462 output, rc = run_cmd(libvirt_session, "grep ^qemu: /etc/passwd")
463 assert rc == 0, f"qemu user not found: {output}"
464
465 def test_libvirt_group_exists(self, libvirt_session):
466 """libvirt group should exist."""
467 output, rc = run_cmd(libvirt_session, "grep ^libvirt: /etc/group")
468 assert rc == 0, f"libvirt group not found: {output}"