Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 42 additions & 16 deletions sssd_test_framework/hosts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import contextlib
import csv
import re
from typing import Any, Generator

import ldap
Expand All @@ -14,7 +13,7 @@
from pytest_mh.utils.services import SystemdServices

from ..config import SSSDMultihostDomain
from ..misc import retry
from ..misc import parse_package_version, retry

__all__ = [
"BaseHost",
Expand Down Expand Up @@ -207,6 +206,7 @@ def __init__(self, *args, **kwargs) -> None:
self.svc: SystemdServices = SystemdServices(self)
self._os_release: dict = {}
self._distro_name: str = "unknown"
self._distro_id: str = "unknown"
self._distro_major: int = 0
self._distro_minor: int = 0

Expand All @@ -219,6 +219,8 @@ def _distro_information(self):
self._os_release = dict(csv.reader([x for x in os_release.splitlines() if x], delimiter="="))
if "NAME" in self._os_release:
self._distro_name = self._os_release["NAME"]
if "ID" in self._os_release:
self._distro_id = self._os_release["ID"]
if "VERSION_ID" not in self._os_release:
return
if "." in self._os_release["VERSION_ID"]:
Expand All @@ -240,6 +242,18 @@ def distro_name(self) -> str:
self._distro_information()
return self._distro_name

@property
def distro_id(self) -> str:
"""
Host distribution id from ``/etc/os-release`` ``ID`` (e.g. ``rhel``, ``centos``, ``fedora``).

:return: Distribution id or ``"unknown"``
:rtype: str
"""
if not self._os_release:
self._distro_information()
return self._distro_id

@property
def distro_major(self) -> int:
"""
Expand Down Expand Up @@ -298,23 +312,12 @@ def get_package_version(self, package: str = "sssd", raise_on_error: bool = True
raise OSError(f"Package {package} not found!")
return vers

v_match = re.match(
r"([0-9]+)(?:\.)?([0-9]+)?(?:\.)?([0-9]+)?(?:~)?([a-z0-9]+)?[\.-]?([0-9]+)?(?:\.)?(.*)?",
ver,
re.IGNORECASE,
)

if v_match is None:
try:
return parse_package_version(ver.strip())
except ValueError:
if raise_on_error:
raise OSError(f"Package {package} version could not be parsed!")
return vers
vers["major"] = int(v_match.group(1))
vers["minor"] = int(v_match.group(2)) if v_match.group(2) else 0
vers["patch"] = int(v_match.group(3)) if v_match.group(3) else 0
vers["prerelease"] = v_match.group(4) if v_match.group(4) else ""
vers["update"] = int(v_match.group(5)) if v_match.group(5) else 0
vers["release"] = v_match.group(6) if v_match.group(6) else ""
return vers

def compare_package_version(self, other_version: dict, package: str = "sssd") -> int:
"""
Expand Down Expand Up @@ -347,6 +350,29 @@ def version_tuple(ver):
else:
return 0

def package_version_at_least(
self,
version: str | dict,
package: str = "sssd",
*,
distro_ids: tuple[str, ...] | list[str] | None = None,
) -> bool:
"""
Return True if the installed package version is greater than or equal to ``version``.

:param version: Minimum version as a string (``"2.13.1-3"``) or version dict.
:param package: Package name (default: sssd).
:param distro_ids: If set (e.g. ``("rhel",)``), only hosts whose
``/etc/os-release`` ``ID`` is in this list are version-checked.
Other distros (CentOS upstream PRCI, Fedora, …) are treated as
meeting the requirement. ``None`` compares on every distro.
:return: Whether the installed package meets the minimum version.
"""
min_version = parse_package_version(version) if isinstance(version, str) else version
if distro_ids is not None and self.distro_id not in distro_ids:
return True
return self.compare_package_version(min_version, package) >= 0

@contextlib.contextmanager
def selinux_permissive_for_test(self) -> Generator[None, None, None]:
"""Temporarily set SELinux to permissive for tests."""
Expand Down
39 changes: 39 additions & 0 deletions sssd_test_framework/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from functools import partial
from typing import Callable

import pytest
from pytest_mh import MultihostItemData, Topology
Expand All @@ -25,6 +26,44 @@ def pytest_configure(config: pytest.Config):
)


