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