diff --git a/uaclient/apt.py b/uaclient/apt.py index c6e002cb1b..1de49c3742 100644 --- a/uaclient/apt.py +++ b/uaclient/apt.py @@ -736,7 +736,7 @@ def get_installed_packages() -> List[InstalledAptPackage]: ] -def get_installed_packages_names(include_versions: bool = False) -> List[str]: +def get_installed_packages_names() -> List[str]: package_list = get_installed_packages() pkg_names = [pkg.name for pkg in package_list] return pkg_names diff --git a/uaclient/apt_news.py b/uaclient/apt_news.py index 4a470f343a..86aa2f722c 100644 --- a/uaclient/apt_news.py +++ b/uaclient/apt_news.py @@ -9,7 +9,12 @@ from uaclient import defaults, messages, system, util from uaclient.api.u.pro.status.is_attached.v1 import _is_attached -from uaclient.apt import ensure_apt_pkg_init +from uaclient.apt import ( + ensure_apt_pkg_init, + get_installed_packages, + is_installed, + version_compare, +) from uaclient.clouds.identity import get_cloud_type from uaclient.config import UAConfig from uaclient.contract import ContractExpiryStatus, get_contract_expiry_status @@ -31,6 +36,10 @@ class AptNewsMessageSelectors(DataObject): Field("codenames", data_list(StringDataValue), required=False), Field("clouds", data_list(StringDataValue), required=False), Field("pro", BoolDataValue, required=False), + Field("architectures", data_list(StringDataValue), required=False), + Field( + "packages", data_list(data_list(StringDataValue)), required=False + ), ] def __init__( @@ -38,11 +47,15 @@ def __init__( *, codenames: Optional[List[str]] = None, clouds: Optional[List[str]] = None, - pro: Optional[bool] = None + pro: Optional[bool] = None, + architectures: Optional[List[str]] = None, + packages: Optional[List[List[str]]] = None, ): self.codenames = codenames self.clouds = clouds self.pro = pro + self.architectures = architectures + self.packages = packages class AptNewsMessage(DataObject): @@ -88,6 +101,52 @@ def do_selectors_apply( if selectors.pro != _is_attached(cfg).is_attached: return False + if selectors.architectures is not None: + if system.get_dpkg_arch() not in selectors.architectures: + return False + + if selectors.packages is not None: + installed_packages = get_installed_packages() + installed_packages_names = [ + package.name for package in installed_packages + ] + package_matched = False + + for package in selectors.packages: + if len(package) != 3: + LOG.warning("Invalid package selector: %r", package) + return False + + package_name, version_operator, package_version = package + if not is_installed(package_name): + installed_package = installed_packages[ + installed_packages_names.index(package_name) + ] + version_comparison = version_compare( + installed_package.version, package_version + ) + if any( + [ + ( + version_comparison == 0 + and version_operator in ["==", "<=", ">="] + ), + ( + version_comparison < 0 + and version_operator in ["<", "<="] + ), + ( + version_comparison > 0 + and version_operator in [">", ">="] + ), + ] + ): + package_matched = True + break + + if not package_matched: + return False + return True diff --git a/uaclient/tests/test_apt_news.py b/uaclient/tests/test_apt_news.py index 9cdb274aa6..b7079ed621 100644 --- a/uaclient/tests/test_apt_news.py +++ b/uaclient/tests/test_apt_news.py @@ -3,7 +3,7 @@ import mock import pytest -from uaclient import apt_news, messages +from uaclient import apt, apt_news, messages from uaclient.clouds.identity import NoCloudTypeReason from uaclient.contract import ContractExpiryStatus @@ -13,16 +13,38 @@ class TestAptNews: + # Add architectures and packages + # get list of packages mocked from test_system.py line 591 + @pytest.mark.parametrize( - ["selectors", "series", "cloud_type", "attached", "expected"], + [ + "selectors", + "series", + "cloud_type", + "attached", + "architecture", + "packages", + "expected", + ], [ ( apt_news.AptNewsMessageSelectors(), "xenial", (None, NoCloudTypeReason.NO_CLOUD_DETECTED), False, + None, + None, True, ), + ( + apt_news.AptNewsMessageSelectors(codenames=["bionic"]), + "xenial", + (None, NoCloudTypeReason.NO_CLOUD_DETECTED), + False, + None, + None, + False, + ), ( apt_news.AptNewsMessageSelectors( codenames=["bionic", "xenial"] @@ -30,13 +52,19 @@ class TestAptNews: "xenial", (None, NoCloudTypeReason.NO_CLOUD_DETECTED), False, + None, + None, True, ), ( - apt_news.AptNewsMessageSelectors(codenames=["bionic"]), + apt_news.AptNewsMessageSelectors( + codenames=["xenial"], pro=True + ), "xenial", (None, NoCloudTypeReason.NO_CLOUD_DETECTED), False, + None, + None, False, ), ( @@ -45,67 +73,151 @@ class TestAptNews: ), "xenial", (None, NoCloudTypeReason.NO_CLOUD_DETECTED), + True, + None, + None, + True, + ), + ( + apt_news.AptNewsMessageSelectors( + codenames=["bionic"], + pro=False, + clouds=["gce"], + ), + "bionic", + (None, NoCloudTypeReason.NO_CLOUD_DETECTED), False, + None, + None, False, ), ( apt_news.AptNewsMessageSelectors( - codenames=["xenial"], pro=True + codenames=["bionic"], + pro=False, + clouds=["gce"], + ), + "bionic", + (None, NoCloudTypeReason.CLOUD_ID_ERROR), + False, + None, + None, + False, + ), + ( + apt_news.AptNewsMessageSelectors( + pro=False, architectures=["amd64"] ), "xenial", (None, NoCloudTypeReason.NO_CLOUD_DETECTED), - True, + False, + "amd64", + None, True, ), ( apt_news.AptNewsMessageSelectors( - codenames=["bionic"], pro=False + pro=False, architectures=["arm64"] ), "xenial", (None, NoCloudTypeReason.NO_CLOUD_DETECTED), False, + "amd64", + None, False, ), ( apt_news.AptNewsMessageSelectors( - codenames=["bionic"], pro=False + pro=False, packages=[["not-desktop", "==", "1.0.0"]] ), - "bionic", + "xenial", (None, NoCloudTypeReason.NO_CLOUD_DETECTED), False, + None, + [ + apt.InstalledAptPackage( + name="not-desktop", version="1.0.0", arch="" + ), + ], True, ), ( apt_news.AptNewsMessageSelectors( - codenames=["bionic"], - pro=False, - clouds=["gce"], + pro=False, packages=[["not-desktop", "=="]] ), - "bionic", + "xenial", (None, NoCloudTypeReason.NO_CLOUD_DETECTED), False, + None, + [ + apt.InstalledAptPackage( + name="not-desktop", version="1.0.0", arch="" + ), + ], False, ), ( apt_news.AptNewsMessageSelectors( - codenames=["bionic"], - pro=False, - clouds=["gce"], + pro=False, packages=[["not-desktop", "==", "1.0.0"]] ), - "bionic", - (None, NoCloudTypeReason.CLOUD_ID_ERROR), + "xenial", + (None, NoCloudTypeReason.NO_CLOUD_DETECTED), + False, + None, + [ + apt.InstalledAptPackage( + name="not-desktop", version="1.0.1", arch="" + ), + ], + False, + ), + ( + apt_news.AptNewsMessageSelectors( + pro=False, packages=[["not-desktop", ">", "1.0.0"]] + ), + "xenial", + (None, NoCloudTypeReason.NO_CLOUD_DETECTED), False, + None, + [ + apt.InstalledAptPackage( + name="not-desktop", version="1.0.1", arch="" + ), + ], + True, + ), + ( + apt_news.AptNewsMessageSelectors( + pro=False, packages=[["not-desktop", "<", "1.0.0"]] + ), + "xenial", + (None, NoCloudTypeReason.NO_CLOUD_DETECTED), False, + None, + [ + apt.InstalledAptPackage( + name="not-desktop", version="0.0.1", arch="" + ), + ], + True, ), ( apt_news.AptNewsMessageSelectors( codenames=["bionic"], pro=False, clouds=["gce"], + architectures=["amd64"], + packages=[["not-desktop", ">", "1.0.0"]], ), "bionic", ("aws", None), False, + "arm4", + [ + apt.InstalledAptPackage( + name="not-desktop", version="0.0.7", arch="" + ), + ], False, ), ( @@ -113,24 +225,38 @@ class TestAptNews: codenames=["bionic"], pro=False, clouds=["gce"], + architectures=["amd64"], + packages=[["not-desktop", ">", "1.0.0"]], ), "bionic", ("gce", None), False, + "amd64", + [ + apt.InstalledAptPackage( + name="not-desktop", version="1.0.1", arch="" + ), + ], True, ), ], ) @mock.patch(M_PATH + "get_cloud_type") @mock.patch(M_PATH + "system.get_release_info") + @mock.patch(M_PATH + "system.get_dpkg_arch") + @mock.patch(M_PATH + "get_installed_packages") def test_do_selectors_apply( self, + m_installed_packages, + m_get_dpkg_arch, m_get_platform_info, m_get_cloud_type, selectors, series, cloud_type, attached, + architecture, + packages, expected, FakeConfig, ): @@ -140,6 +266,8 @@ def test_do_selectors_apply( cfg = FakeConfig() m_get_platform_info.return_value = mock.MagicMock(series=series) m_get_cloud_type.return_value = cloud_type + m_get_dpkg_arch.return_value = architecture + m_installed_packages.return_value = packages assert expected == apt_news.do_selectors_apply(cfg, selectors) @pytest.mark.parametrize(