From 2fbc5b580661b094cf79cc5da0860745b72088e4 Mon Sep 17 00:00:00 2001 From: Alexander Graul 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/.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 ", + "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 ", + "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