File add-all_versions-parameter-to-include-all-installed-.patch of Package salt.7664

From 8d3b589b0ac9b5cda98d9ffe25c75d3ee387f898 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
 <psuarezhernandez@suse.com>
Date: Mon, 14 May 2018 11:33:13 +0100
Subject: [PATCH] Add "all_versions" parameter to include all installed
 version on rpm.info

Enable "all_versions" parameter for zypper.info_installed

Enable "all_versions" parameter for yumpkg.info_installed

Prevent adding failed packages when pkg name contains the arch (on SUSE)

Add 'all_versions' documentation for info_installed on yum/zypper modules

Add unit tests for info_installed with all_versions

Refactor: use dict.setdefault instead if-else statement

Allow removing only specific package versions with zypper and yum
---
 salt/modules/rpm.py               | 18 ++++++++---
 salt/modules/yumpkg.py            | 49 ++++++++++++++++++++++--------
 salt/modules/zypper.py            | 64 ++++++++++++++++++++++++++++++++-------
 salt/states/pkg.py                | 33 +++++++++++++++++++-
 tests/unit/modules/test_yumpkg.py | 50 ++++++++++++++++++++++++++++++
 tests/unit/modules/test_zypper.py | 50 ++++++++++++++++++++++++++++++
 6 files changed, 236 insertions(+), 28 deletions(-)

diff --git a/salt/modules/rpm.py b/salt/modules/rpm.py
index d065f1e2d9..3683234f59 100644
--- a/salt/modules/rpm.py
+++ b/salt/modules/rpm.py
@@ -453,7 +453,7 @@ def diff(package, path):
     return res
 
 
-def info(*packages, **attr):
+def info(*packages, **kwargs):
     '''
     Return a detailed package(s) summary information.
     If no packages specified, all packages will be returned.
@@ -467,6 +467,9 @@ def info(*packages, **attr):
             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.
 
+    :param all_versions:
+        Return information for all installed versions of the packages
+
     :return:
 
     CLI example:
@@ -476,7 +479,9 @@ def info(*packages, **attr):
         salt '*' lowpkg.info apache2 bash
         salt '*' lowpkg.info apache2 bash attr=version
         salt '*' lowpkg.info apache2 bash attr=version,build_date_iso,size
+        salt '*' lowpkg.info apache2 bash attr=version,build_date_iso,size all_versions=True
     '''
+    all_versions = kwargs.get('all_versions', False)
     # LONGSIZE is not a valid tag for all versions of rpm. If LONGSIZE isn't
     # available, then we can just use SIZE for older versions. See Issue #31366.
     rpm_tags = __salt__['cmd.run_stdout'](
@@ -516,7 +521,7 @@ def info(*packages, **attr):
         "edition": "edition: %|EPOCH?{%{EPOCH}:}|%{VERSION}-%{RELEASE}\\n",
     }
 
-    attr = attr.get('attr', None) and attr['attr'].split(",") or None
+    attr = kwargs.get('attr', None) and kwargs['attr'].split(",") or None
     query = list()
     if attr:
         for attr_k in attr:
@@ -610,8 +615,13 @@ def info(*packages, **attr):
         if pkg_name.startswith('gpg-pubkey'):
             continue
         if pkg_name not in ret:
-            ret[pkg_name] = pkg_data.copy()
-            del ret[pkg_name]['edition']
+            if all_versions:
+                ret[pkg_name] = [pkg_data.copy()]
+            else:
+                ret[pkg_name] = pkg_data.copy()
+                del ret[pkg_name]['edition']
+        elif all_versions:
+            ret[pkg_name].append(pkg_data.copy())
 
     return ret
 
diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py
index 9eb27e7701..42eedca227 100644
--- a/salt/modules/yumpkg.py
+++ b/salt/modules/yumpkg.py
@@ -994,31 +994,39 @@ def list_downloaded():
     return ret
 
 
-def info_installed(*names):
+def info_installed(*names, **kwargs):
     '''
     .. versionadded:: 2015.8.1
 
     Return the information of the named package(s), installed on the system.
 
+    :param all_versions:
+        Include information for all versions of the packages installed on the minion.
+
     CLI example:
 
     .. code-block:: bash
 
         salt '*' pkg.info_installed <package1>
         salt '*' pkg.info_installed <package1> <package2> <package3> ...
+        salt '*' pkg.info_installed <package1> <package2> <package3> all_versions=True
     '''
