1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
|
# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
#
# SPDX-License-Identifier: MIT
"""
Xen runtime boot tests - boot xen-image-minimal and verify hypervisor.
These tests boot an actual Xen Dom0 image via runqemu, verify the
hypervisor is functional, check guest bundling, and exercise vxn/containerd.
Build prerequisites (minimum for Dom0 boot tests):
DISTRO_FEATURES:append = " xen systemd"
MACHINE = "qemux86-64" # or qemuarm64
bitbake xen-image-minimal
For guest bundling tests:
IMAGE_INSTALL:append:pn-xen-image-minimal = " alpine-xen-guest-bundle"
For vxn/containerd tests:
DISTRO_FEATURES:append = " virtualization vcontainer vxn"
IMAGE_INSTALL:append:pn-xen-image-minimal = " vxn"
BBMULTICONFIG = "vruntime-aarch64 vruntime-x86-64"
Run with:
pytest tests/test_xen_runtime.py -v --machine qemux86-64
Skip network-dependent tests:
pytest tests/test_xen_runtime.py -v -m "boot and not network"
Custom paths and longer timeout:
pytest tests/test_xen_runtime.py -v \
--poky-dir /opt/bruce/poky \
--build-dir /opt/bruce/poky/build \
--boot-timeout 180
"""
import re
import time
import pytest
from pathlib import Path
# Optional import for boot tests
try:
import pexpect
PEXPECT_AVAILABLE = True
except ImportError:
PEXPECT_AVAILABLE = False
# Note: Command line options (--poky-dir, --build-dir, --machine, --boot-timeout, --no-kvm)
# are defined in conftest.py to avoid conflicts with other test files.
class XenRunner:
"""
Manages a runqemu session for Xen boot testing.
Uses pexpect to interact with the serial console of a booted
xen-image-minimal via runqemu.
"""
def __init__(self, poky_dir, build_dir, machine, use_kvm=True, timeout=120):
self.poky_dir = Path(poky_dir)
self.build_dir = Path(build_dir)
self.machine = machine
self.use_kvm = use_kvm
self.timeout = timeout
self.child = None
self.booted = False
def start(self):
"""Start runqemu and wait for login prompt."""
if not PEXPECT_AVAILABLE:
raise RuntimeError("pexpect not installed. Run: pip install pexpect")
kvm_opt = "kvm" if self.use_kvm else ""
cmd = (
f"bash -c 'cd {self.poky_dir} && "
f"source oe-init-build-env {self.build_dir} >/dev/null 2>&1 && "
f"runqemu {self.machine} xen-image-minimal wic nographic slirp {kvm_opt} "
f"qemuparams=\"-m 4096\"'"
)
print(f"Starting runqemu (Xen): {cmd}")
self.child = pexpect.spawn(cmd, encoding='utf-8', timeout=self.timeout)
# Log output for debugging
self.child.logfile_read = open('/tmp/runqemu-xen-test.log', 'w')
# Wait for login prompt
try:
index = self.child.expect([
r'login:',
r'root@', # Already logged in
pexpect.TIMEOUT,
pexpect.EOF,
], timeout=self.timeout)
if index == 0:
self.child.sendline('root')
self.child.expect([r'root@', r'#', r'\$'], timeout=30)
self.booted = True
elif index == 1:
self.booted = True
if self.booted:
# Disable terminal UI (progress bars, cursor movement) from
# tools like ctr, vxn, vctr that use fancy terminal output
self.child.sendline('export TERM=dumb')
self.child.expect(r'root@[^:]+:[^#]+#', timeout=10)
if index == 2:
raise RuntimeError(f"Timeout waiting for login (>{self.timeout}s)")
elif index == 3:
raise RuntimeError("runqemu terminated unexpectedly")
except Exception as e:
self.stop()
raise RuntimeError(f"Failed to boot Xen image: {e}")
return self
@staticmethod
def _strip_escape_sequences(text):
"""Strip ANSI and OSC escape sequences from terminal output."""
# OSC sequences: ESC ] ... ESC \ or ESC ] ... BEL
text = re.sub(r'\x1b\][^\x1b\x07]*(?:\x1b\\|\x07)', '', text)
# CSI sequences: ESC [ ... final_byte
text = re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', text)
# Any remaining bare ESC sequences
text = re.sub(r'\x1b[^[\]].?', '', text)
return text
def run_command(self, cmd, timeout=60):
"""Run a command and return the output."""
if not self.booted:
raise RuntimeError("System not booted")
# Wait for prompt to be ready
time.sleep(0.3)
self.child.sendline(cmd)
try:
self.child.expect(r'root@[^:]+:[^#]+#', timeout=timeout)
raw_output = self.child.before
# Strip terminal escape sequences (OSC 3008 shell integration, etc.)
raw_output = self._strip_escape_sequences(raw_output)
# Parse: split by newlines, skip command echo
lines = raw_output.replace('\r', '').split('\n')
output_lines = []
for i, line in enumerate(lines):
stripped = line.strip()
if not stripped:
continue
# First non-empty line is usually the command echo
if i == 0 or (output_lines == [] and cmd[:10] in line):
continue
output_lines.append(stripped)
return '\n'.join(output_lines)
except pexpect.TIMEOUT:
print(f"[TIMEOUT] Command '{cmd}' timed out after {timeout}s")
return ""
def stop(self):
"""Shutdown the QEMU instance."""
if self.child:
try:
if self.booted:
self.child.sendline('poweroff')
time.sleep(2)
if self.child.isalive():
self.child.terminate(force=True)
except Exception:
pass
finally:
if self.child.logfile_read:
self.child.logfile_read.close()
self.child = None
self.booted = False
# ============================================================================
# Fixtures
# ============================================================================
@pytest.fixture(scope="module")
def poky_dir(request):
"""Path to poky directory."""
path = Path(request.config.getoption("--poky-dir"))
if not path.exists():
pytest.skip(f"Poky directory not found: {path}")
return path
@pytest.fixture(scope="module")
def build_dir(request, poky_dir):
"""Path to build directory."""
bd = request.config.getoption("--build-dir")
if bd:
path = Path(bd)
else:
path = poky_dir / "build"
if not path.exists():
pytest.skip(f"Build directory not found: {path}")
return path
@pytest.fixture(scope="module")
def machine(request):
"""Target machine."""
return request.config.getoption("--machine")
@pytest.fixture(scope="module")
def xen_session(request, poky_dir, build_dir, machine):
"""
Module-scoped fixture that boots xen-image-minimal once for all tests.
Skips if pexpect is not available, image is not found, or boot fails.
"""
if not PEXPECT_AVAILABLE:
pytest.skip("pexpect not installed. Run: pip install pexpect")
# Check that the .wic image exists
deploy_dir = build_dir / "tmp" / "deploy" / "images" / machine
wic_files = list(deploy_dir.glob("xen-image-minimal-*.rootfs.wic"))
if not wic_files:
pytest.skip(f"xen-image-minimal .wic image not found in {deploy_dir}")
timeout = request.config.getoption("--boot-timeout")
use_kvm = not request.config.getoption("--no-kvm")
runner = XenRunner(poky_dir, build_dir, machine,
use_kvm=use_kvm, timeout=timeout)
try:
runner.start()
yield runner
except RuntimeError as e:
pytest.skip(f"Failed to boot Xen image: {e}")
finally:
runner.stop()
# ============================================================================
# TestXenDom0Boot — Core hypervisor verification
# ============================================================================
@pytest.mark.boot
class TestXenDom0Boot:
"""Core Xen hypervisor verification after booting xen-image-minimal."""
def test_dom0_reaches_prompt(self, xen_session):
"""Boot succeeds and reaches a shell prompt."""
assert xen_session.booted, "System failed to boot"
output = xen_session.run_command('uname -a')
assert 'Linux' in output
def test_xen_hypervisor_running(self, xen_session):
"""xl list shows Domain-0, proving Xen hypervisor is running."""
output = xen_session.run_command('xl list')
assert 'Domain-0' in output, \
f"Domain-0 not found in xl list output:\n{output}"
def test_dom0_memory_reserved(self, xen_session):
"""Domain-0 memory is capped (not consuming all RAM)."""
output = xen_session.run_command('xl list')
# Parse xl list output for Domain-0 line
# Format: Name ID Mem VCPUs State Time(s)
for line in output.splitlines():
if 'Domain-0' in line:
parts = line.split()
# Mem is the 3rd column (index 2)
if len(parts) >= 3:
try:
mem_mb = int(parts[2])
assert mem_mb <= 512, \
f"Domain-0 memory {mem_mb}MB exceeds 512MB cap"
except ValueError:
pass # Non-numeric column, skip
break
def test_xen_dmesg(self, xen_session):
"""Kernel dmesg contains Xen initialization messages."""
output = xen_session.run_command('dmesg | grep -i xen | head -10')
assert output, "No Xen messages found in dmesg"
# Should see Xen-related init messages
xen_found = any(
kw in output.lower()
for kw in ['xen', 'hypervisor', 'xenbus']
)
assert xen_found, \
f"No Xen keywords in dmesg output:\n{output}"
# ============================================================================
# TestXenGuestBundleRuntime — Guest autostart verification
# ============================================================================
@pytest.mark.boot
class TestXenGuestBundleRuntime:
"""Verify bundled Xen guests auto-start in Dom0."""
def test_bundled_guests_visible(self, xen_session):
"""xl list shows more than just Domain-0 (bundled guests running)."""
output = xen_session.run_command('xl list')
lines = [l for l in output.splitlines()
if l.strip() and not l.startswith('Name')]
if len(lines) <= 1:
pytest.skip("No bundled guests detected (only Domain-0 in xl list)")
# At least one guest beyond Domain-0
guest_count = len(lines) - 1 # subtract Domain-0
assert guest_count >= 1, \
f"Expected bundled guests, only found Domain-0:\n{output}"
def test_xendomains_service(self, xen_session):
"""xendomains systemd service is active (manages guest autostart)."""
output = xen_session.run_command(
'systemctl is-active xendomains 2>/dev/null || echo INACTIVE')
if 'INACTIVE' in output or 'inactive' in output:
pytest.skip("xendomains service not installed or inactive")
assert 'active' in output.lower(), \
f"xendomains not active: {output}"
# Minimum free memory (MB) needed to create a new Xen domain
_XEN_GUEST_MIN_FREE_MB = 256
def _check_xen_free_memory(xen_session, min_mb=_XEN_GUEST_MIN_FREE_MB):
"""Check Xen free memory, skip test if insufficient for a new domain."""
output = xen_session.run_command(
'xl info 2>&1 | grep free_memory')
# Format: "free_memory : 240"
match = re.search(r'free_memory\s*:\s*(\d+)', output)
if match:
free_mb = int(match.group(1))
if free_mb < min_mb:
xl_list = xen_session.run_command('xl list 2>&1')
pytest.skip(
f"Insufficient Xen free memory for new domain "
f"({free_mb} MB free, need {min_mb} MB)\n"
f"xl list:\n{xl_list}")
# ============================================================================
# TestXenVxnStandalone — vxn on Dom0
# ============================================================================
@pytest.mark.boot
@pytest.mark.network
class TestXenVxnStandalone:
"""Test vxn (Docker CLI for Xen) on Dom0. Requires network access."""
def test_vxn_available(self, xen_session):
"""vxn binary is installed in Dom0."""
output = xen_session.run_command('which vxn 2>/dev/null || echo NOT_FOUND')
if 'NOT_FOUND' in output:
pytest.skip("vxn not installed in image")
assert '/vxn' in output
def test_vxn_run_hello(self, xen_session):
"""vxn run --rm alpine echo hello produces 'hello'."""
check = xen_session.run_command('which vxn 2>/dev/null || echo NOT_FOUND')
if 'NOT_FOUND' in check:
pytest.skip("vxn not installed in image")
_check_xen_free_memory(xen_session)
output = xen_session.run_command(
'vxn run --rm alpine echo hello 2>&1', timeout=120)
assert 'hello' in output, \
f"Expected 'hello' in vxn output:\n{output}"
# ============================================================================
# TestXenContainerd — containerd + vctr on Dom0
# ============================================================================
@pytest.mark.boot
@pytest.mark.network
class TestXenContainerd:
"""Test containerd and vctr on Xen Dom0. Requires network access."""
def test_containerd_running(self, xen_session):
"""containerd systemd service is active."""
output = xen_session.run_command(
'systemctl is-active containerd 2>/dev/null || echo INACTIVE')
if 'INACTIVE' in output or 'inactive' in output:
pytest.skip("containerd not installed or inactive")
assert 'active' in output.lower(), \
f"containerd not active: {output}"
def test_ctr_pull_and_vctr_run(self, xen_session):
"""Pull alpine via ctr and run hello-world via vctr."""
svc = xen_session.run_command(
'systemctl is-active containerd 2>/dev/null || echo INACTIVE')
if 'INACTIVE' in svc or 'inactive' in svc:
pytest.skip("containerd not installed or inactive")
check = xen_session.run_command(
'which vctr 2>/dev/null || echo NOT_FOUND')
if 'NOT_FOUND' in check:
pytest.skip("vctr not installed in image")
_check_xen_free_memory(xen_session)
# Pull alpine image
xen_session.run_command(
'ctr image pull docker.io/library/alpine:latest 2>&1', timeout=120)
# Run hello via vctr
output = xen_session.run_command(
'vctr run --rm docker.io/library/alpine:latest echo hello 2>&1',
timeout=120)
assert 'hello' in output, \
f"Expected 'hello' in vctr output:\n{output}"
|