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

From 7720401d74ed6eafe860aab297aee0c8e22bc00f 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               | 108 ++++++++++++++++++----
 tests/pytests/unit/modules/test_aptpkg.py |  52 +++++++++++
 3 files changed, 166 insertions(+), 18 deletions(-)

diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py
index 8d9f1b9f52..3c3fbf4970 100644
--- a/salt/modules/aptpkg.py
+++ b/salt/modules/aptpkg.py
@@ -3035,6 +3035,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
@@ -3045,11 +3054,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
@@ -3070,7 +3087,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 6a88573a8f..afbd619490 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"
@@ -296,9 +334,16 @@ 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
+        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)
 
@@ -324,24 +369,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():
@@ -391,6 +446,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
@@ -405,6 +469,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)
 
@@ -432,6 +500,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 6c5ed29848..51b7ffbe4d 100644
--- a/tests/pytests/unit/modules/test_aptpkg.py
+++ b/tests/pytests/unit/modules/test_aptpkg.py
@@ -336,6 +336,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.34.1


openSUSE Build Service is sponsored by