+    all_versions = kwargs.get('all_versions', False)
     ret = dict()
-    for pkg_name, pkg_nfo in __salt__['lowpkg.info'](*names).items():
-        t_nfo = dict()
-        # Translate dpkg-specific keys to a common structure
-        for key, value in pkg_nfo.items():
-            if key == 'source_rpm':
-                t_nfo['source'] = value
+    for pkg_name, pkgs_nfo in __salt__['lowpkg.info'](*names, **kwargs).items():
+        pkg_nfo = pkgs_nfo if all_versions else [pkgs_nfo]
+        for _nfo in pkg_nfo:
+            t_nfo = dict()
+            # Translate dpkg-specific keys to a common structure
+            for key, value in _nfo.items():
+                if key == 'source_rpm':
+                    t_nfo['source'] = value
+                else:
+                    t_nfo[key] = value
+            if not all_versions:
+                ret[pkg_name] = t_nfo
             else:
-                t_nfo[key] = value
-
-        ret[pkg_name] = t_nfo
-
+                ret.setdefault(pkg_name, []).append(t_nfo)
     return ret
 
 
@@ -1919,7 +1927,24 @@ def remove(name=None, pkgs=None, **kwargs):  # pylint: disable=W0613
         raise CommandExecutionError(exc)
 
     old = list_pkgs()
-    targets = [x for x in pkg_params if x in old]
+    targets = []
+    for target in pkg_params:
+        # Check if package version set to be removed is actually installed:
+        # old[target] contains a comma-separated list of installed versions
+        if target in old and not pkg_params[target]:
+            targets.append(target)
+        elif target in old and pkg_params[target] in old[target].split(','):
+            arch = ''
+            pkgname = target
+            try:
+                namepart, archpart = target.rsplit('.', 1)
+            except ValueError:
+                pass
+            else:
+                if archpart in salt.utils.pkg.rpm.ARCHES:
+                    arch = '.' + archpart
+                    pkgname = namepart
+            targets.append('{0}-{1}{2}'.format(pkgname, pkg_params[target], arch))
     if not targets:
         return {}
 
diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py
index 16fc877684..68dd37f1b7 100644
--- a/salt/modules/zypper.py
+++ b/salt/modules/zypper.py
@@ -470,28 +470,37 @@ def info_installed(*names, **kwargs):
         Valid attributes are:
             ignore, report
 
+    :param all_versions:
+        Include information for all versions of the packages installed on the minion.
+
     CLI example:
 
     .. code-block:: bash
 
         salt '*' pkg.info_installed <package1>
         salt '*' pkg.info_installed <package1> <package2> <package3> ...
