Skip to content

Commit 75b8016

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 d1834af commit 75b8016

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

tests/guest_tools/unix/conftest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
_GITLAB_API = 'https://gitlab.com/api/v4'
6+
_GITLAB_PROJECT_PATH = 'xen-project%2Fxen-guest-agent'
7+
# Numeric ID avoids %2F in the URL, which APT decodes and breaks.
8+
_GITLAB_PROJECT_ID = 28547076
9+
10+
# RPM: no hosted yum repo exists yet, so we fall back to CI artifact download.
11+
_RPM_ARTIFACT_URL = (
12+
f'{_GITLAB_API}/projects/{_GITLAB_PROJECT_PATH}'
13+
'/jobs/artifacts/main/download'
14+
'?search_recent_successful_pipelines=true&job=pkg-rpm-x86_64'
15+
)
16+
17+
# DEB: packages are deployed to the GitLab Generic Package Registry after each
18+
# push to main, forming a proper APT repo with stable URLs.
19+
# Uses the numeric project ID so APT does not mangle the URL.
20+
_DEB_REPO_URL = f'{_GITLAB_API}/projects/{_GITLAB_PROJECT_ID}/packages/generic/deb-amd64/'
21+
22+
23+
@pytest.fixture(scope="module")
24+
def xen_guest_agent_urls() -> dict[str, str]:
25+
return {
26+
'rpm_artifact': _RPM_ARTIFACT_URL,
27+
'deb_repo': _DEB_REPO_URL,
28+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
import logging
6+
7+
from lib.common import PackageManagerEnum, wait_for
8+
from lib.host import Host
9+
from lib.vm import VM
10+
11+
# Requirements:
12+
# From --hosts parameter:
13+
# - host(A1): first XCP-ng host >= 8.0
14+
# From --vm parameter:
15+
# - A Linux VM with systemd and a supported package manager (RPM or APT)
16+
17+
18+
def _vif_published_ips(host: Host, xs_prefix: str, vif_id: int, proto: str) -> list[str]:
19+
"""Return all IPs published under attr/vif/{vif_id}/{proto}/* in Xenstore."""
20+
parent = f'{xs_prefix}/attr/vif/{vif_id}/{proto}'
21+
res = host.ssh_with_result(f'xenstore-list {parent}')
22+
if res.returncode != 0:
23+
return []
24+
return [
25+
host.ssh(f'xenstore-read {parent}/{slot}').strip()
26+
for slot in res.stdout.split()
27+
]
28+
29+
30+
@pytest.mark.multi_vms
31+
@pytest.mark.usefixtures("unix_vm")
32+
class TestXenGuestAgent:
33+
@pytest.fixture(scope="class", autouse=True)
34+
def agent_install(self, running_vm: VM, xen_guest_agent_urls: dict[str, str]) -> None:
35+
vm = running_vm
36+
37+
if vm.ssh_with_result('which systemctl').returncode != 0:
38+
pytest.skip("systemd not available on this VM")
39+
40+
pkg_mgr = vm.detect_package_manager()
41+
if pkg_mgr not in (PackageManagerEnum.RPM, PackageManagerEnum.APT_GET):
42+
pytest.skip(f"Package manager '{pkg_mgr}' not supported in this test")
43+
44+
# Remove conflicting xe-guest-utilities if present
45+
logging.info("Removing xe-guest-utilities if present")
46+
if pkg_mgr == PackageManagerEnum.RPM:
47+
vm.ssh('rpm -qa | grep xe-guest-utilities | xargs --no-run-if-empty rpm -e')
48+
elif pkg_mgr == PackageManagerEnum.APT_GET and \
49+
vm.ssh_with_result('dpkg -l xe-guest-utilities').returncode == 0:
50+
vm.ssh('apt-get remove -y xe-guest-utilities')
51+
52+
if pkg_mgr == PackageManagerEnum.RPM:
53+
# No hosted yum repo exists yet; download the artifact zip via Python's
54+
# standard library to avoid requiring curl/wget/unzip in the guest.
55+
url = xen_guest_agent_urls['rpm_artifact']
56+
py_script = (
57+
f"import urllib.request, zipfile, io; "
58+
f"z = zipfile.ZipFile(io.BytesIO(urllib.request.urlopen('{url}').read())); "
59+
f"pkg = next(n for n in z.namelist() "
60+
f"if n.endswith('.rpm') and 'debug' not in n and 'dbgsym' not in n); "
61+
f"z.extract(pkg, '/tmp/xga/')"
62+
)
63+
vm.ssh(f'python3 -c "{py_script}"')
64+
vm.ssh("yum install -y $(find /tmp/xga -name '*.rpm')")
65+
elif pkg_mgr == PackageManagerEnum.APT_GET:
66+
# DEB packages are published to a stable APT repo in the GitLab
67+
# Generic Package Registry after each push to main.
68+
deb_repo = xen_guest_agent_urls['deb_repo']
69+
vm.ssh(f"echo 'deb [trusted=yes] {deb_repo} main/' "
70+
f"> /etc/apt/sources.list.d/xen-guest-agent.list")
71+
vm.ssh('apt-get update')
72+
vm.ssh('apt-get install -y xen-guest-agent')
73+
74+
wait_for(
75+
lambda: vm.ssh_with_result('systemctl is-active xen-guest-agent').returncode == 0,
76+
"Wait for xen-guest-agent service to be active",
77+
)
78+
79+
def test_agent_running_after_reboot(self, running_vm: VM) -> None:
80+
running_vm.reboot(verify=True)
81+
running_vm.ssh('systemctl is-active xen-guest-agent')
82+
83+
def test_xenstore_version(self, running_vm: VM) -> None:
84+
host = running_vm.host
85+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
86+
host.ssh(f'xenstore-read {xs_prefix}/attr/PVAddons/MajorVersion')
87+
host.ssh(f'xenstore-read {xs_prefix}/attr/PVAddons/BuildVersion')
88+
89+
def test_xenstore_os_info(self, running_vm: VM) -> None:
90+
host = running_vm.host
91+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
92+
host.ssh(f'xenstore-read {xs_prefix}/data/os_distro')
93+
host.ssh(f'xenstore-read {xs_prefix}/data/os_uname')
94+
95+
def test_xenstore_memory(self, running_vm: VM) -> None:
96+
host = running_vm.host
97+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
98+
host.ssh(f'xenstore-read {xs_prefix}/data/meminfo_total')
99+
# meminfo_free is published on a 60s timer, wait for it to appear
100+
wait_for(
101+
lambda: host.ssh_with_result(f'xenstore-read {xs_prefix}/data/meminfo_free').returncode == 0,
102+
"Wait for meminfo_free in Xenstore",
103+
timeout_secs=90,
104+
)
105+
106+
def test_xenstore_feature_balloon(self, running_vm: VM) -> None:
107+
host = running_vm.host
108+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
109+
res = host.ssh_with_result(f'xenstore-read {xs_prefix}/control/feature-balloon')
110+
if res.returncode != 0:
111+
pytest.skip("control/feature-balloon not present — agent may lack write permission on this host")
112+
assert res.stdout.strip() == '1', \
113+
f"Expected control/feature-balloon to be '1', got {res.stdout.strip()!r}"
114+
115+
def test_xenstore_vif_ip(self, running_vm: VM) -> None:
116+
vm = running_vm
117+
host = vm.host
118+
xs_prefix = f'/local/domain/{vm.param_get("dom-id")}'
119+
if host.ssh_with_result(f'xenstore-exists {xs_prefix}/attr/vif').returncode != 0:
120+
pytest.skip("No VIF published in Xenstore — VM may not be using a Xen PV NIC")
121+
ipv4s = _vif_published_ips(host, xs_prefix, vif_id=0, proto='ipv4')
122+
ipv6s = _vif_published_ips(host, xs_prefix, vif_id=0, proto='ipv6')
123+
logging.info("Published IPv4: %s, IPv6: %s", ipv4s, ipv6s)
124+
assert ipv4s or ipv6s, "No IPs published in Xenstore under attr/vif/0"
125+
assert vm.ip in ipv4s + ipv6s, \
126+
f"VM IP {vm.ip!r} not found in Xenstore (ipv4: {ipv4s}, ipv6: {ipv6s})"

0 commit comments

Comments
 (0)