diff --git a/sssd_test_framework/utils/tools.py b/sssd_test_framework/utils/tools.py index 77303143..340d3377 100644 --- a/sssd_test_framework/utils/tools.py +++ b/sssd_test_framework/utils/tools.py @@ -10,6 +10,7 @@ from pytest_mh.utils.fs import LinuxFileSystem __all__ = [ + "AHostSv4Entry", "GetentUtils", "GroupEntry", "LinuxToolsUtils", @@ -601,6 +602,32 @@ def FromOutput(cls, stdout: str) -> HostsEntry: return cls.FromList(result) +class AHostSv4Entry(object): + """ + Result of ``getent ahostsv4`` — first IPv4 from the first data line. + + Same style as :class:`HostsEntry` (use the ``.ip`` field). + """ + + def __init__(self, ip: str | None) -> None: + self.ip: str | None = ip + """IPv4 dotted-quad (first column of ``getent ahostsv4`` output).""" + + def __str__(self) -> str: + return f"({self.ip})" + + def __repr__(self) -> str: + return str(self) + + @classmethod + def FromOutput(cls, stdout: str) -> AHostSv4Entry | None: + for line in stdout.splitlines(): + parts = line.split() + if parts: + return cls(ip=parts[0]) + return None + + class NetworksEntry(object): """ Result of ``getent networks`` @@ -1058,6 +1085,31 @@ def hosts(self, name: str, *, service: str | None = None) -> HostsEntry: """ return self.__exec(HostsEntry, "hosts", name, service) + def ahostsv4(self, name: str, *, service: str | None = None) -> AHostSv4Entry | None: + """ + Call ``getent ahostsv4`` and return the first IPv4 address from output. + + For DNS lookups using ``dig`` (A, SRV, etc.), use + :meth:`NetworkUtils.dig ` + on ``client.net`` instead of ad hoc shell commands in tests. + + :param name: Host name or address to resolve. + :type name: str + :param service: Optional NSS service name (``getent -s``). + :type service: str | None, optional + :return: Parsed entry or ``None`` if lookup failed or produced no address. + :rtype: AHostSv4Entry | None + """ + args: list[str] = [] + if service is not None: + args = ["-s", service] + + command = self.host.conn.exec(["getent", *args, "ahostsv4", str(name)], raise_on_error=False) + if command.rc != 0: + return None + + return AHostSv4Entry.FromOutput(command.stdout) + def networks(self, name: str, *, service: str | None = None) -> NetworksEntry: """ Call ``getent networks $name`` diff --git a/tests/test_tools.py b/tests/test_tools.py new file mode 100644 index 00000000..8bbc2d8d --- /dev/null +++ b/tests/test_tools.py @@ -0,0 +1,25 @@ +"""Unit tests for :mod:`sssd_test_framework.utils.tools`.""" + +from __future__ import annotations + +import pytest + +from sssd_test_framework.utils.tools import AHostSv4Entry + + +@pytest.mark.parametrize( + "stdout, expected_ip", + [ + ("192.168.1.1 STREAM hostname.example\n", "192.168.1.1"), + ("10.0.0.5 STREAM foo\n10.0.0.6 STREAM foo\n", "10.0.0.5"), + ("", None), + ("\n\n", None), + ], +) +def test_ahostsv4_entry_from_output(stdout: str, expected_ip: str | None) -> None: + entry = AHostSv4Entry.FromOutput(stdout) + if expected_ip is None: + assert entry is None + else: + assert entry is not None + assert entry.ip == expected_ip