-        salt '*' pkg.info_installed <package1> attr=version,vendor
+        salt '*' pkg.info_installed <package1> <package2> <package3> all_versions=True
+        salt '*' pkg.info_installed <package1> attr=version,vendor all_versions=True
         salt '*' pkg.info_installed <package1> <package2> <package3> ... attr=version,vendor
         salt '*' pkg.info_installed <package1> <package2> <package3> ... attr=version,vendor errors=ignore
         salt '*' pkg.info_installed <package1> <package2> <package3> ... attr=version,vendor errors=report
     '''
+    all_versions = kwargs.get('all_versions', False)
     ret = dict()
-    for pkg_name, pkg_nfo in __salt__['lowpkg.info'](*names, **kwargs).items():
-        t_nfo = dict()
-        # Translate dpkg-specific keys to a common structure
-        for key, value in six.iteritems(pkg_nfo):
-            if key == 'source_rpm':
-                t_nfo['source'] = value
+    for pkg_name, pkgs_nfo in __salt__['lowpkg.info'](*names, **kwargs).items():
+        pkg_nfo = pkgs_nfo if all_versions else [pkgs_nfo]
+        for _nfo in pkg_nfo:
+            t_nfo = dict()
+            # Translate dpkg-specific keys to a common structure
+            for key, value in six.iteritems(_nfo):
+                if key == 'source_rpm':
+                    t_nfo['source'] = value
+                else:
+                    t_nfo[key] = value
+            if not all_versions:
+                ret[pkg_name] = t_nfo
             else:
-                t_nfo[key] = value
-        ret[pkg_name] = t_nfo
-
+                ret.setdefault(pkg_name, []).append(t_nfo)
     return ret
 
 
@@ -1367,7 +1376,14 @@ def _uninstall(name=None, pkgs=None):
         raise CommandExecutionError(exc)
 
     old = list_pkgs()
-    targets = [target for target in pkg_params if target in old]
+    targets = []
+    for target in pkg_params:
+        # Check if package version set to be removed is actually installed:
+        # old[target] contains a comma-separated list of installed versions
+        if target in old and pkg_params[target] in old[target].split(','):
+            targets.append(target + "-" + pkg_params[target])
+        elif target in old and not pkg_params[target]:
+            targets.append(target)
     if not targets:
         return {}
 
@@ -1390,6 +1406,32 @@ def _uninstall(name=None, pkgs=None):
     return ret
 
 
+def normalize_name(name):
+    '''
+    Strips the architecture from the specified package name, if necessary.
+    Circumstances where this would be done include:
+
+    * If the arch is 32 bit and the package name ends in a 32-bit arch.
+    * If the arch matches the OS arch, or is ``noarch``.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' pkg.normalize_name zsh.x86_64
+    '''
+    try:
+        arch = name.rsplit('.', 1)[-1]
+        if arch not in salt.utils.pkg.rpm.ARCHES + ('noarch',):
+            return name
+    except ValueError:
+        return name
+    if arch in (__grains__['osarch'], 'noarch') \
+            or salt.utils.pkg.rpm.check_32(arch, osarch=__grains__['osarch']):
+        return name[:-(len(arch) + 1)]
+    return name
+
+
 def remove(name=None, pkgs=None, **kwargs):  # pylint: disable=unused-argument
     '''
     .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0
diff --git a/salt/states/pkg.py b/salt/states/pkg.py
index 4c406aa04e..58a5896a6a 100644
--- a/salt/states/pkg.py
+++ b/salt/states/pkg.py
@@ -405,6 +405,16 @@ def _find_remove_targets(name=None,
 
         if __grains__['os'] == 'FreeBSD' and origin:
             cver = [k for k, v in six.iteritems(cur_pkgs) if v['origin'] == pkgname]
+        elif __grains__['os_family'] == 'Suse':
+            # On SUSE systems. Zypper returns packages without "arch" in name
+            try:
+                namepart, archpart = pkgname.rsplit('.', 1)
+            except ValueError:
+                cver = cur_pkgs.get(pkgname, [])
+            else:
+                if archpart in salt.utils.pkg.rpm.ARCHES + ("noarch",):
+                    pkgname = namepart
+                cver = cur_pkgs.get(pkgname, [])
         else:
             cver = cur_pkgs.get(pkgname, [])
 
@@ -812,6 +822,17 @@ def _verify_install(desired, new_pkgs, ignore_epoch=False, new_caps=None):
             cver = new_pkgs.get(pkgname.split('%')[0])
         elif __grains__['os_family'] == 'Debian':
             cver = new_pkgs.get(pkgname.split('=')[0])
+        elif __grains__['os_family'] == 'Suse':
+            # On SUSE systems. Zypper returns packages without "arch" in name
+            try:
+                namepart, archpart = pkgname.rsplit('.', 1)
+            except ValueError:
+                cver = new_pkgs.get(pkgname)
+            else:
+                if archpart in salt.utils.pkg.rpm.ARCHES + ("noarch",):
+                    cver = new_pkgs.get(namepart)
+                else:
+                    cver = new_pkgs.get(pkgname)
         else:
             cver = new_pkgs.get(pkgname)
             if not cver and pkgname in new_caps:
@@ -2610,7 +2631,17 @@ def _uninstall(
 
     changes = __salt__['pkg.{0}'.format(action)](name, pkgs=pkgs, version=version, **kwargs)
     new = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs)
-    failed = [x for x in pkg_params if x in new]
+    failed = []
+    for x in pkg_params:
+        if __grains__['os_family'] in ['Suse', 'RedHat']:
+            # Check if the package version set to be removed is actually removed:
+            if x in new and not pkg_params[x]:
+                failed.append(x)
+            elif x in new and pkg_params[x] in new[x]:
+                failed.append(x + "-" + pkg_params[x])
+        elif x in new:
+            failed.append(x)
+
     if action == 'purge':
         new_removed = __salt__['pkg.list_pkgs'](versions_as_list=True,
                                                 removed=True,
diff --git a/tests/unit/modules/test_yumpkg.py b/tests/unit/modules/test_yumpkg.py
index 9b2d39b4dd..74992071a1 100644
--- a/tests/unit/modules/test_yumpkg.py
+++ b/tests/unit/modules/test_yumpkg.py
@@ -602,3 +602,53 @@ class YumTestCase(TestCase, LoaderModuleMockMixin):
                      '--branch=foo', '--exclude=kernel*', 'upgrade'],
                     output_loglevel='trace',
                     python_shell=False)
