From c6503e510544dbbe3ae007468253f8d11bd71a16 Mon Sep 17 00:00:00 2001
From: "Gael Chamoulaud (Strider)" <gchamoul@redhat.com>
Date: Tue, 9 Feb 2021 11:31:32 +0100
Subject: [PATCH] Make check_latest_packages_version roles more generic

This patch removes reference to TripleO packages.

It also brings a better testing coverage through Molecule.

This patch also refactors the module itself, so it now process
the packages in one go, and reports results to ansible.

Futhermore, validation will now check for appropriate package
arhchitecture, instead of assuming they are all the same.

Validation now also determines the package manager itself,
assuming it isn't provided one by ansible.
Default fact gathering is thus no longer needed,
potentially saving storage and improving performance.

Change-Id: I5427523f6397f30538cdebabdfc22bc547c2580e
Signed-off-by: Gael Chamoulaud (Strider) <gchamoul@redhat.com>
Signed-off-by: Jiri Podivin <jpodivin@redhat.com>
---
 .../library/check_package_update.py           | 294 ++++++++++++++----
 .../defaults/main.yml                         |  10 +-
 .../molecule/default/converge.yml             |  61 +++-
 .../molecule/default/prepare.yml              |  39 ++-
 ....0-1.20210331045404.4c29590.el8.x86_64.rpm | Bin 0 -> 6992 bytes
 ....0-2.20210401064344.c8ee186.el8.x86_64.rpm | Bin 0 -> 7060 bytes
 ....1-1.20210401074356.drh345o.el8.x86_64.rpm | Bin 0 -> 7128 bytes
 .../tasks/main.yml                            |  11 +-
 .../library/test_check_package_update.py      | 255 +++++++++++++--
 9 files changed, 543 insertions(+), 127 deletions(-)
 create mode 100644 validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.0-1.20210331045404.4c29590.el8.x86_64.rpm
 create mode 100644 validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.0-2.20210401064344.c8ee186.el8.x86_64.rpm
 create mode 100644 validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.1-1.20210401074356.drh345o.el8.x86_64.rpm

diff --git a/validations_common/library/check_package_update.py b/validations_common/library/check_package_update.py
index aa273ed..2a0cc56 100644
--- a/validations_common/library/check_package_update.py
+++ b/validations_common/library/check_package_update.py
@@ -13,7 +13,23 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-""" Check for available updates for a given package."""
+"""Check for available updates for a given package.
+Module queries and parses output of at least two separate
+external binaries, in order to obtain information about
+supported package manager, installed and available packages.
+As such it has many points of failure.
+
+Information about supported package managers,
+such as the commands to use while working with them
+and the expected stderr output we can encounter while querying repos,
+are stored as a nested dictionery SUPPORTED_PKG_MGRS.
+With names of the supported package managers as keys
+of the first level elements. And the aformentioned information
+on the second level, as lists of strings, with self-explanatory keys.
+
+Formally speaking it is a tree of a sort.
+But so is entire python namespace.
+"""
 
 import collections
 import subprocess
@@ -24,21 +40,23 @@ from yaml import safe_load as yaml_safe_load
 DOCUMENTATION = '''
 ---
 module: check_package_update
-short_description: Check for available updates for a given package
+short_description: Check for available updates for given packages
 description:
-    - Check for available updates for a given package
+    - Check for available updates for given packages
 options:
-    package:
+    packages_list:
         required: true
         description:
-            - The name of the package you want to check
-        type: str
+            - The names of the packages you want to check
+        type: list
     pkg_mgr:
-        required: true
+        required: false
         description:
             - Supported Package Manager, DNF or YUM
         type: str
-author: "Florian Fuchs"
+author:
+    - Florian Fuchs
+    - Jiri Podivin (@jpodivin)
 '''
 
 EXAMPLES = '''
@@ -46,88 +64,239 @@ EXAMPLES = '''
   tasks:
     - name: Get available updates for packages
       check_package_update:
