diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-06-05 20:51:01 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-06-12 02:58:55 +0000 |
| commit | 7d6e51b7706e305fa393bbc741c869c3d56f5abe (patch) | |
| tree | aa1b7aec13f7ce6a74062a0f6cd2838d18293cfb /tests | |
| parent | 1feb6132d9b56e92903cca7f0f0d50cbee1c4e02 (diff) | |
| download | meta-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.py | 468 |
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 | """ | ||
| 5 | Libvirt recipe and runtime tests. | ||
| 6 | |||
| 7 | Tier 1: Static assertions on recipe files (no build required) | ||
| 8 | Tier 2: Build verification (requires bitbake) | ||
| 9 | Tier 3: Boot tests on kvm-image-minimal (requires QEMU with KVM) | ||
| 10 | |||
| 11 | The tests automatically build kvm-image-minimal with the kvm | ||
| 12 | DISTRO_FEATURE before booting. No local.conf changes needed. | ||
| 13 | |||
| 14 | Run: | ||
| 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 | |||
| 21 | Options: | ||
| 22 | --boot-timeout QEMU boot timeout (default: 120s) | ||
| 23 | --no-kvm Disable KVM acceleration | ||
| 24 | """ | ||
| 25 | |||
| 26 | import os | ||
| 27 | import re | ||
| 28 | import subprocess | ||
| 29 | import tempfile | ||
| 30 | import time | ||
| 31 | import pytest | ||
| 32 | from pathlib import Path | ||
| 33 | |||
| 34 | try: | ||
| 35 | import pexpect | ||
| 36 | PEXPECT_AVAILABLE = True | ||
| 37 | except ImportError: | ||
| 38 | PEXPECT_AVAILABLE = False | ||
| 39 | |||
| 40 | |||
| 41 | def _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") | ||
| 69 | def 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") | ||
| 77 | def 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") | ||
| 86 | def 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") | ||
| 94 | def 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") | ||
| 102 | def 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") | ||
| 110 | def 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") | ||
| 123 | def 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 | |||
| 167 | def 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 | |||
| 185 | class 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 | |||
| 256 | class 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 | |||
| 298 | class 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 | |||
| 324 | class 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 | |||
| 340 | class 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 | ||
| 372 | class 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 | ||
| 396 | class 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}" | ||
