Skip to content

Commit 833a9d9

Browse files
committed
tests: add xen-guest-agent integration tests
Add a test suite that validates the xen-guest-agent daemon running inside a guest VM correctly publishes data to Xenstore. The conftest downloads RPM and DEB packages from the xen-guest-agent GitLab CI artifacts and SCPs them to the VM for installation. The test class (TestXenGuestAgent) then installs the agent on the VM via yum (RPM distros) or dpkg (APT distros), then verifies: - the systemd service is active after install and after reboot - Xenstore paths for version, OS info, memory and VIF IP are populated (meminfo_free is polled with a 90s timeout as it is only published on a 60s timer; the VIF/IP check is skipped if no Xen PV NIC is present) - Check that balloning feature return 1 Signed-off-by: Julian Vetter <julian.vetter@vates.tech>
1 parent c3ff2d0 commit 833a9d9

3 files changed

Lines changed: 160 additions & 0 deletions

File tree

data.py-dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,7 @@ IMAGE_EQUIVS: dict[str, str] = {
262262

263263
# This should be a working DNS server that's not used by any VM images.
264264
TEST_DNS_SERVER = "1.1.1.1"
265+
266+
GITLAB_API = 'https://gitlab.com/api/v4/projects/xen-project%2Fxen-guest-' \
267+
'agent/jobs/artifacts/main/download?search_recent_successful_' \
268+
'pipelines=true&job='

tests/guest_tools/unix/conftest.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
3+
import os
4+
import tempfile
5+
import zipfile
6+
7+
from data import GITLAB_API
8+
from lib.common import url_download
9+
10+
def _extract_from_zip(zip_path, suffix, dest_dir):
11+
"""
12+
Extract the main package matching suffix from zip_path into dest_dir.
13+
Excludes debug/dbgsym packages which may also be present.
14+
"""
15+
with zipfile.ZipFile(zip_path) as zf:
16+
matches = [
17+
n for n in zf.namelist()
18+
if n.endswith(suffix) and not any(
19+
kw in os.path.basename(n) for kw in ('debug', 'dbgsym')
20+
)
21+
]
22+
assert len(matches) == 1, \
23+
f"Expected exactly one non-debug *{suffix} in artifact zip, found: {matches}"
24+
zf.extract(matches[0], dest_dir)
25+
return os.path.join(dest_dir, matches[0])
26+
27+
28+
@pytest.fixture(scope="module")
29+
def xen_guest_agent_packages():
30+
"""
31+
Download the latest xen-guest-agent RPM and DEB from GitLab CI artifacts.
32+
Yields a dict with keys 'rpm' and 'deb' pointing to the local file paths.
33+
"""
34+
artifact_urls = {'rpm': f'{GITLAB_API}pkg-rpm-x86_64',
35+
'deb': f'{GITLAB_API}pkg-deb-amd64'}
36+
37+
with tempfile.TemporaryDirectory() as tmpdir:
38+
zip_path = os.path.join(tmpdir, 'artifacts.zip')
39+
40+
url_download(artifact_urls['rpm'], zip_path)
41+
rpm_path = _extract_from_zip(zip_path, '.rpm', tmpdir)
42+
43+
url_download(artifact_urls['deb'], zip_path)
44+
deb_path = _extract_from_zip(zip_path, '.deb', tmpdir)
45+
46+
yield {'rpm': rpm_path, 'deb': deb_path}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import pytest
2+
3+
import logging
4+
5+
from lib.common import PackageManagerEnum, wait_for
6+
7+
# Requirements:
8+
# From --hosts parameter:
9+
# - host(A1): first XCP-ng host >= 8.0
10+
# From --vm parameter:
11+
# - A Linux VM with systemd and a supported package manager (RPM or APT)
12+
13+
def _vif_published_ips(host, xs_prefix, vif_id, proto):
14+
"""Return all IPs published under attr/vif/{vif_id}/{proto}/* in Xenstore."""
15+
parent = f'{xs_prefix}/attr/vif/{vif_id}/{proto}'
16+
res = host.ssh_with_result(['xenstore-list', parent])
17+
if res.returncode != 0:
18+
return []
19+
return [
20+
host.ssh(['xenstore-read', f'{parent}/{slot}']).strip()
21+
for slot in res.stdout.split()
22+
]
23+
24+
25+
@pytest.mark.multi_vms
26+
@pytest.mark.usefixtures("unix_vm")
27+
class TestXenGuestAgent:
28+
@pytest.fixture(scope="class", autouse=True)
29+
def agent_install(self, running_vm, xen_guest_agent_packages):
30+
vm = running_vm
31+
32+
if vm.ssh_with_result(['which', 'systemctl']).returncode != 0:
33+
pytest.skip("systemd not available on this VM")
34+
35+
pkg_mgr = vm.detect_package_manager()
36+
if pkg_mgr not in (PackageManagerEnum.RPM, PackageManagerEnum.APT_GET):
37+
pytest.skip(f"Package manager '{pkg_mgr}' not supported in this test")
38+
39+
# Remove conflicting xe-guest-utilities if present
40+
logging.info("Removing xe-guest-utilities if present")
41+
if pkg_mgr == PackageManagerEnum.RPM:
42+
vm.ssh('rpm -qa | grep xe-guest-utilities | xargs --no-run-if-empty rpm -e')
43+
if pkg_mgr == PackageManagerEnum.APT_GET and \
44+
vm.ssh_with_result(['dpkg', '-l', 'xe-guest-utilities']).returncode == 0:
45+
vm.ssh(['apt-get', 'remove', '-y', 'xe-guest-utilities'])
46+
47+
# Copy package to VM and install
48+
if pkg_mgr == PackageManagerEnum.RPM:
49+
vm.scp(xen_guest_agent_packages['rpm'], '/root/xen-guest-agent.rpm')
50+
vm.ssh(['yum', 'install', '-y', '/root/xen-guest-agent.rpm'])
51+
if pkg_mgr == PackageManagerEnum.APT_GET:
52+
vm.scp(xen_guest_agent_packages['deb'], '/root/xen-guest-agent.deb')
53+
vm.ssh(['dpkg', '-i', '/root/xen-guest-agent.deb'])
54+
55+
wait_for(
56+
lambda: vm.ssh_with_result(['systemctl', 'is-active', 'xen-guest-agent']).returncode == 0,
57+
"Wait for xen-guest-agent service to be active",
58+
)
59+
60+
def test_agent_running(self, running_vm):
61+
running_vm.ssh(['systemctl', 'is-active', 'xen-guest-agent'])
62+
63+
def test_agent_running_after_reboot(self, running_vm):
64+
running_vm.reboot(verify=True)
65+
running_vm.ssh(['systemctl', 'is-active', 'xen-guest-agent'])
66+
67+
def test_xenstore_version(self, running_vm):
68+
host = running_vm.host
69+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
70+
host.ssh(['xenstore-read', f'{xs_prefix}/attr/PVAddons/MajorVersion'])
71+
host.ssh(['xenstore-read', f'{xs_prefix}/attr/PVAddons/BuildVersion'])
72+
73+
def test_xenstore_os_info(self, running_vm):
74+
host = running_vm.host
75+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
76+
host.ssh(['xenstore-read', f'{xs_prefix}/data/os_distro'])
77+
host.ssh(['xenstore-read', f'{xs_prefix}/data/os_uname'])
78+
79+
def test_xenstore_memory(self, running_vm):
80+
host = running_vm.host
81+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
82+
host.ssh(['xenstore-read', f'{xs_prefix}/data/meminfo_total'])
83+
# meminfo_free is published on a 60s timer, wait for it to appear
84+
wait_for(
85+
lambda: host.ssh_with_result(['xenstore-read', f'{xs_prefix}/data/meminfo_free']).returncode == 0,
86+
"Wait for meminfo_free in Xenstore",
87+
timeout_secs=90,
88+
)
89+
90+
def test_xenstore_feature_balloon(self, running_vm):
91+
host = running_vm.host
92+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
93+
res = host.ssh_with_result(['xenstore-read', f'{xs_prefix}/control/feature-balloon'])
94+
if res.returncode != 0:
95+
pytest.skip("control/feature-balloon not present — agent may lack write permission on this host")
96+
assert res.stdout.strip() == '1', \
97+
f"Expected control/feature-balloon to be '1', got {res.stdout.strip()!r}"
98+
99+
def test_xenstore_vif_ip(self, running_vm):
100+
vm = running_vm
101+
host = vm.host
102+
xs_prefix = f'/local/domain/{vm.param_get("dom-id")}'
103+
if host.ssh_with_result(['xenstore-exists', f'{xs_prefix}/attr/vif']).returncode != 0:
104+
pytest.skip("No VIF published in Xenstore — VM may not be using a Xen PV NIC")
105+
ipv4s = _vif_published_ips(host, xs_prefix, vif_id=0, proto='ipv4')
106+
ipv6s = _vif_published_ips(host, xs_prefix, vif_id=0, proto='ipv6')
107+
logging.info("Published IPv4: %s, IPv6: %s", ipv4s, ipv6s)
108+
assert ipv4s or ipv6s, "No IPs published in Xenstore under attr/vif/0"
109+
assert vm.ip in ipv4s + ipv6s, \
110+
f"VM IP {vm.ip!r} not found in Xenstore (ipv4: {ipv4s}, ipv6: {ipv6s})"

0 commit comments

Comments
 (0)