-        package: python-tripleoclient
-        pkg_mgr: "{{ ansible_pkg_mgr}}"
+        packages_list:
+          - coreutils
+          - wget
+        pkg_mgr: "{{ ansible_pkg_mgr }}"
 '''
 
-SUPPORTED_PKG_MGRS = (
-    'yum',
-    'dnf',
-)
+SUPPORTED_PKG_MGRS = {
+    'dnf': {
+        'query_installed': [
+            'rpm', '-qa', '--qf',
+            '%{NAME}|%{VERSION}|%{RELEASE}|%{ARCH}\n'
+        ],
+        'query_available': [
+            'dnf', '-q', 'list', '--available'
+        ],
+        'allowed_errors': [
+            '',
+            'Error: No matching Packages to list\n'
+        ]
+    },
+    'yum': {
+        'query_installed': [
+            'rpm', '-qa', '--qf',
+            '%{NAME}|%{VERSION}|%{RELEASE}|%{ARCH}\n'
+        ],
+        'query_available': [
+            'yum', '-q', 'list', 'available'
+        ],
+        'allowed_errors': [
+            '',
+            'Error: No matching Packages to list\n'
+        ]
+    },
+}
 
 
-PackageDetails = collections.namedtuple('PackageDetails',
-                                        ['name', 'version', 'release', 'arch'])
+PackageDetails = collections.namedtuple(
+    'PackageDetails',
+    ['name', 'version', 'release', 'arch'])
 
 
-def get_package_details(output):
-    if output:
-        return PackageDetails(
-            output.split('|')[0],
-            output.split('|')[1],
-            output.split('|')[2],
-            output.split('|')[3],
+def get_package_details(pkg_details_string):
+    """Returns PackageDetails namedtuple from given string.
+    Raises ValueError if the number of '|' separated
+    fields is < 4.
+    """
+    split_output = pkg_details_string.split('|')
+    try:
+        pkg_details = PackageDetails(
+            split_output[0],
+            split_output[1],
+            split_output[2],
+            split_output[3],
         )
+    except IndexError:
+        raise ValueError(
+            (
+                "Package description '{}' doesn't contain fields"
+                " required for processing."
+            ).format(pkg_details_string)
+        )
+
+    return pkg_details
+
+
+def _allowed_pkg_manager_stderr(stderr, allowed_errors):
+    """Returns False if the error message isn't in the
+    allowed_errors list.
+    This function factors out large, and possibly expanding,
+    condition so it doesn't cause too much confusion.
+    """
+
+    if stderr in allowed_errors:
+        return True
+    return False
 
 
 def _command(command):
-    # Return the result of a subprocess call
-    # as [stdout, stderr]
-    process = subprocess.Popen(command,
-                               stdout=subprocess.PIPE,
-                               stderr=subprocess.PIPE,
-                               universal_newlines=True)
+    """
+    :returns: the result of a subprocess call
+    as a tuple (stdout, stderr).
+    """
+    process = subprocess.Popen(
+        command,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        universal_newlines=True)
+
     return process.communicate()
 
 
-def check_update(module, package, pkg_mgr):
+def _get_pkg_manager(module):
+    """Return name of available package manager.
+    Queries binaries using `command -v`, in order defined by
+    the `SUPPORTED_PKG_MGRS`.
+    :returns: string
+    """
+    for possible_pkg_mgr in SUPPORTED_PKG_MGRS:
+
+        stdout, stderr = _command(['command', '-v', possible_pkg_mgr])
+        if stdout != '' and stderr == '':
+            return possible_pkg_mgr
+
+    module.fail_json(
+        msg=(
+            "None of the supported package managers '{}' seems to be "
+            "available on this system."
+        ).format(' '.join(SUPPORTED_PKG_MGRS))
+    )
+
+
+def _get_new_pkg_info(available_stdout):
+    """Return package information as dictionary. With package names
+    as keys and detailed information as list of strings.
+    """
+    available_stdout = available_stdout.split('\n')[1:]
+
+    available_stdout = [line.rstrip().split() for line in available_stdout]
+
+    new_pkgs_info = {}
+
+    for line in available_stdout:
+        if len(line) != 0:
+            new_pkgs_info[line[0]] = PackageDetails(
+                line[0],
+                line[1].split('-')[0],
+                line[1].split('-')[1],
+                line[0].split('.')[1])
+
+    return new_pkgs_info
+
+
+def _get_installed_pkgs(installed_stdout, packages, module):
+    """Return dictionary of installed packages.
+    Package names form keys and the output of the get_package_details
+    function values of the dictionary.
+    """
+    installed = {}
+    installed_stdout = installed_stdout.split('\n')[:-1]
+
+    for package in installed_stdout:
+        if package != '':
+            package = get_package_details(package)
+            if package.name in packages:
+                installed[package.name + '.' + package.arch] = package
+                packages.remove(package.name)
+        #Once find all the requested packages we don't need to continue search
+        if len(packages) == 0:
+            break
+
+    #Even a single missing package is a reason for failure.
+    if len(packages) > 0:
+        msg = "Following packages are not installed {}".format(packages)
+        module.fail_json(
+            msg=msg
+        )
+        return
+
+    return installed
+
+
+def check_update(module, packages_list, pkg_mgr):
+    """Check if the packages in the 'packages_list are up to date.
+    Queries binaries, defined the in relevant SUPPORTED_PKG_MGRS entry,
+    to obtain information about present and available packages.
+    """
+    if len(packages_list) == 0:
+        module.fail_json(
+            msg="No packages given to check.")
+        return
+
+    if pkg_mgr is None:
+        pkg_mgr = _get_pkg_manager(module=module)
     if pkg_mgr not in SUPPORTED_PKG_MGRS:
         module.fail_json(
             msg='Package manager "{}" is not supported.'.format(pkg_mgr))
         return
 
-    installed_stdout, installed_stderr = _command(
-        ['rpm', '-qa', '--qf',
-         '%{NAME}|%{VERSION}|%{RELEASE}|%{ARCH}',
-         package])
+    pkg_mgr = SUPPORTED_PKG_MGRS[pkg_mgr]
+
+    installed_stdout, installed_stderr = _command(pkg_mgr['query_installed'])
 
     # Fail the module if for some reason we can't lookup the current package.
     if installed_stderr != '':
         module.fail_json(msg=installed_stderr)
         return
-    elif not installed_stdout:
+    if not installed_stdout:
         module.fail_json(
-            msg='"{}" is not an installed package.'.format(package))
+            msg='no output returned for the query.{}'.format(
+                ' '.join(pkg_mgr['query_installed'])
+            ))
         return
 
-    installed = get_package_details(installed_stdout)
+    installed = _get_installed_pkgs(installed_stdout, packages_list, module)
 
-    pkg_mgr_option = 'available'
-    if pkg_mgr == 'dnf':
-        pkg_mgr_option = '--available'
+    installed_pkg_names = ' '.join(installed)
 
-    available_stdout, available_stderr = _command(
-        [pkg_mgr, '-q', 'list', pkg_mgr_option, installed.name])
+    pkg_mgr['query_available'].append(installed_pkg_names)
 
+    available_stdout, available_stderr = _command(pkg_mgr['query_available'])
+
+    #We need to check that the stderr consists only of the expected strings
+    #This can get complicated if the CLI on the pkg manager side changes.
+    if not _allowed_pkg_manager_stderr(available_stderr, pkg_mgr['allowed_errors']):
+        module.fail_json(msg=available_stderr)
+        return
     if available_stdout:
-        new_pkg_info = available_stdout.split('\n')[1].rstrip().split()[:2]
-        new_ver, new_rel = new_pkg_info[1].split('-')
-
-        module.exit_json(
-            changed=False,
-            name=installed.name,
-            current_version=installed.version,
-            current_release=installed.release,
-            new_version=new_ver,
-            new_release=new_rel)
+        new_pkgs_info = _get_new_pkg_info(available_stdout)
     else:
-        module.exit_json(
-            changed=False,
-            name=installed.name,
-            current_version=installed.version,
-            current_release=installed.release,
-            new_version=None,
-            new_release=None)
+        new_pkgs_info = {}
+
+    results = []
+
+    for installed_pkg in installed:
+
+        results.append(
+            {
+                'name': installed_pkg,
+                'current_version': installed[installed_pkg].version,
+                'current_release': installed[installed_pkg].release,
+                'new_version': None,
+                'new_release': None
+            }
+        )
+
+        if installed_pkg in new_pkgs_info:
+            results[-1]['new_version'] = new_pkgs_info[installed_pkg][1]
+            results[-1]['new_release'] = new_pkgs_info[installed_pkg][2]
+
+    module.exit_json(
+        changed=False,
+        outdated_pkgs=results
+    )
 
 
 def main():
@@ -135,9 +304,10 @@ def main():
         argument_spec=yaml_safe_load(DOCUMENTATION)['options']
     )
 
-    check_update(module,
-                 module.params.get('package'),
-                 module.params.get('pkg_mgr'))
+    check_update(
+        module,
+        packages_list=module.params.get('packages_list'),
+        pkg_mgr=module.params.get('pkg_mgr', None))
 
 
 if __name__ == '__main__':
diff --git a/validations_common/roles/check_latest_packages_version/defaults/main.yml b/validations_common/roles/check_latest_packages_version/defaults/main.yml
index c20e5ba..aa19ef6 100644
--- a/validations_common/roles/check_latest_packages_version/defaults/main.yml
+++ b/validations_common/roles/check_latest_packages_version/defaults/main.yml
@@ -1,10 +1,2 @@
 ---
-tripleoclient: >-
-  {%- if ansible_distribution == 'RedHat' and ansible_distribution_major_version == '8' -%}
-  python3-tripleoclient
-  {%- else -%}
-  python2-tripleoclient
-  {%- endif -%}
-
-packages_list:
-  - "{{ tripleoclient }}"
+packages_list: []
diff --git a/validations_common/roles/check_latest_packages_version/molecule/default/converge.yml b/validations_common/roles/check_latest_packages_version/molecule/default/converge.yml
index 79e3662..e93c29a 100644
--- a/validations_common/roles/check_latest_packages_version/molecule/default/converge.yml
+++ b/validations_common/roles/check_latest_packages_version/molecule/default/converge.yml
@@ -19,27 +19,64 @@
   hosts: all
 
   tasks:
-    - name: Validate No Available Update for patch rpm
-      include_role:
-        name: check_latest_packages_version
-      vars:
-        packages_list:
-          - patch
-
-    - name: Working Detection of Update for Pam package
+    - name: Run validation with empty package list
       block:
         - include_role:
             name: check_latest_packages_version
-          vars:
-            packages_list:
-              - pam
 
       rescue:
         - name: Clear host errors
           meta: clear_host_errors
 
         - debug:
-            msg: The validation works! End the playbook run
+            msg: |
+              The validation fails due to an empty package list
+              given as parameter.
+
+    - name: Working Detection of Update for valfrwk-release package
+      block:
+        - include_role:
+            name: check_latest_packages_version
+          vars:
+            packages_list:
+              - valfrwk-release
+
+      rescue:
+        - name: Clear host errors
+          meta: clear_host_errors
+
+        - debug:
+            msg: The validation has detected a new version!
+
+    - name: Validate No Available Update for valfrwk-release rpm
+      block:
+        - name: Update valfrwk-release rpm to the latest one
+          package:
+            name: valfrwk-release
+            state: latest
+
+        - include_role:
+            name: check_latest_packages_version
+          vars:
+            packages_list:
+              - valfrwk-release
+
+    - name: Working Detection of Update for an uninstalled package
+      block:
+        - include_role:
+            name: check_latest_packages_version
+          vars:
+            packages_list:
+              - whatchamacallit
+
+      rescue:
+        - name: Clear host errors
+          meta: clear_host_errors
+
+        - debug:
+            msg: |
+              The validation fails because 'whatchamacallit' rpm is not
+              installed! End the playbook run
 
         - name: End play
           meta: end_play
diff --git a/validations_common/roles/check_latest_packages_version/molecule/default/prepare.yml b/validations_common/roles/check_latest_packages_version/molecule/default/prepare.yml
index c55cfc7..2ca21ee 100644
--- a/validations_common/roles/check_latest_packages_version/molecule/default/prepare.yml
+++ b/validations_common/roles/check_latest_packages_version/molecule/default/prepare.yml
@@ -20,6 +20,41 @@
   gather_facts: false
 
   tasks:
-    - name: install patch rpm
+    - name: Create /opt/valfrwk directory
+      file:
+        path: '/opt/valfrwk'
+        state: directory
+        mode: "0777"
+
+    - name: Copy valfrwk-release packages
+      copy:
+        src: "{{ item }}"
+        dest: /opt/valfrwk
+      with_items:
+        - ./valfrwk-release-1.0.0-1.20210331045404.4c29590.el8.x86_64.rpm
+        - ./valfrwk-release-1.0.0-2.20210401064344.c8ee186.el8.x86_64.rpm
+        - ./valfrwk-release-1.0.1-1.20210401074356.drh345o.el8.x86_64.rpm
+
+    - name: Install createrepo rpm
       package:
-        name: patch
+        name: createrepo
+        state: present
+
+    - name: Generate yum repo
+      command: >-
+        createrepo /opt/valfrwk/
+      args:
+        warn: false
+
+    - name: Add valfrwk yum repository
+      yum_repository:
+        name: valfrwk
+        description: Validations Framework Repo
+        file: valfrwk
+        baseurl: file:///opt/valfrwk
+        gpgcheck: false
+
+    - name: Install the oldest valfrwk-release rpm
+      package:
+        name: valfrwk-release-1.0.0-1.20210331045404.4c29590.el8
+        state: present
diff --git a/validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.0-1.20210331045404.4c29590.el8.x86_64.rpm b/validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.0-1.20210331045404.4c29590.el8.x86_64.rpm
new file mode 100644
index 0000000000000000000000000000000000000000..c5ba21bee3d7971f1cc9f62baae1d2cfe2c9c14c
GIT binary patch
literal 6992
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
H+q&&PZ^U@n

literal 0
HcmV?d00001

diff --git a/validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.0-2.20210401064344.c8ee186.el8.x86_64.rpm b/validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.0-2.20210401064344.c8ee186.el8.x86_64.rpm
new file mode 100644
index 0000000000000000000000000000000000000000..4f86d07df08ba4b21b18190fd00683dc6d70d652
GIT binary patch
literal 7060
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
W|Fn;NxMKG1&px>Uny|8soBjh$v5*k}

literal 0
HcmV?d00001

diff --git a/validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.1-1.20210401074356.drh345o.el8.x86_64.rpm b/validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.1-1.20210401074356.drh345o.el8.x86_64.rpm
new file mode 100644
index 0000000000000000000000000000000000000000..9ba2086e8683ba45ec750e6ffa7c2838b391e3cf
GIT binary patch
literal 7128
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

literal 0
HcmV?d00001

diff --git a/validations_common/roles/check_latest_packages_version/tasks/main.yml b/validations_common/roles/check_latest_packages_version/tasks/main.yml
index 5750710..3d5e005 100644
--- a/validations_common/roles/check_latest_packages_version/tasks/main.yml
+++ b/validations_common/roles/check_latest_packages_version/tasks/main.yml
@@ -4,17 +4,10 @@
     gather_subset:
       - '!all'
       - '!min'
-      - pkg_mgr
-
-- name: Gather package facts
-  package_facts:
-    manager: auto
 
 - name: Get available updates for packages
   check_package_update:
-    package: "{{ item }}"
-    pkg_mgr: "{{ ansible_pkg_mgr }}"
-  with_items: "{{ packages_list }}"
+    packages_list: "{{ packages_list }}"
   register: updates
 
 - name: Check if current version is the latest one
@@ -23,5 +16,5 @@
       A newer version of the {{ item.name }} package is
       available: {{ item.new_version }}-{{ item.new_release }}
       (currently {{ item.current_version }}-{{ item.current_release }})
-  with_items: "{{ updates.results }}"
+  with_items: "{{ updates.outdated_pkgs }}"
   when: item.new_version
diff --git a/validations_common/tests/library/test_check_package_update.py b/validations_common/tests/library/test_check_package_update.py
index 173d0fb..65cb9ca 100644
--- a/validations_common/tests/library/test_check_package_update.py
+++ b/validations_common/tests/library/test_check_package_update.py
@@ -12,16 +12,16 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-from unittest.mock import MagicMock
-from unittest.mock import patch
+import subprocess
+from unittest import mock
+
+from validations_common.library import check_package_update as cppkg
 
-from validations_common.library.check_package_update import check_update
-from validations_common.library.check_package_update import get_package_details
 from validations_common.tests import base
 
 
 PKG_INSTALLED = "foo-package|6.1.5|1|x86_64"
-
+PKG_INVALID = "foo-package|6.1.5|x86_64"
 PKG_AVAILABLE = """\
 Available Packages
 foo-package.x86_64        8.0.0-1         foo-stable
@@ -31,65 +31,254 @@ foo-package.x86_64        8.0.0-1         foo-stable
 class TestGetPackageDetails(base.TestCase):
     def setUp(self):
         super(TestGetPackageDetails, self).setUp()
-        self.entry = get_package_details("foo-package|6.2.0|1|x86_64")
+        self.entry = PKG_INSTALLED
+        self.invalid_pkg = PKG_INVALID
 
     def test_name(self):
-        self.assertEqual(self.entry.name, 'foo-package')
+        details = cppkg.get_package_details(self.entry)
+        self.assertEqual(details.name, 'foo-package')
 
     def test_arch(self):
-        self.assertEqual(self.entry.arch, 'x86_64')
+        details = cppkg.get_package_details(self.entry)
+        self.assertEqual(details.arch, 'x86_64')
 
     def test_version(self):
-        self.assertEqual(self.entry.version, '6.2.0')
+        details = cppkg.get_package_details(self.entry)
+        self.assertEqual(details.version, '6.1.5')
 
     def test_release(self):
-        self.assertEqual(self.entry.release, '1')
+        details = cppkg.get_package_details(self.entry)
+        self.assertEqual(details.release, '1')
+
+    def test_index_error(self):
+        self.assertRaises(ValueError, cppkg.get_package_details, self.invalid_pkg)
 
 
 class TestCheckUpdate(base.TestCase):
     def setUp(self):
         super(TestCheckUpdate, self).setUp()
-        self.module = MagicMock()
+        self.module = mock.MagicMock()
+        self.package_details = cppkg.get_package_details("foo-package|6.1.5|1|x86_64")
+
+    def test_empty_pkg_list_fails(self):
+
+        cppkg.check_update(self.module, [], 'dnf')
+
+        self.module.fail_json.assert_called_once_with(
+            msg='No packages given to check.')
+
+        self.module.reset_mock()
 
     def test_unsupported_pkg_mgr_fails(self):
-        check_update(self.module, 'foo-package', 'apt')
+
+        cppkg.check_update(self.module, ['foo-package'], 'apt')
+
         self.module.fail_json.assert_called_with(
             msg='Package manager "apt" is not supported.')
 
-    @patch('validations_common.library.check_package_update._command')
+        self.module.reset_mock()
+
+    @mock.patch('validations_common.library.check_package_update._command')
     def test_fails_if_installed_package_not_found(self, mock_command):
         mock_command.side_effect = [
             ['', 'No package found.'],
         ]
-        check_update(self.module, 'foo-package', 'yum')
+
+        cppkg.check_update(self.module, ['foo-package'], 'yum')
+
         self.module.fail_json.assert_called_with(
             msg='No package found.')
 
-    @patch('validations_common.library.check_package_update._command')
-    def test_returns_current_and_available_versions(self, mock_command):
+        self.module.reset_mock()
+
+    @mock.patch(
+        'validations_common.library.check_package_update._get_new_pkg_info',
+        return_value={
+            'foo-package.x86_64': cppkg.PackageDetails(
+                'foo-package.x86_64',
+                '8.0.0',
+                '1',
+                'foo-stable')
+        }
+    )
+    @mock.patch(
+        'validations_common.library.check_package_update._get_installed_pkgs')
+    @mock.patch('validations_common.library.check_package_update._command')
+    def test_returns_current_and_available_versions(self, mock_command,
+                                                    mock_get_installed, mock_get_new_pkg_info):
+
         mock_command.side_effect = [
             [PKG_INSTALLED, ''],
             [PKG_AVAILABLE, ''],
         ]
 
-        check_update(self.module, 'foo-package', 'yum')
-        self.module.exit_json.assert_called_with(changed=False,
-                                                 name='foo-package',
-                                                 current_version='6.1.5',
-                                                 current_release='1',
-                                                 new_version='8.0.0',
-                                                 new_release='1')
+        mock_get_installed.side_effect = [{'foo-package.x86_64': self.package_details}]
 
-    @patch('validations_common.library.check_package_update._command')
-    def test_returns_current_version_if_no_updates(self, mock_command):
+        cppkg.check_update(self.module, ['foo-package'], 'yum')
+
+        mock_get_installed.assert_called_once_with(
+            PKG_INSTALLED,
+            ['foo-package'],
+            self.module)
+
+        self.module.exit_json.assert_called_with(
+            changed=False,
+            outdated_pkgs=[
+                {
+                    'name': 'foo-package.x86_64',
+                    'current_version': '6.1.5',
+                    'current_release': '1',
+                    'new_version': '8.0.0',
+                    'new_release': '1'
+                }
+            ])
+
+        self.module.reset_mock()
+
+    @mock.patch(
+        'validations_common.library.check_package_update._get_new_pkg_info',
+        return_value={
+            'foo-package.x86_64': cppkg.PackageDetails(
+                'foo-package.x86_64',
+                '8.0.0',
+                '1',
+                'foo-stable')
+        }
+    )
+    @mock.patch(
+        'validations_common.library.check_package_update._get_installed_pkgs')
+    @mock.patch('validations_common.library.check_package_update._command')
+    def test_returns_current_version_if_no_updates(self, mock_command,
+                                                   mock_get_installed, mock_get_new_pkg_info):
         mock_command.side_effect = [
             [PKG_INSTALLED, ''],
-            ['', 'No packages found'],
+            ['', 'Error: No matching Packages to list\n'],
         ]
-        check_update(self.module, 'foo-package', 'yum')
-        self.module.exit_json.assert_called_with(changed=False,
-                                                 name='foo-package',
-                                                 current_version='6.1.5',
-                                                 current_release='1',
-                                                 new_version=None,
-                                                 new_release=None)
+
+        mock_get_installed.side_effect = [{'foo-package.x86_64': self.package_details}]
+
+        cppkg.check_update(self.module, ['foo-package'], 'yum')
+
+        mock_get_installed.assert_called_once_with(
+            PKG_INSTALLED,
+            ['foo-package'],
+            self.module)
+
+        self.module.exit_json.assert_called_with(
+            changed=False,
+            outdated_pkgs=[
+                {
+                    'name': 'foo-package.x86_64',
+                    'current_version': '6.1.5',
+                    'current_release': '1',
+                    'new_version': None,
+                    'new_release': None
+                }
+            ])
+
+        self.module.reset_mock()
+
+    @mock.patch(
+        'validations_common.library.check_package_update.subprocess.PIPE')
+    @mock.patch(
+        'validations_common.library.check_package_update.subprocess.Popen')
+    def test_command_rpm_no_process(self, mock_popen, mock_pipe):
+
+        cli_command = [
+            'rpm',
+            '-qa',
+            '--qf',
+            '%{NAME}|%{VERSION}|%{RELEASE}|%{ARCH}\n'
+        ]
+
+        command_output = cppkg._command(cli_command)
+
+        mock_popen.assert_called_once_with(
+            cli_command,
+            stdout=mock_pipe,
+            stderr=mock_pipe,
+            universal_newlines=True)
+
+    def test_get_new_pkg_info(self):
+
+        pkg_info = cppkg._get_new_pkg_info(PKG_AVAILABLE)
+
+        self.assertIsInstance(pkg_info, dict)
+        self.assertTrue('foo-package.x86_64' in pkg_info)
+        self.assertIsInstance(pkg_info['foo-package.x86_64'], cppkg.PackageDetails)
+
+    @mock.patch('validations_common.library.check_package_update._command')
+    def test_get_pkg_mgr_fail(self, mock_command):
+
+        mock_command.side_effect = [
+            ('barSTDOUT', 'fooERROR'),
+            ('', '')
+        ]
+
+        pkg_manager = cppkg._get_pkg_manager(self.module)
+
+        self.assertEqual(pkg_manager, None)
+        self.module.fail_json.assert_called_once_with(msg=mock.ANY)
+
+        self.module.reset_mock()
+
+    @mock.patch('validations_common.library.check_package_update._command')
+    def test_get_pkg_mgr_succes_dnf(self, mock_command):
+        mock_command.side_effect = [('fizzSTDOUT', '')]
+
+        pkg_manager = cppkg._get_pkg_manager(self.module)
+
+        self.assertEqual(pkg_manager, 'dnf')
+        self.module.fail_json.assert_not_called()
+
+        self.module.reset_mock()
+
+    @mock.patch('validations_common.library.check_package_update._command')
+    def test_get_pkg_mgr_succes_yum(self, mock_command):
+        mock_command.side_effect = [
+            ('barSTDOUT', 'fooERROR'),
+            ('fizzSTDOUT', '')
+        ]
+
+        pkg_manager = cppkg._get_pkg_manager(self.module)
+
+        self.assertEqual(pkg_manager, 'yum')
+        self.module.fail_json.assert_not_called()
+
+        self.module.reset_mock()
+
+    def test_get_installed_pkgs_success(self):
+        """Test that _get_installed_pkgs will correctly process
+        output of rpm, compare it with provided package name list
+        and return dictionary of PackageDetails.
+        """
+
+        installed_pkgs = cppkg._get_installed_pkgs(
+            PKG_INSTALLED + '\n',
+            ['foo-package'],
+            self.module)
+
+        self.assertIsInstance(installed_pkgs, dict)
+        self.assertIsInstance(installed_pkgs['foo-package.x86_64'], cppkg.PackageDetails)
+        self.assertEqual(installed_pkgs['foo-package.x86_64'].name, 'foo-package')
+        self.assertEqual(installed_pkgs['foo-package.x86_64'].arch, 'x86_64')
+        self.assertEqual(installed_pkgs['foo-package.x86_64'].version, '6.1.5')
+        self.assertEqual(installed_pkgs['foo-package.x86_64'].release, '1')
+
+        self.module.fail_json.assert_not_called()
+
+        self.module.reset_mock()
+
+    def test_get_installed_pkgs_failure_pkg_missing(self):
+
+        cppkg._get_installed_pkgs(
+            installed_stdout=PKG_INSTALLED + '\n',
+            packages=['foo-package', 'bar-package'],
+            module=self.module
+        )
+
+        self.module.fail_json.assert_called_once_with(
+            msg="Following packages are not installed ['bar-package']"
+        )
+
+        self.module.reset_mock()