+
+    def test_info_installed_with_all_versions(self):
+        '''
+        Test the return information of all versions for the named package(s), installed on the system.
+
+        :return:
+        '''
+        run_out = {
+            'virgo-dummy': [
+                {'build_date': '2015-07-09T10:55:19Z',
+                 'vendor': 'openSUSE Build Service',
+                 'description': 'This is the Virgo dummy package used for testing SUSE Manager',
+                 'license': 'GPL-2.0', 'build_host': 'sheep05', 'url': 'http://www.suse.com',
+                 'build_date_time_t': 1436432119, 'relocations': '(not relocatable)',
+                 'source_rpm': 'virgo-dummy-1.0-1.1.src.rpm', 'install_date': '2016-02-23T16:31:57Z',
+                 'install_date_time_t': 1456241517, 'summary': 'Virgo dummy package', 'version': '1.0',
+                 'signature': 'DSA/SHA1, Thu Jul  9 08:55:33 2015, Key ID 27fa41bd8a7c64f9',
+                 'release': '1.1', 'group': 'Applications/System', 'arch': 'i686', 'size': '17992'},
+                {'build_date': '2015-07-09T10:15:19Z',
+                 'vendor': 'openSUSE Build Service',
+                 'description': 'This is the Virgo dummy package used for testing SUSE Manager',
+                 'license': 'GPL-2.0', 'build_host': 'sheep05', 'url': 'http://www.suse.com',
+                 'build_date_time_t': 1436432119, 'relocations': '(not relocatable)',
+                 'source_rpm': 'virgo-dummy-1.0-1.1.src.rpm', 'install_date': '2016-02-23T16:31:57Z',
+                 'install_date_time_t': 14562415127, 'summary': 'Virgo dummy package', 'version': '1.0',
+                 'signature': 'DSA/SHA1, Thu Jul  9 08:55:33 2015, Key ID 27fa41bd8a7c64f9',
+                 'release': '1.1', 'group': 'Applications/System', 'arch': 'x86_64', 'size': '13124'}
+            ],
+            'libopenssl1_0_0': [
+                {'build_date': '2015-11-04T23:20:34Z', 'vendor': 'SUSE LLC <https://www.suse.com/>',
+                 'description': 'The OpenSSL Project is a collaborative effort.',
+                 'license': 'OpenSSL', 'build_host': 'sheep11', 'url': 'https://www.openssl.org/',
+                 'build_date_time_t': 1446675634, 'relocations': '(not relocatable)',
+                 'source_rpm': 'openssl-1.0.1i-34.1.src.rpm', 'install_date': '2016-02-23T16:31:35Z',
+                 'install_date_time_t': 1456241495, 'summary': 'Secure Sockets and Transport Layer Security',
+                 'version': '1.0.1i', 'signature': 'RSA/SHA256, Wed Nov  4 22:21:34 2015, Key ID 70af9e8139db7c82',
+                 'release': '34.1', 'group': 'Productivity/Networking/Security', 'packager': 'https://www.suse.com/',
+                 'arch': 'x86_64', 'size': '2576912'}
+            ]
+        }
+        with patch.dict(yumpkg.__salt__, {'lowpkg.info': MagicMock(return_value=run_out)}):
+            installed = yumpkg.info_installed(all_versions=True)
+            # Test overall products length
+            self.assertEqual(len(installed), 2)
+
+            # Test multiple versions for the same package
+            for pkg_name, pkg_info_list in installed.items():
+                self.assertEqual(len(pkg_info_list), 2 if pkg_name == "virgo-dummy" else 1)
+                for info in pkg_info_list:
+                    self.assertTrue(info['arch'] in ('x86_64', 'i686'))
diff --git a/tests/unit/modules/test_zypper.py b/tests/unit/modules/test_zypper.py
index 539a950252..6eccee568b 100644
--- a/tests/unit/modules/test_zypper.py
+++ b/tests/unit/modules/test_zypper.py
@@ -327,6 +327,56 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin):
             installed = zypper.info_installed()
             self.assertEqual(installed['vīrgô']['description'], 'vīrgô d€šçripţiǫñ')
 