def require_sssd_version(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this require_sssd_version and not generic require_package_version?
require_sssd_version could be just calling it instead. with package: str = "sssd"?

version: str,
*,
distro_ids: tuple[str, ...] | list[str] | None = ("rhel",),
package: str = "sssd",
) -> Callable[..., bool]:
"""
Build a condition for ``@pytest.mark.require``.

Skips the test when the client's installed package is older than ``version``.

By default the check applies only on RHEL (``/etc/os-release`` ``ID=rhel``):
downstream gating, nightlies, and signoff. Upstream PRCI on CentOS, Fedora,
and other distros are not skipped. Pass ``distro_ids=None`` to compare on
every distro.

Example::

from sssd_test_framework.markers import require_sssd_version

@pytest.mark.require(
require_sssd_version("2.13.1-3", distro_ids=("rhel",)),
"Needs sssd >= 2.13.1-3 on RHEL (RHEL-248164)",
)
def test_something(client):
...
"""

def _check(client) -> bool:
return client.host.package_version_at_least(
version,
package=package,
distro_ids=distro_ids,
)

return _check


def builtwith(item: pytest.Function, requirements: dict[str, str], **kwargs: BaseRole):
def value_error(msg: str) -> ValueError:
return ValueError(f"{item.nodeid}::{item.originalname}: @pytest.mark.builtwith: {msg}")
Expand Down
32 changes: 32 additions & 0 deletions sssd_test_framework/misc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import ipaddress
import itertools
import re
from functools import wraps
from time import sleep
from typing import Any, Callable, ParamSpec, TypeVar
Expand Down Expand Up @@ -379,3 +380,34 @@ def attrs_ad_parse(output: str) -> dict[str, list[str]]:
"""
lines = [line.strip() for line in (output or "").splitlines() if line.strip()]
return attrs_parse(lines)


def parse_package_version(version: str) -> dict[str, Any]:
"""
Parse a ``VERSION-RELEASE`` style string into the dict used by
:meth:`sssd_test_framework.hosts.base.BaseLinuxHost.compare_package_version`.

Examples: ``"2.13.1-3"``, ``"2.13.1-2.el10"``, ``"2.9.9"``.

:param version: Version string to parse.
:type version: str
:return: Dictionary with major, minor, patch, prerelease, update, release.
:rtype: dict[str, Any]
:raises ValueError: If the version string cannot be parsed.
"""
match = re.match(
r"([0-9]+)(?:\.)?([0-9]+)?(?:\.)?([0-9]+)?(?:~)?([a-z0-9]+)?[\.-]?([0-9]+)?(?:\.)?(.*)?",
version,
re.IGNORECASE,
)
if match is None:
raise ValueError(f"Invalid package version string: {version!r}")

return {
"major": int(match.group(1)),
"minor": int(match.group(2)) if match.group(2) else 0,
"patch": int(match.group(3)) if match.group(3) else 0,
"prerelease": match.group(4) if match.group(4) else "",
"update": int(match.group(5)) if match.group(5) else 0,
"release": match.group(6) if match.group(6) else "",
}
31 changes: 31 additions & 0 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
ip_to_ptr,
ip_version,
parse_ldif,
parse_package_version,
retry,
seconds_to_timespan,
to_list,
Expand Down Expand Up @@ -471,3 +472,33 @@ def test_get_attr(get_attr_sample_data, key, expected, check_membership):
def test_attrs_ad_parse(value, expected):
value = textwrap.dedent(value).strip()
assert attrs_ad_parse(value) == expected


@pytest.mark.parametrize(
"version,expected",
[
(
"2.13.1-3",
{"major": 2, "minor": 13, "patch": 1, "prerelease": "", "update": 3, "release": ""},
),
(
"2.13.1-2.el10",
{"major": 2, "minor": 13, "patch": 1, "prerelease": "", "update": 2, "release": "el10"},
),
(
"2.13.1-3.el10_3",
{"major": 2, "minor": 13, "patch": 1, "prerelease": "", "update": 3, "release": "el10_3"},
),
(
"2.9.9",
{"major": 2, "minor": 9, "patch": 9, "prerelease": "", "update": 0, "release": ""},
),
],
)
def test_parse_package_version(version, expected):
assert parse_package_version(version) == expected


def test_parse_package_version__invalid():
with pytest.raises(ValueError, match="Invalid package version"):
parse_package_version("not-a-version")
Loading