File debian-info_installed-compatibility-50453.patch of Package salt

From 2fbc5b580661b094cf79cc5da0860745b72088e4 Mon Sep 17 00:00:00 2001
From: Alexander Graul <agraul@suse.com>
Date: Tue, 25 Jan 2022 17:08:57 +0100
Subject: [PATCH] Debian info_installed compatibility (#50453)

Remove unused variable

Get unit ticks installation time

Pass on unix ticks installation date time

Implement function to figure out package build time

Unify arch attribute

Add 'attr' support.

Use attr parameter in aptpkg

Add 'all_versions' output structure backward compatibility

Fix docstring

Add UT for generic test of function 'info'

Add UT for 'info' function with the parameter 'attr'

Add UT for info_installed's 'attr' param

Fix docstring

Add returned type check

Add UT for info_installed with 'all_versions=True' output structure

Refactor UT for 'owner' function

Refactor UT: move to decorators, add more checks

Schedule TODO for next refactoring of UT 'show' function

Refactor UT: get rid of old assertion way, flatten tests

Refactor UT: move to native assertions, cleanup noise, flatten complexity for better visibility what is tested

Lintfix: too many empty lines

Adjust architecture getter according to the lowpkg info

Fix wrong Git merge: missing function signature

Reintroducing reverted changes

Reintroducing changes from commit e20362f6f053eaa4144583604e6aac3d62838419
that got partially reverted by this commit:
https://github.com/openSUSE/salt/commit/d0ef24d113bdaaa29f180031b5da384cffe08c64#diff-820e6ce667fe3afddbc1b9cf1682fdef
---
 salt/modules/aptpkg.py                    |  24 ++++-
 salt/modules/dpkg_lowpkg.py               | 110 ++++++++++++++++++----
 tests/pytests/unit/modules/test_aptpkg.py |  52 ++++++++++
 3 files changed, 167 insertions(+), 19 deletions(-)

diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py
index 8e89744b5e..938e37cc9e 100644
--- a/salt/modules/aptpkg.py
+++ b/salt/modules/aptpkg.py
@@ -3440,6 +3440,15 @@ def info_installed(*names, **kwargs):
 
         .. versionadded:: 2016.11.3
 
+    attr
+        Comma-separated package attributes. If no 'attr' is specified, all available attributes returned.
+
+        Valid attributes are:
+            version, vendor, release, build_date, build_date_time_t, install_date, install_date_time_t,
+            build_host, group, source_rpm, arch, epoch, size, license, signature, packager, url, summary, description.
+
+        .. versionadded:: Neon
+
     CLI Example:
 
     .. code-block:: bash
@@ -3450,11 +3459,19 @@ def info_installed(*names, **kwargs):
     """
     kwargs = salt.utils.args.clean_kwargs(**kwargs)
     failhard = kwargs.pop("failhard", True)
+    kwargs.pop("errors", None)  # Only for compatibility with RPM
+    attr = kwargs.pop("attr", None)  # Package attributes to return
+    all_versions = kwargs.pop(
+        "all_versions", False
+    )  # This is for backward compatible structure only
+
     if kwargs:
         salt.utils.args.invalid_kwargs(kwargs)
 
     ret = dict()
-    for pkg_name, pkg_nfo in __salt__["lowpkg.info"](*names, failhard=failhard).items():
+    for pkg_name, pkg_nfo in __salt__["lowpkg.info"](
+        *names, failhard=failhard, attr=attr
+    ).items():
         t_nfo = dict()
         if pkg_nfo.get("status", "ii")[1] != "i":
             continue  # return only packages that are really installed
@@ -3475,7 +3492,10 @@ def info_installed(*names, **kwargs):
             else:
                 t_nfo[key] = value
 
-        ret[pkg_name] = t_nfo
+        if all_versions:
+            ret.setdefault(pkg_name, []).append(t_nfo)
+        else:
+            ret[pkg_name] = t_nfo
 
     return ret
 
diff --git a/salt/modules/dpkg_lowpkg.py b/salt/modules/dpkg_lowpkg.py
index eefd852c51..4d716c8772 100644
--- a/salt/modules/dpkg_lowpkg.py
+++ b/salt/modules/dpkg_lowpkg.py
@@ -234,6 +234,44 @@ def file_dict(*packages, **kwargs):
     return {"errors": errors, "packages": ret}
 
 
+def _get_pkg_build_time(name):
+    """
+    Get package build time, if possible.
+
+    :param name:
+    :return:
+    """
+    iso_time = iso_time_t = None
+    changelog_dir = os.path.join("/usr/share/doc", name)
+    if os.path.exists(changelog_dir):
+        for fname in os.listdir(changelog_dir):
+            try:
+                iso_time_t = int(os.path.getmtime(os.path.join(changelog_dir, fname)))
+                iso_time = (
+                    datetime.datetime.utcfromtimestamp(iso_time_t).isoformat() + "Z"
+                )
+                break
+            except OSError:
+                pass
+
+    # Packager doesn't care about Debian standards, therefore Plan B: brute-force it.
+    if not iso_time:
+        for pkg_f_path in __salt__["cmd.run"](
+            "dpkg-query -L {}".format(name)
+        ).splitlines():
+            if "changelog" in pkg_f_path.lower() and os.path.exists(pkg_f_path):
+                try:
+                    iso_time_t = int(os.path.getmtime(pkg_f_path))
+                    iso_time = (
+                        datetime.datetime.utcfromtimestamp(iso_time_t).isoformat() + "Z"
+                    )
+                    break
+                except OSError:
+                    pass
+
+    return iso_time, iso_time_t
+
+
 def _get_pkg_info(*packages, **kwargs):
     """
     Return list of package information. If 'packages' parameter is empty,
@@ -257,7 +295,7 @@ def _get_pkg_info(*packages, **kwargs):
     cmd = (
         "dpkg-query -W -f='package:" + bin_var + "\\n"
         "revision:${binary:Revision}\\n"
-        "architecture:${Architecture}\\n"
+        "arch:${Architecture}\\n"
         "maintainer:${Maintainer}\\n"
         "summary:${Summary}\\n"
         "source:${source:Package}\\n"
@@ -299,10 +337,17 @@ def _get_pkg_info(*packages, **kwargs):
             key, value = pkg_info_line.split(":", 1)
             if value:
                 pkg_data[key] = value
-            install_date = _get_pkg_install_time(pkg_data.get("package"))
-            if install_date:
-                pkg_data["install_date"] = install_date
-        pkg_data["description"] = pkg_descr
+        install_date, install_date_t = _get_pkg_install_time(
+            pkg_data.get("package"), pkg_data.get("arch")
+        )
+        if install_date:
+            pkg_data["install_date"] = install_date
+            pkg_data["install_date_time_t"] = install_date_t  # Unix ticks
+        build_date, build_date_t = _get_pkg_build_time(pkg_data.get("package"))
+        if build_date:
+            pkg_data["build_date"] = build_date
+            pkg_data["build_date_time_t"] = build_date_t
+        pkg_data["description"] = pkg_descr.split(":", 1)[-1]
         ret.append(pkg_data)
 
     return ret
@@ -327,24 +372,34 @@ def _get_pkg_license(pkg):
     return ", ".join(sorted(licenses))
 
 
-def _get_pkg_install_time(pkg):
+def _get_pkg_install_time(pkg, arch):
     """
     Return package install time, based on the /var/lib/dpkg/info/<package>.list
 
     :return:
     """
-    iso_time = None
+    iso_time = iso_time_t = None
+    loc_root = "/var/lib/dpkg/info"
     if pkg is not None:
-        location = "/var/lib/dpkg/info/{}.list".format(pkg)
-        if os.path.exists(location):
-            iso_time = (
-                datetime.datetime.utcfromtimestamp(
-                    int(os.path.getmtime(location))
-                ).isoformat()
-                + "Z"
-            )
+        locations = []
+        if arch is not None and arch != "all":
+            locations.append(os.path.join(loc_root, "{}:{}.list".format(pkg, arch)))
 
-    return iso_time
+        locations.append(os.path.join(loc_root, "{}.list".format(pkg)))
+        for location in locations:
+            try:
+                iso_time_t = int(os.path.getmtime(location))
+                iso_time = (
+                    datetime.datetime.utcfromtimestamp(iso_time_t).isoformat() + "Z"
+                )
+                break
+            except OSError:
+                pass
+
+        if iso_time is None:
+            log.debug('Unable to get package installation time for package "%s".', pkg)
+
+    return iso_time, iso_time_t
 
 
 def _get_pkg_ds_avail():
@@ -394,6 +449,15 @@ def info(*packages, **kwargs):
 
         .. versionadded:: 2016.11.3
 
+    attr
+        Comma-separated package attributes. If no 'attr' is specified, all available attributes returned.
+
+        Valid attributes are:
+            version, vendor, release, build_date, build_date_time_t, install_date, install_date_time_t,
+            build_host, group, source_rpm, arch, epoch, size, license, signature, packager, url, summary, description.
+
+        .. versionadded:: Neon
+
     CLI Example:
 
     .. code-block:: bash
@@ -408,6 +472,10 @@ def info(*packages, **kwargs):
 
     kwargs = salt.utils.args.clean_kwargs(**kwargs)
     failhard = kwargs.pop("failhard", True)
+    attr = kwargs.pop("attr", None) or None
+    if attr:
+        attr = attr.split(",")
+
     if kwargs:
         salt.utils.args.invalid_kwargs(kwargs)
 
@@ -435,6 +503,14 @@ def info(*packages, **kwargs):
         lic = _get_pkg_license(pkg["package"])
         if lic:
             pkg["license"] = lic
-        ret[pkg["package"]] = pkg
+
+        # Remove keys that aren't in attrs
+        pkg_name = pkg["package"]
+        if attr:
+            for k in list(pkg.keys())[:]:
+                if k not in attr:
+                    del pkg[k]
+
+        ret[pkg_name] = pkg
 
     return ret
diff --git a/tests/pytests/unit/modules/test_aptpkg.py b/tests/pytests/unit/modules/test_aptpkg.py
index b69402578a..4226957eeb 100644
--- a/tests/pytests/unit/modules/test_aptpkg.py
+++ b/tests/pytests/unit/modules/test_aptpkg.py
@@ -360,6 +360,58 @@ def test_info_installed(lowpkg_info_var):
         assert len(aptpkg.info_installed()) == 1
 
 
+def test_info_installed_attr(lowpkg_info_var):
+    """
+    Test info_installed 'attr'.
+    This doesn't test 'attr' behaviour per se, since the underlying function is in dpkg.
+    The test should simply not raise exceptions for invalid parameter.
+
+    :return:
+    """
+    expected_pkg = {
+        "url": "http://www.gnu.org/software/wget/",
+        "packager": "Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>",
+        "name": "wget",
+        "install_date": "2016-08-30T22:20:15Z",
+        "description": "retrieves files from the web",
+        "version": "1.15-1ubuntu1.14.04.2",
+        "architecture": "amd64",
+        "group": "web",
+        "source": "wget",
+    }
+    mock = MagicMock(return_value=lowpkg_info_var)
+    with patch.dict(aptpkg.__salt__, {"lowpkg.info": mock}):
+        ret = aptpkg.info_installed("wget", attr="foo,bar")
+        assert ret["wget"] == expected_pkg
+
+
+def test_info_installed_all_versions(lowpkg_info_var):
+    """
+    Test info_installed 'all_versions'.
+    Since Debian won't return same name packages with the different names,
+    this should just return different structure, backward compatible with
+    the RPM equivalents.
+
+    :return:
+    """
+    expected_pkg = {
+        "url": "http://www.gnu.org/software/wget/",
+        "packager": "Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>",
+        "name": "wget",
+        "install_date": "2016-08-30T22:20:15Z",
+        "description": "retrieves files from the web",
+        "version": "1.15-1ubuntu1.14.04.2",
+        "architecture": "amd64",
+        "group": "web",
+        "source": "wget",
+    }
+    mock = MagicMock(return_value=lowpkg_info_var)
+    with patch.dict(aptpkg.__salt__, {"lowpkg.info": mock}):
+        ret = aptpkg.info_installed("wget", all_versions=True)
+        assert isinstance(ret, dict)
+        assert ret["wget"] == [expected_pkg]
+
+
 def test_owner():
     """
     Test - Return the name of the package that owns the file.
-- 
2.39.2


openSUSE Build Service is sponsored by