+    def test_info_installed_with_all_versions(self):
+        '''
+        Test the return information of all versions for the named package(s), installed on the system.
+
+        :return:
+        '''
+        run_out = {
+            'virgo-dummy': [
+                {'build_date': '2015-07-09T10:55:19Z',
+                 'vendor': 'openSUSE Build Service',
+                 'description': 'This is the Virgo dummy package used for testing SUSE Manager',
+                 'license': 'GPL-2.0', 'build_host': 'sheep05', 'url': 'http://www.suse.com',
+                 'build_date_time_t': 1436432119, 'relocations': '(not relocatable)',
+                 'source_rpm': 'virgo-dummy-1.0-1.1.src.rpm', 'install_date': '2016-02-23T16:31:57Z',
+                 'install_date_time_t': 1456241517, 'summary': 'Virgo dummy package', 'version': '1.0',
+                 'signature': 'DSA/SHA1, Thu Jul  9 08:55:33 2015, Key ID 27fa41bd8a7c64f9',
+                 'release': '1.1', 'group': 'Applications/System', 'arch': 'i686', 'size': '17992'},
+                {'build_date': '2015-07-09T10:15:19Z',
+                 'vendor': 'openSUSE Build Service',
+                 'description': 'This is the Virgo dummy package used for testing SUSE Manager',
+                 'license': 'GPL-2.0', 'build_host': 'sheep05', 'url': 'http://www.suse.com',
+                 'build_date_time_t': 1436432119, 'relocations': '(not relocatable)',
+                 'source_rpm': 'virgo-dummy-1.0-1.1.src.rpm', 'install_date': '2016-02-23T16:31:57Z',
+                 'install_date_time_t': 14562415127, 'summary': 'Virgo dummy package', 'version': '1.0',
+                 'signature': 'DSA/SHA1, Thu Jul  9 08:55:33 2015, Key ID 27fa41bd8a7c64f9',
+                 'release': '1.1', 'group': 'Applications/System', 'arch': 'x86_64', 'size': '13124'}
+            ],
+            'libopenssl1_0_0': [
+                {'build_date': '2015-11-04T23:20:34Z', 'vendor': 'SUSE LLC <https://www.suse.com/>',
+                 'description': 'The OpenSSL Project is a collaborative effort.',
+                 'license': 'OpenSSL', 'build_host': 'sheep11', 'url': 'https://www.openssl.org/',
+                 'build_date_time_t': 1446675634, 'relocations': '(not relocatable)',
+                 'source_rpm': 'openssl-1.0.1i-34.1.src.rpm', 'install_date': '2016-02-23T16:31:35Z',
+                 'install_date_time_t': 1456241495, 'summary': 'Secure Sockets and Transport Layer Security',
+                 'version': '1.0.1i', 'signature': 'RSA/SHA256, Wed Nov  4 22:21:34 2015, Key ID 70af9e8139db7c82',
+                 'release': '34.1', 'group': 'Productivity/Networking/Security', 'packager': 'https://www.suse.com/',
+                 'arch': 'x86_64', 'size': '2576912'}
+            ]
+        }
+        with patch.dict(zypper.__salt__, {'lowpkg.info': MagicMock(return_value=run_out)}):
+            installed = zypper.info_installed(all_versions=True)
+            # Test overall products length
+            self.assertEqual(len(installed), 2)
+
+            # Test multiple versions for the same package
+            for pkg_name, pkg_info_list in installed.items():
+                self.assertEqual(len(pkg_info_list), 2 if pkg_name == "virgo-dummy" else 1)
+                for info in pkg_info_list:
+                    self.assertTrue(info['arch'] in ('x86_64', 'i686'))
+
     def test_info_available(self):
         '''
         Test return the information of the named package available for the system.
-- 
2.15.1


openSUSE Build Service is sponsored by