Skip to content

Commit f44a732

Browse files
committed
tests: add xen-guest-agent integration tests for Linux VMs
Install the agent from the CI-published package repositories on a running Linux VM and verify its core Xenstore behaviour: version attributes, OS info, memory counters, feature-balloon flag, and VIF IP publication. RPM packages are served from GitLab Pages (which supports the repodata/ directory structure that DNF requires) and DEB packages are served from the GitLab Generic Package Registry using a numeric project ID to prevent APT from decoding the %2F in the project path. Signed-off-by: Julian Vetter <julian.vetter@vates.tech>
1 parent d1834af commit f44a732

2 files changed

Lines changed: 139 additions & 0 deletions

File tree

tests/guest_tools/unix/conftest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
_GITLAB_API = 'https://gitlab.com/api/v4'
6+
# Numeric ID avoids %2F in the URL, which APT decodes and breaks.
7+
_GITLAB_PROJECT_ID = 28547076
8+
9+
_PKG_REGISTRY = f'{_GITLAB_API}/projects/{_GITLAB_PROJECT_ID}/packages/generic'
10+
11+
_RPM_REPO_URL = 'https://xen-project.gitlab.io/xen-guest-agent/rpm-x86_64/'
12+
# DEB uses the numeric project ID so APT doesn't mangle the URL.
13+
_DEB_REPO_URL = f'{_PKG_REGISTRY}/deb-amd64/'
14+
15+
16+
@pytest.fixture(scope="module")
17+
def xen_guest_agent_urls() -> dict[str, str]:
18+
return {
19+
'rpm_repo': _RPM_REPO_URL,
20+
'deb_repo': _DEB_REPO_URL,
21+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
rpm_repo = xen_guest_agent_urls['rpm_repo']
54+
vm.ssh(f"echo -e '[xen-guest-agent]\\nbaseurl={rpm_repo}main/\\ngpgcheck=0'"
55+
f" > /etc/yum.repos.d/xen-guest-agent.repo")
56+
vm.ssh('dnf install -y xen-guest-agent')
57+
elif pkg_mgr == PackageManagerEnum.APT_GET:
58+
# DEB packages are published to a stable APT repo in the GitLab
59+
# Generic Package Registry after each push to main.
60+
deb_repo = xen_guest_agent_urls['deb_repo']
61+
vm.ssh(f"echo 'deb [trusted=yes] {deb_repo} main/' "
62+
f"> /etc/apt/sources.list.d/xen-guest-agent.list")
63+
vm.ssh('apt-get update')
64+
vm.ssh('apt-get install -y xen-guest-agent')
65+
66+
wait_for(
67+
lambda: vm.ssh_with_result('systemctl is-active xen-guest-agent').returncode == 0,
68+
"Wait for xen-guest-agent service to be active",
69+
)
70+
71+
def test_agent_running_after_reboot(self, running_vm: VM) -> None:
72+
running_vm.reboot(verify=True)
73+
running_vm.ssh('systemctl is-active xen-guest-agent')
74+
75+
def test_xenstore_version(self, running_vm: VM) -> None:
76+
host = running_vm.host
77+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
78+
host.ssh(f'xenstore-read {xs_prefix}/attr/PVAddons/MajorVersion')
79+
host.ssh(f'xenstore-read {xs_prefix}/attr/PVAddons/BuildVersion')
80+
81+
def test_xenstore_os_info(self, running_vm: VM) -> None:
82+
host = running_vm.host
83+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
84+
host.ssh(f'xenstore-read {xs_prefix}/data/os_distro')
85+
host.ssh(f'xenstore-read {xs_prefix}/data/os_uname')
86+
87+
def test_xenstore_memory(self, running_vm: VM) -> None:
88+
host = running_vm.host
89+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
90+
host.ssh(f'xenstore-read {xs_prefix}/data/meminfo_total')
91+
# meminfo_free is published on a 60s timer, wait for it to appear
92+
wait_for(
93+
lambda: host.ssh_with_result(f'xenstore-read {xs_prefix}/data/meminfo_free').returncode == 0,
94+
"Wait for meminfo_free in Xenstore",
95+
timeout_secs=90,
96+
)
97+
98+
def test_xenstore_feature_balloon(self, running_vm: VM) -> None:
99+
host = running_vm.host
100+
xs_prefix = f'/local/domain/{running_vm.param_get("dom-id")}'
101+
res = host.ssh_with_result(f'xenstore-read {xs_prefix}/control/feature-balloon')
102+
if res.returncode != 0:
103+
pytest.skip("control/feature-balloon not present — agent may lack write permission on this host")
104+
assert res.stdout.strip() == '1', \
105+
f"Expected control/feature-balloon to be '1', got {res.stdout.strip()!r}"
106+
107+
def test_xenstore_vif_ip(self, running_vm: VM) -> None:
108+
vm = running_vm
109+
host = vm.host
110+
xs_prefix = f'/local/domain/{vm.param_get("dom-id")}'
111+
if host.ssh_with_result(f'xenstore-exists {xs_prefix}/attr/vif').returncode != 0:
112+
pytest.skip("No VIF published in Xenstore — VM may not be using a Xen PV NIC")
113+
ipv4s = _vif_published_ips(host, xs_prefix, vif_id=0, proto='ipv4')
114+
ipv6s = _vif_published_ips(host, xs_prefix, vif_id=0, proto='ipv6')
115+
logging.info("Published IPv4: %s, IPv6: %s", ipv4s, ipv6s)
116+
assert ipv4s or ipv6s, "No IPs published in Xenstore under attr/vif/0"
117+
assert vm.ip in ipv4s + ipv6s, \
118+
f"VM IP {vm.ip!r} not found in Xenstore (ipv4: {ipv4s}, ipv6: {ipv6s})"

0 commit comments

Comments
 (0)