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 0000000..c5ba21b Binary files /dev/null and b/validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.0-1.20210331045404.4c29590.el8.x86_64.rpm differ 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 0000000..4f86d07 Binary files /dev/null and b/validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.0-2.20210401064344.c8ee186.el8.x86_64.rpm differ 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 0000000..9ba2086 Binary files /dev/null and b/validations_common/roles/check_latest_packages_version/molecule/default/valfrwk-release-1.0.1-1.20210401074356.drh345o.el8.x86_64.rpm differ 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()