|
| 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