From 6d95624c22d1c275974db1e7c94dcc171dc1457e Mon Sep 17 00:00:00 2001 From: Adam Miller Date: Fri, 17 Aug 2018 10:15:11 -0500 Subject: [PATCH] Refactor yum and dnf, add feature parity (#43621) * Refactor yum and dnf, add feature parity Signed-off-by: Adam Miller * remove unnecessary module_utils, move the classes into the module code Signed-off-by: Adam Miller * remove yum -> yum4, out of scope Signed-off-by: Adam Miller * use ABCMeta Signed-off-by: Adam Miller * re-arrange run() caller vs callee Signed-off-by: Adam Miller * make sanity checks happy Signed-off-by: Adam Miller * fix yum unit tests Signed-off-by: Adam Miller * remove unecessary debug statements, fix typo Signed-off-by: Adam Miller * fix licensing and attribution in yumdnf module_util Signed-off-by: Adam Miller * include fix from PR 40737 original commit 5cbda9658ab13d45a210c0581458327a4d975bcc original Author: Strahinja Kustudic yum will fail on 'No space left on device', fixes #32791 (#40737) During the installing of packages if yum runs out of free disk space, some post install scripts could fail (like e.g. when the kernel package generates initramfs), but yum would still exit with a status 0. This is bad, especially for the kernel package, because it makes it unable to boot. Because the yum module is usually used for automation, which means the users cannot read every message yum prints, it's better that the yum module fails if it detects that there is no free space on the disk. Signed-off-by: Adam Miller * Revert "fix licensing and attribution in yumdnf module_util" This reverts commit 59e11de5a2a6efa17ac3f0076bb162348c02e1bd. * move fetch_rpm_from_url out of yumdnf module_util Signed-off-by: Adam Miller * fix the move of fetch_rpm_from_url Signed-off-by: Adam Miller --- lib/ansible/module_utils/yumdnf.py | 99 + lib/ansible/modules/packaging/os/dnf.py | 865 ++++--- lib/ansible/modules/packaging/os/yum.py | 2232 +++++++++---------- test/integration/targets/dnf/tasks/dnf.yml | 219 +- test/units/modules/packaging/os/test_yum.py | 20 +- 5 files changed, 1940 insertions(+), 1495 deletions(-) create mode 100644 lib/ansible/module_utils/yumdnf.py diff --git a/lib/ansible/module_utils/yumdnf.py b/lib/ansible/module_utils/yumdnf.py new file mode 100644 index 0000000000..0d53914462 --- /dev/null +++ b/lib/ansible/module_utils/yumdnf.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# # Copyright: (c) 2012, Red Hat, Inc +# Written by Seth Vidal +# Contributing Authors: +# - Ansible Core Team +# - Eduard Snesarev (@verm666) +# - Berend De Schouwer (@berenddeschouwer) +# - Abhijeet Kasurde (@Akasurde) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +import os +import tempfile +from abc import ABCMeta, abstractmethod + +from ansible.module_utils._text import to_native +from ansible.module_utils.six import with_metaclass + +yumdnf_argument_spec = dict( + argument_spec=dict( + allow_downgrade=dict(type='bool', default=False), + autoremove=dict(type='bool', default=False), + bugfix=dict(required=False, type='bool', default=False), + conf_file=dict(type='str'), + disable_excludes=dict(type='str', default=None, choices=['all', 'main', 'repoid']), + disable_gpg_check=dict(type='bool', default=False), + disable_plugin=dict(type='list', default=[]), + disablerepo=dict(type='list', default=[]), + download_only=dict(type='bool', default=False), + enable_plugin=dict(type='list', default=[]), + enablerepo=dict(type='list', default=[]), + exclude=dict(type='list', default=[]), + installroot=dict(type='str', default="/"), + install_repoquery=dict(type='bool', default=True), + list=dict(type='str'), + name=dict(type='list', aliases=['pkg'], default=[]), + releasever=dict(default=None), + security=dict(type='bool', default=False), + skip_broken=dict(type='bool', default=False), + # removed==absent, installed==present, these are accepted as aliases + state=dict(type='str', default='present', choices=['absent', 'installed', 'latest', 'present', 'removed']), + update_cache=dict(type='bool', default=False, aliases=['expire-cache']), + update_only=dict(required=False, default="no", type='bool'), + validate_certs=dict(type='bool', default=True), + # this should not be needed, but exists as a failsafe + ), + required_one_of=[['name', 'list']], + mutually_exclusive=[['name', 'list']], + supports_check_mode=True, +) + + +class YumDnf(with_metaclass(ABCMeta, object)): + """ + Abstract class that handles the population of instance variables that should + be identical between both YUM and DNF modules because of the feature parity + and shared argument spec + """ + + def __init__(self, module): + + self.module = module + + self.allow_downgrade = self.module.params['allow_downgrade'] + self.autoremove = self.module.params['autoremove'] + self.bugfix = self.module.params['bugfix'] + self.conf_file = self.module.params['conf_file'] + self.disable_excludes = self.module.params['disable_excludes'] + self.disable_gpg_check = self.module.params['disable_gpg_check'] + self.disable_plugin = self.module.params['disable_plugin'] + self.disablerepo = self.module.params.get('disablerepo', []) + self.download_only = self.module.params['download_only'] + self.enable_plugin = self.module.params['enable_plugin'] + self.enablerepo = self.module.params.get('enablerepo', []) + self.exclude = self.module.params['exclude'] + self.installroot = self.module.params['installroot'] + self.install_repoquery = self.module.params['install_repoquery'] + self.list = self.module.params['list'] + self.names = [p.strip() for p in self.module.params['name']] + self.releasever = self.module.params['releasever'] + self.security = self.module.params['security'] + self.skip_broken = self.module.params['skip_broken'] + self.state = self.module.params['state'] + self.update_only = self.module.params['update_only'] + self.update_cache = self.module.params['update_cache'] + self.validate_certs = self.module.params['validate_certs'] + + # It's possible someone passed a comma separated string since it used + # to be a string type, so we should handle that + if self.enablerepo and len(self.enablerepo) == 1 and ',' in self.enablerepo: + self.enablerepo = self.module.params['enablerepo'].split(',') + if self.disablerepo and len(self.disablerepo) == 1 and ',' in self.disablerepo: + self.disablerepo = self.module.params['disablerepo'].split(',') + if self.exclude and len(self.exclude) == 1 and ',' in self.exclude: + self.exclude = self.module.params['exclude'].split(',') + + @abstractmethod + def run(self): + raise NotImplementedError diff --git a/lib/ansible/modules/packaging/os/dnf.py b/lib/ansible/modules/packaging/os/dnf.py index bdcbdee3e6..5b9bcf07e8 100644 --- a/lib/ansible/modules/packaging/os/dnf.py +++ b/lib/ansible/modules/packaging/os/dnf.py @@ -3,6 +3,7 @@ # Copyright 2015 Cristian van Ee # Copyright 2015 Igor Gnatenko +# Copyright 2018 Adam Miller # # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -88,6 +89,95 @@ options: type: bool default: false version_added: "2.4" + exclude: + description: + - Package name(s) to exclude when state=present, or latest. This can be a + list or a comma separated string. + version_added: "2.7" + skip_broken: + description: + - Skip packages with broken dependencies(devsolve) and are causing problems. + type: bool + default: "no" + version_added: "2.7" + update_cache: + description: + - Force yum to check if cache is out of date and redownload if needed. + Has an effect only if state is I(present) or I(latest). + type: bool + default: "no" + aliases: [ expire-cache ] + version_added: "2.7" + update_only: + description: + - When using latest, only update installed packages. Do not install packages. + - Has an effect only if state is I(latest) + required: false + default: "no" + type: bool + version_added: "2.7" + security: + description: + - If set to C(yes), and C(state=latest) then only installs updates that have been marked security related. + type: bool + default: "no" + version_added: "2.7" + bugfix: + description: + - If set to C(yes), and C(state=latest) then only installs updates that have been marked bugfix related. + required: false + default: "no" + type: bool + version_added: "2.7" + enable_plugin: + description: + - I(Plugin) name to enable for the install/update operation. + The enabled plugin will not persist beyond the transaction. + required: false + version_added: "2.7" + disable_plugin: + description: + - I(Plugin) name to disable for the install/update operation. + The disabled plugins will not persist beyond the transaction. + required: false + version_added: "2.7" + disable_excludes: + description: + - Disable the excludes defined in DNF config files. + - If set to C(all), disables all excludes. + - If set to C(main), disable excludes defined in [main] in yum.conf. + - If set to C(repoid), disable excludes defined for given repo id. + required: false + choices: [ all, main, repoid ] + version_added: "2.7" + validate_certs: + description: + - This only applies if using a https url as the source of the rpm. e.g. for localinstall. If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates as it avoids verifying the source site. + type: bool + default: "yes" + version_added: "2.7" + allow_downgrade: + description: + - This is effectively a no-op in DNF as it is the default behavior of dnf, but is an accepted parameter for feature + parity/compatibility with the I(yum) module. + type: bool + default: False + version_added: "2.7" + install_repoquery: + description: + - This is effectively a no-op in DNF as it is not needed with DNF, but is an accepted parameter for feature + parity/compatibility with the I(yum) module. + type: bool + default: True + version_added: "2.7" + download_only: + description: + - Only download the packages, do not install them. + required: false + default: "no" + type: bool + version_added: "2.7" notes: - When used with a `loop:` each package will be processed individually, it is much more efficient to pass the list directly to the `name` option. requirements: @@ -98,6 +188,7 @@ author: - '"Igor Gnatenko (@ignatenkobrain)" ' - '"Cristian van Ee (@DJMuggs)" ' - "Berend De Schouwer (github.com/berenddeschouwer)" + - '"Adam Miller (@maxamillion)" "' ''' EXAMPLES = ''' @@ -147,7 +238,9 @@ EXAMPLES = ''' state: absent autoremove: no ''' + import os +import tempfile try: import dnf @@ -160,378 +253,476 @@ try: except ImportError: HAS_DNF = False -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native +from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.urls import fetch_url from ansible.module_utils.six import PY2 from distutils.version import LooseVersion +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec -def _ensure_dnf(module): - if not HAS_DNF: - if PY2: - package = 'python2-dnf' - else: - package = 'python3-dnf' +# 64k. Number of bytes to read at a time when manually downloading pkgs via a url +BUFSIZE = 65536 - if module.check_mode: - module.fail_json(msg="`{0}` is not installed, but it is required" - "for the Ansible dnf module.".format(package)) - module.run_command(['dnf', 'install', '-y', package], check_rc=True) - global dnf +class DnfModule(YumDnf): + """ + DNF Ansible module back-end implementation + """ + + def __init__(self, module): + # This populates instance vars for all argument spec params + super(DnfModule, self).__init__(module) + + self._ensure_dnf() + + def fetch_rpm_from_url(self, spec): + # FIXME: Remove this once this PR is merged: + # https://github.com/ansible/ansible/pull/19172 + + # download package so that we can query it + package_name, dummy = os.path.splitext(str(spec.rsplit('/', 1)[1])) + package_file = tempfile.NamedTemporaryFile(dir=self.module.tmpdir, prefix=package_name, suffix='.rpm', delete=False) + self.module.add_cleanup_file(package_file.name) try: - import dnf - import dnf.cli - import dnf.const - import dnf.exceptions - import dnf.subject - import dnf.util - except ImportError: - module.fail_json(msg="Could not import the dnf python module. " - "Please install `{0}` package.".format(package)) + rsp, info = fetch_url(self.module, spec) + if not rsp: + self.module.fail_json(msg="Failure downloading %s, %s" % (spec, info['msg'])) + data = rsp.read(BUFSIZE) + while data: + package_file.write(data) + data = rsp.read(BUFSIZE) + package_file.close() + except Exception as e: + self.module.fail_json(msg="Failure downloading %s, %s" % (spec, to_native(e))) + return package_file.name -def _configure_base(module, base, conf_file, disable_gpg_check, installroot='/', releasever=None): - """Configure the dnf Base object.""" - conf = base.conf - - # Turn off debug messages in the output - conf.debuglevel = 0 - - # Set whether to check gpg signatures - conf.gpgcheck = not disable_gpg_check - - # Don't prompt for user confirmations - conf.assumeyes = True - - # Set installroot - conf.installroot = installroot - - # Set releasever - if releasever is not None: - conf.substitutions['releasever'] = releasever - - # Change the configuration file path if provided - if conf_file: - # Fail if we can't read the configuration file. - if not os.access(conf_file, os.R_OK): - module.fail_json( - msg="cannot read configuration file", conf_file=conf_file) - else: - conf.config_file_path = conf_file - - # Read the configuration file - conf.read() - - -def _specify_repositories(base, disablerepo, enablerepo): - """Enable and disable repositories matching the provided patterns.""" - base.read_all_repos() - repos = base.repos - - # Disable repositories - for repo_pattern in disablerepo: - for repo in repos.get_matching(repo_pattern): - repo.disable() - - # Enable repositories - for repo_pattern in enablerepo: - for repo in repos.get_matching(repo_pattern): - repo.enable() - - -def _base(module, conf_file, disable_gpg_check, disablerepo, enablerepo, installroot, releasever): - """Return a fully configured dnf Base object.""" - base = dnf.Base() - _configure_base(module, base, conf_file, disable_gpg_check, installroot, releasever) - _specify_repositories(base, disablerepo, enablerepo) - base.fill_sack(load_system_repo='auto') - return base - - -def _package_dict(package): - """Return a dictionary of information for the package.""" - # NOTE: This no longer contains the 'dnfstate' field because it is - # already known based on the query type. - result = { - 'name': package.name, - 'arch': package.arch, - 'epoch': str(package.epoch), - 'release': package.release, - 'version': package.version, - 'repo': package.repoid} - result['nevra'] = '{epoch}:{name}-{version}-{release}.{arch}'.format( - **result) - - return result - - -def list_items(module, base, command): - """List package info based on the command.""" - # Rename updates to upgrades - if command == 'updates': - command = 'upgrades' - - # Return the corresponding packages - if command in ['installed', 'upgrades', 'available']: - results = [ - _package_dict(package) - for package in getattr(base.sack.query(), command)()] - # Return the enabled repository ids - elif command in ['repos', 'repositories']: - results = [ - {'repoid': repo.id, 'state': 'enabled'} - for repo in base.repos.iter_enabled()] - # Return any matching packages - else: - packages = dnf.subject.Subject(command).get_best_query(base.sack) - results = [_package_dict(package) for package in packages] - - module.exit_json(results=results) - - -def _mark_package_install(module, base, pkg_spec): - """Mark the package for install.""" - try: - base.install(pkg_spec) - except dnf.exceptions.MarkingError: - module.fail_json(msg="No package {0} available.".format(pkg_spec)) - - -def _parse_spec_group_file(names): - pkg_specs, grp_specs, filenames = [], [], [] - for name in names: - if name.endswith(".rpm"): - filenames.append(name) - elif name.startswith("@"): - grp_specs.append(name[1:]) - else: - pkg_specs.append(name) - return pkg_specs, grp_specs, filenames - - -def _install_remote_rpms(base, filenames): - if int(dnf.__version__.split(".")[0]) >= 2: - pkgs = list(sorted(base.add_remote_rpms(list(filenames)), reverse=True)) - else: - pkgs = [] - for filename in filenames: - pkgs.append(base.add_remote_rpm(filename)) - for pkg in pkgs: - base.package_install(pkg) - - -def ensure(module, base, state, names, autoremove): - # Accumulate failures. Package management modules install what they can - # and fail with a message about what they can't. - failures = [] - allow_erasing = False - - # Autoremove is called alone - # Jump to remove path where base.autoremove() is run - if not names and autoremove: - names = [] - state = 'absent' - - if names == ['*'] and state == 'latest': - base.upgrade_all() - else: - pkg_specs, group_specs, filenames = _parse_spec_group_file(names) - if group_specs: - base.read_comps() - - pkg_specs = [p.strip() for p in pkg_specs] - filenames = [f.strip() for f in filenames] - groups = [] - environments = [] - for group_spec in (g.strip() for g in group_specs): - group = base.comps.group_by_pattern(group_spec) - if group: - groups.append(group.id) + def _ensure_dnf(self): + if not HAS_DNF: + if PY2: + package = 'python2-dnf' else: - environment = base.comps.environment_by_pattern(group_spec) - if environment: - environments.append(environment.id) - else: - module.fail_json( - msg="No group {0} available.".format(group_spec)) + package = 'python3-dnf' - if state in ['installed', 'present']: - # Install files. - _install_remote_rpms(base, filenames) + if self.module.check_mode: + self.module.fail_json( + msg="`{0}` is not installed, but it is required" + "for the Ansible dnf module.".format(package) + ) - # Install groups. - for group in groups: - try: - base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) - except dnf.exceptions.Error as e: - # In dnf 2.0 if all the mandatory packages in a group do - # not install, an error is raised. We want to capture - # this but still install as much as possible. - failures.append((group, to_native(e))) + self.module.run_command(['dnf', 'install', '-y', package], check_rc=True) + global dnf + try: + import dnf + import dnf.cli + import dnf.const + import dnf.exceptions + import dnf.subject + import dnf.util + except ImportError: + self.module.fail_json( + msg="Could not import the dnf python module. " + "Please install `{0}` package.".format(package) + ) - for environment in environments: - try: - base.environment_install(environment, dnf.const.GROUP_PACKAGE_TYPES) - except dnf.exceptions.Error as e: - failures.append((environment, to_native(e))) + def _configure_base(self, base, conf_file, disable_gpg_check, installroot='/'): + """Configure the dnf Base object.""" - # Install packages. - for pkg_spec in pkg_specs: - _mark_package_install(module, base, pkg_spec) + if self.enable_plugin and self.disable_plugin: + base.init_plugins(self.disable_plugin, self.enable_plugin) + elif self.enable_plugin: + base.init_plugins(enable_plugins=self.enable_plugin) + elif self.disable_plugin: + base.init_plugins(self.disable_plugin) - elif state == 'latest': - # "latest" is same as "installed" for filenames. - _install_remote_rpms(base, filenames) + conf = base.conf - for group in groups: - try: - try: - base.group_upgrade(group) - except dnf.exceptions.CompsError: - # If not already installed, try to install. - base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) - except dnf.exceptions.Error as e: - failures.append((group, to_native(e))) + # Turn off debug messages in the output + conf.debuglevel = 0 - for environment in environments: - try: - try: - base.environment_upgrade(environment) - except dnf.exceptions.CompsError: - # If not already installed, try to install. - base.environment_install(environment, dnf.const.GROUP_PACKAGE_TYPES) - except dnf.exceptions.Error as e: - failures.append((environment, to_native(e))) + # Set whether to check gpg signatures + conf.gpgcheck = not disable_gpg_check - for pkg_spec in pkg_specs: - # best effort causes to install the latest package - # even if not previously installed - base.conf.best = True - try: - base.install(pkg_spec) - except dnf.exceptions.MarkingError as e: - failures.append((pkg_spec, to_native(e))) + # Don't prompt for user confirmations + conf.assumeyes = True + # Set installroot + conf.installroot = installroot + + # Set excludes + if self.exclude: + conf.exclude(self.exclude) + + # Set disable_excludes + if self.disable_excludes: + conf.disable_excludes = [self.disable_excludes] + + # Set releasever + if self.releasever is not None: + conf.substitutions['releasever'] = self.releasever + + # Set skip_broken (in dnf this is strict=0) + if self.skip_broken: + conf.strict = 0 + + if self.download_only: + conf.downloadonly = True + + # Change the configuration file path if provided + if conf_file: + # Fail if we can't read the configuration file. + if not os.access(conf_file, os.R_OK): + self.module.fail_json( + msg="cannot read configuration file", conf_file=conf_file) + else: + conf.config_file_path = conf_file + + # Read the configuration file + conf.read() + + def _specify_repositories(self, base, disablerepo, enablerepo): + """Enable and disable repositories matching the provided patterns.""" + base.read_all_repos() + repos = base.repos + + # Disable repositories + for repo_pattern in disablerepo: + for repo in repos.get_matching(repo_pattern): + repo.disable() + + # Enable repositories + for repo_pattern in enablerepo: + for repo in repos.get_matching(repo_pattern): + repo.enable() + + def _base(self, conf_file, disable_gpg_check, disablerepo, enablerepo, installroot): + """Return a fully configured dnf Base object.""" + base = dnf.Base() + self._configure_base(base, conf_file, disable_gpg_check, installroot) + self._specify_repositories(base, disablerepo, enablerepo) + base.fill_sack(load_system_repo='auto') + if self.bugfix: + key = {'advisory_type__eq': 'bugfix'} + base._update_security_filters = [base.sack.query().filter(**key)] + if self.security: + key = {'advisory_type__eq': 'security'} + base._update_security_filters = [base.sack.query().filter(**key)] + if self.update_cache: + base.update_cache() + return base + + def _package_dict(self, package): + """Return a dictionary of information for the package.""" + # NOTE: This no longer contains the 'dnfstate' field because it is + # already known based on the query type. + result = { + 'name': package.name, + 'arch': package.arch, + 'epoch': str(package.epoch), + 'release': package.release, + 'version': package.version, + 'repo': package.repoid} + result['nevra'] = '{epoch}:{name}-{version}-{release}.{arch}'.format( + **result) + + return result + + def list_items(self, command): + """List package info based on the command.""" + # Rename updates to upgrades + if command == 'updates': + command = 'upgrades' + + # Return the corresponding packages + if command in ['installed', 'upgrades', 'available']: + results = [ + self._package_dict(package) + for package in getattr(self.base.sack.query(), command)()] + # Return the enabled repository ids + elif command in ['repos', 'repositories']: + results = [ + {'repoid': repo.id, 'state': 'enabled'} + for repo in self.base.repos.iter_enabled()] + # Return any matching packages else: - # state == absent - if autoremove: - base.conf.clean_requirements_on_remove = autoremove + packages = dnf.subject.Subject(command).get_best_query(self.base.sack) + results = [self._package_dict(package) for package in packages] - if filenames: - module.fail_json( - msg="Cannot remove paths -- please specify package name.") + self.module.exit_json(results=results) - for group in groups: - try: - base.group_remove(group) - except dnf.exceptions.CompsError: - # Group is already uninstalled. - pass + def _mark_package_install(self, pkg_spec): + """Mark the package for install.""" + try: + self.base.install(pkg_spec) + except dnf.exceptions.MarkingError: + self.module.fail_json(msg="No package {0} available.".format(pkg_spec)) - for environment in environments: - try: - base.environment_remove(environment) - except dnf.exceptions.CompsError: - # Environment is already uninstalled. - pass + def _parse_spec_group_file(self): + pkg_specs, grp_specs, filenames = [], [], [] + for name in self.names: + if name.endswith(".rpm"): + if '://' in name: + name = self.fetch_rpm_from_url(name) + filenames.append(name) + elif name.startswith("@"): + grp_specs.append(name[1:]) + else: + pkg_specs.append(name) + return pkg_specs, grp_specs, filenames - installed = base.sack.query().installed() - for pkg_spec in pkg_specs: - if installed.filter(name=pkg_spec): - base.remove(pkg_spec) + def _update_only(self, pkgs): + installed = self.base.sack.query().installed() + for pkg in pkgs: + if installed.filter(name=pkg): + self.base.package_upgrade(pkg) - # Like the dnf CLI we want to allow recursive removal of dependent - # packages - allow_erasing = True + def _install_remote_rpms(self, filenames): + if int(dnf.__version__.split(".")[0]) >= 2: + pkgs = list(sorted(self.base.add_remote_rpms(list(filenames)), reverse=True)) + else: + pkgs = [] + for filename in filenames: + pkgs.append(self.base.add_remote_rpm(filename)) + if self.update_only: + self._update_only(pkgs) + else: + for pkg in pkgs: + self.base.package_install(pkg) - if autoremove: - base.autoremove() + def ensure(self): + # Accumulate failures. Package management modules install what they can + # and fail with a message about what they can't. + failures = [] + allow_erasing = False - if not base.resolve(allow_erasing=allow_erasing): - if failures: - module.fail_json(msg='Failed to install some of the ' - 'specified packages', - failures=failures) - module.exit_json(msg="Nothing to do") - else: - if module.check_mode: + # Autoremove is called alone + # Jump to remove path where base.autoremove() is run + if not self.names and self.autoremove: + self.names = [] + self.state = 'absent' + + if self.names == ['*'] and self.state == 'latest': + self.base.upgrade_all() + else: + pkg_specs, group_specs, filenames = self._parse_spec_group_file() + if group_specs: + self.base.read_comps() + + pkg_specs = [p.strip() for p in pkg_specs] + filenames = [f.strip() for f in filenames] + groups = [] + environments = [] + for group_spec in (g.strip() for g in group_specs): + group = self.base.comps.group_by_pattern(group_spec) + if group: + groups.append(group.id) + else: + environment = self.base.comps.environment_by_pattern(group_spec) + if environment: + environments.append(environment.id) + else: + self.module.fail_json( + msg="No group {0} available.".format(group_spec)) + + if self.state in ['installed', 'present']: + # Install files. + self._install_remote_rpms(filenames) + + # Install groups. + for group in groups: + try: + self.base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) + except dnf.exceptions.Error as e: + # In dnf 2.0 if all the mandatory packages in a group do + # not install, an error is raised. We want to capture + # this but still install as much as possible. + failures.append((group, to_native(e))) + + for environment in environments: + try: + self.base.environment_install(environment, dnf.const.GROUP_PACKAGE_TYPES) + except dnf.exceptions.Error as e: + failures.append((environment, to_native(e))) + + # Install packages. + if self.update_only: + self._update_only(pkg_specs) + else: + for pkg_spec in pkg_specs: + self._mark_package_install(pkg_spec) + + elif self.state == 'latest': + # "latest" is same as "installed" for filenames. + self._install_remote_rpms(filenames) + + for group in groups: + try: + try: + self.base.group_upgrade(group) + except dnf.exceptions.CompsError: + # If not already installed, try to install. + self.base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) + except dnf.exceptions.Error as e: + failures.append((group, to_native(e))) + + for environment in environments: + try: + try: + self.base.environment_upgrade(environment) + except dnf.exceptions.CompsError: + # If not already installed, try to install. + self.base.environment_install(environment, dnf.const.GROUP_PACKAGE_TYPES) + except dnf.exceptions.Error as e: + failures.append((environment, to_native(e))) + + if self.update_only: + self._update_only(pkg_specs) + else: + for pkg_spec in pkg_specs: + # best effort causes to install the latest package + # even if not previously installed + self.base.conf.best = True + try: + self.base.install(pkg_spec) + except dnf.exceptions.MarkingError as e: + failures.append((pkg_spec, to_native(e))) + + else: + # state == absent + if self.autoremove: + self.base.conf.clean_requirements_on_remove = self.autoremove + + if filenames: + self.module.fail_json( + msg="Cannot remove paths -- please specify package name.") + + for group in groups: + try: + self.base.group_remove(group) + except dnf.exceptions.CompsError: + # Group is already uninstalled. + pass + + for environment in environments: + try: + self.base.environment_remove(environment) + except dnf.exceptions.CompsError: + # Environment is already uninstalled. + pass + + installed = self.base.sack.query().installed() + for pkg_spec in pkg_specs: + if installed.filter(name=pkg_spec): + self.base.remove(pkg_spec) + + # Like the dnf CLI we want to allow recursive removal of dependent + # packages + allow_erasing = True + + if self.autoremove: + self.base.autoremove() + + if not self.base.resolve(allow_erasing=allow_erasing): if failures: - module.fail_json(msg='Failed to install some of the ' - 'specified packages', - failures=failures) - module.exit_json(changed=True) + self.module.fail_json( + msg='Failed to install some of the specified packages', + failures=failures + ) + self.module.exit_json(msg="Nothing to do") + else: + if self.module.check_mode: + if failures: + self.module.fail_json( + msg='Failed to install some of the specified packages', + failures=failures + ) + self.module.exit_json(changed=True) - base.download_packages(base.transaction.install_set) - base.do_transaction() - response = {'changed': True, 'results': []} - for package in base.transaction.install_set: - response['results'].append("Installed: {0}".format(package)) - for package in base.transaction.remove_set: - response['results'].append("Removed: {0}".format(package)) + try: + self.base.download_packages(self.base.transaction.install_set) + except dnf.exceptions.DownloadError as e: + self.module.fail_json(msg="Failed to download packages: {0}".format(to_text(e))) - if failures: - module.fail_json(msg='Failed to install some of the ' - 'specified packages', - failures=failures) - module.exit_json(**response) + response = {'changed': True, 'results': []} + if self.download_only: + for package in self.base.transaction.install_set: + response['results'].append("Downloaded: {0}".format(package)) + self.module.exit_json(**response) + else: + self.base.do_transaction() + for package in self.base.transaction.install_set: + response['results'].append("Installed: {0}".format(package)) + for package in self.base.transaction.remove_set: + response['results'].append("Removed: {0}".format(package)) + + if failures: + self.module.fail_json( + msg='Failed to install some of the specified packages', + failures=failures + ) + self.module.exit_json(**response) + + @staticmethod + def has_dnf(): + return HAS_DNF + + def run(self): + """The main function.""" + + # Check if autoremove is called correctly + if self.autoremove: + if LooseVersion(dnf.__version__) < LooseVersion('2.0.1'): + self.module.fail_json(msg="Autoremove requires dnf>=2.0.1. Current dnf version is %s" % dnf.__version__) + if self.state not in ["absent", None]: + self.module.fail_json(msg="Autoremove should be used alone or with state=absent") + + # Set state as installed by default + # This is not set in AnsibleModule() because the following shouldn't happend + # - dnf: autoremove=yes state=installed + if self.state is None: + self.state = 'installed' + + if self.list: + self.base = self._base( + self.conf_file, self.disable_gpg_check, self.disablerepo, + self.enablerepo, self.installroot + ) + self.list_items(self.module, self.list) + else: + # Note: base takes a long time to run so we want to check for failure + # before running it. + if not dnf.util.am_i_root(): + self.module.fail_json(msg="This command has to be run under the root user.") + self.base = self._base( + self.conf_file, self.disable_gpg_check, self.disablerepo, + self.enablerepo, self.installroot + ) + + self.ensure() def main(): - """The main function.""" + # state=installed name=pkgspec + # state=removed name=pkgspec + # state=latest name=pkgspec + # + # informational commands: + # list=installed + # list=updates + # list=available + # list=repos + # list=pkgspec + module = AnsibleModule( - argument_spec=dict( - name=dict(aliases=['pkg'], type='list'), - state=dict( - choices=['absent', 'present', 'installed', 'removed', 'latest'], - default='present', - ), - enablerepo=dict(type='list', default=[]), - disablerepo=dict(type='list', default=[]), - list=dict(), - conf_file=dict(default=None, type='path'), - disable_gpg_check=dict(default=False, type='bool'), - installroot=dict(default='/', type='path'), - autoremove=dict(type='bool', default=False), - releasever=dict(default=None), - ), - required_one_of=[['name', 'list', 'autoremove']], - mutually_exclusive=[['name', 'list'], ['autoremove', 'list']], - supports_check_mode=True) - params = module.params + **yumdnf_argument_spec + ) - _ensure_dnf(module) - - # Check if autoremove is called correctly - if params['autoremove']: - if LooseVersion(dnf.__version__) < LooseVersion('2.0.1'): - module.fail_json(msg="Autoremove requires dnf>=2.0.1. Current dnf version is %s" % dnf.__version__) - if params['state'] not in ["absent", None]: - module.fail_json(msg="Autoremove should be used alone or with state=absent") - - # Set state as installed by default - # This is not set in AnsibleModule() because the following shouldn't happend - # - dnf: autoremove=yes state=installed - if params['state'] is None: - params['state'] = 'installed' - - if params['list']: - base = _base( - module, params['conf_file'], params['disable_gpg_check'], - params['disablerepo'], params['enablerepo'], params['installroot'], - params['releasever']) - list_items(module, base, params['list']) - else: - # Note: base takes a long time to run so we want to check for failure - # before running it. - if not dnf.util.am_i_root(): - module.fail_json(msg="This command has to be run under the root user.") - base = _base( - module, params['conf_file'], params['disable_gpg_check'], - params['disablerepo'], params['enablerepo'], params['installroot'], - params['releasever']) - - ensure(module, base, params['state'], params['name'], params['autoremove']) + module_implementation = DnfModule(module) + try: + module_implementation.run() + except dnf.exceptions.RepoError as de: + module.exit_json(msg="Failed to synchronize repodata: {0}".format(de)) if __name__ == '__main__': diff --git a/lib/ansible/modules/packaging/os/yum.py b/lib/ansible/modules/packaging/os/yum.py index 1c2f9d6680..2fe93fb7a0 100644 --- a/lib/ansible/modules/packaging/os/yum.py +++ b/lib/ansible/modules/packaging/os/yum.py @@ -53,12 +53,16 @@ options: - I(Repoid) of repositories to enable for the install/update operation. These repos will not persist beyond the transaction. When specifying multiple repos, separate them with a C(","). + - As of Ansible 2.7, this can alternatively be a list instead of C(",") + separated string version_added: "0.9" disablerepo: description: - I(Repoid) of repositories to disable for the install/update operation. These repos will not persist beyond the transaction. When specifying multiple repos, separate them with a C(","). + - As of Ansible 2.7, this can alternatively be a list instead of C(",") + separated string version_added: "0.9" conf_file: description: @@ -146,6 +150,22 @@ options: The disabled plugins will not persist beyond the transaction. required: false version_added: "2.5" + releasever: + description: + - Specifies an alternative release from which all packages will be + installed. + required: false + version_added: "2.7" + default: null + autoremove: + description: + - If C(yes), removes all "leaf" packages from the system that were originally + installed as dependencies of user-installed packages but which are no longer + required by any such package. Should be used alone or when state is I(absent) + - "NOTE: This feature requires yum >= 3.4.3 (RHEL/CentOS 7+)" + type: bool + default: false + version_added: "2.7" disable_excludes: description: - Disable the excludes defined in YUM config files. @@ -191,6 +211,7 @@ author: - Eduard Snesarev (@verm666) - Berend De Schouwer (@berenddeschouwer) - Abhijeet Kasurde (@Akasurde) + - Adam Miller (@maxamillion) ''' EXAMPLES = ''' @@ -285,6 +306,11 @@ EXAMPLES = ''' download_only: true ''' +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible.module_utils.urls import fetch_url +from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec + import os import re import tempfile @@ -310,1152 +336,1157 @@ except ImportError: from contextlib import contextmanager -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible.module_utils.urls import fetch_url +def_qf = "%{epoch}:%{name}-%{version}-%{release}.%{arch}" +rpmbin = None # 64k. Number of bytes to read at a time when manually downloading pkgs via a url BUFSIZE = 65536 -def_qf = "%{epoch}:%{name}-%{version}-%{release}.%{arch}" -rpmbin = None +class YumModule(YumDnf): + """ + Yum Ansible module back-end implementation + """ -def yum_base(conf_file=None, installroot='/', enabled_plugins=None, - disabled_plugins=None, disable_excludes=None): - my = yum.YumBase() - my.preconf.debuglevel = 0 - my.preconf.errorlevel = 0 - my.preconf.plugins = True - my.preconf.enabled_plugins = enabled_plugins - my.preconf.disabled_plugins = disabled_plugins - # my.preconf.releasever = '/' - if installroot != '/': - # do not setup installroot by default, because of error - # CRITICAL:yum.cli:Config Error: Error accessing file for config file:////etc/yum.conf - # in old yum version (like in CentOS 6.6) - my.preconf.root = installroot - my.conf.installroot = installroot - if conf_file and os.path.exists(conf_file): - my.preconf.fn = conf_file - if os.geteuid() != 0: - if hasattr(my, 'setCacheDir'): - my.setCacheDir() - else: - cachedir = yum.misc.getCacheDir() - my.repos.setCacheDir(cachedir) - my.conf.cache = 0 - if disable_excludes: - my.conf.disable_excludes = disable_excludes + def __init__(self, module): - return my + # state=installed name=pkgspec + # state=removed name=pkgspec + # state=latest name=pkgspec + # + # informational commands: + # list=installed + # list=updates + # list=available + # list=repos + # list=pkgspec + # This populates instance vars for all argument spec params + super(YumModule, self).__init__(module) -def ensure_yum_utils(module): - repoquerybin = module.get_bin_path('repoquery', required=False) + def fetch_rpm_from_url(self, spec): + # FIXME: Remove this once this PR is merged: + # https://github.com/ansible/ansible/pull/19172 - if module.params['install_repoquery'] and not repoquerybin and not module.check_mode: - yum_path = module.get_bin_path('yum') - if yum_path: - module.run_command('%s -y install yum-utils' % yum_path) - repoquerybin = module.get_bin_path('repoquery', required=False) - - return repoquerybin - - -def fetch_rpm_from_url(spec, module=None): - # download package so that we can query it - package_name, _ = os.path.splitext(str(spec.rsplit('/', 1)[1])) - package_file = tempfile.NamedTemporaryFile(dir=module.tmpdir, prefix=package_name, suffix='.rpm', delete=False) - module.add_cleanup_file(package_file.name) - try: - rsp, info = fetch_url(module, spec) - if not rsp: - module.fail_json(msg="Failure downloading %s, %s" % (spec, info['msg'])) - data = rsp.read(BUFSIZE) - while data: - package_file.write(data) + # download package so that we can query it + package_name, dummy = os.path.splitext(str(spec.rsplit('/', 1)[1])) + package_file = tempfile.NamedTemporaryFile(dir=self.module.tmpdir, prefix=package_name, suffix='.rpm', delete=False) + self.module.add_cleanup_file(package_file.name) + try: + rsp, info = fetch_url(self.module, spec) + if not rsp: + self.module.fail_json(msg="Failure downloading %s, %s" % (spec, info['msg'])) data = rsp.read(BUFSIZE) - package_file.close() - except Exception as e: - if module: - module.fail_json(msg="Failure downloading %s, %s" % (spec, to_native(e))) + while data: + package_file.write(data) + data = rsp.read(BUFSIZE) + package_file.close() + except Exception as e: + self.module.fail_json(msg="Failure downloading %s, %s" % (spec, to_native(e))) + + return package_file.name + + def yum_base(self): + my = yum.YumBase() + my.preconf.debuglevel = 0 + my.preconf.errorlevel = 0 + my.preconf.plugins = True + my.preconf.enabled_plugins = self.enable_plugin + my.preconf.disabled_plugins = self.disable_plugin + if self.releasever: + my.preconf.releasever = self.releasever + if self.installroot != '/': + # do not setup installroot by default, because of error + # CRITICAL:yum.cli:Config Error: Error accessing file for config file:////etc/yum.conf + # in old yum version (like in CentOS 6.6) + my.preconf.root = self.installroot + my.conf.installroot = self.installroot + if self.conf_file and os.path.exists(self.conf_file): + my.preconf.fn = self.conf_file + if os.geteuid() != 0: + if hasattr(my, 'setCacheDir'): + my.setCacheDir() + else: + cachedir = yum.misc.getCacheDir() + my.repos.setCacheDir(cachedir) + my.conf.cache = 0 + if self.disable_excludes: + my.conf.disable_excludes = self.disable_excludes + + return my + + def po_to_envra(self, po): + if hasattr(po, 'ui_envra'): + return po.ui_envra + + return '%s:%s-%s-%s.%s' % (po.epoch, po.name, po.version, po.release, po.arch) + + def is_group_env_installed(self, name): + name_lower = name.lower() + + my = self.yum_base() + if yum.__version_info__ >= (3, 4): + groups_list = my.doGroupLists(return_evgrps=True) else: - raise e + groups_list = my.doGroupLists() - return package_file.name - - -def po_to_envra(po): - if hasattr(po, 'ui_envra'): - return po.ui_envra - - return '%s:%s-%s-%s.%s' % (po.epoch, po.name, po.version, po.release, po.arch) - - -def is_group_env_installed(name, conf_file, en_plugins=None, dis_plugins=None, - installroot='/', disable_excludes=None): - name_lower = name.lower() - - my = yum_base(conf_file, installroot, en_plugins, dis_plugins, disable_excludes) - if yum.__version_info__ >= (3, 4): - groups_list = my.doGroupLists(return_evgrps=True) - else: - groups_list = my.doGroupLists() - - # list of the installed groups on the first index - groups = groups_list[0] - for group in groups: - if name_lower.endswith(group.name.lower()) or name_lower.endswith(group.groupid.lower()): - return True - - if yum.__version_info__ >= (3, 4): - # list of the installed env_groups on the third index - envs = groups_list[2] - for env in envs: - if name_lower.endswith(env.name.lower()) or name_lower.endswith(env.environmentid.lower()): + # list of the installed groups on the first index + groups = groups_list[0] + for group in groups: + if name_lower.endswith(group.name.lower()) or name_lower.endswith(group.groupid.lower()): return True - return False + if yum.__version_info__ >= (3, 4): + # list of the installed env_groups on the third index + envs = groups_list[2] + for env in envs: + if name_lower.endswith(env.name.lower()) or name_lower.endswith(env.environmentid.lower()): + return True + return False -def is_installed(module, repoq, pkgspec, conf_file, qf=None, en_repos=None, dis_repos=None, en_plugins=None, dis_plugins=None, is_pkg=False, - installroot='/', disable_excludes=None): - if en_repos is None: - en_repos = [] - if dis_repos is None: - dis_repos = [] - if qf is None: - qf = "%{epoch}:%{name}-%{version}-%{release}.%{arch}\n" + def is_installed(self, repoq, pkgspec, qf=None, is_pkg=False): + if qf is None: + qf = "%{epoch}:%{name}-%{version}-%{release}.%{arch}\n" - if not repoq: - pkgs = [] - try: - my = yum_base(conf_file, installroot, en_plugins, dis_plugins, disable_excludes) - for rid in dis_repos: - my.repos.disableRepo(rid) - for rid in en_repos: - my.repos.enableRepo(rid) + if not repoq: + pkgs = [] + try: + my = self.yum_base() + for rid in self.disablerepo: + my.repos.disableRepo(rid) + for rid in self.enablerepo: + my.repos.enableRepo(rid) - e, m, _ = my.rpmdb.matchPackageNames([pkgspec]) - pkgs = e + m + e, m, _ = my.rpmdb.matchPackageNames([pkgspec]) + pkgs = e + m + if not pkgs and not is_pkg: + pkgs.extend(my.returnInstalledPackagesByDep(pkgspec)) + except Exception as e: + self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) + + return [self.po_to_envra(p) for p in pkgs] + + else: + global rpmbin + if not rpmbin: + rpmbin = self.module.get_bin_path('rpm', required=True) + + cmd = [rpmbin, '-q', '--qf', qf, pkgspec] + if self.installroot != '/': + cmd.extend(['--root', self.installroot]) + # rpm localizes messages and we're screen scraping so make sure we use + # the C locale + lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') + rc, out, err = self.module.run_command(cmd, environ_update=lang_env) + if rc != 0 and 'is not installed' not in out: + self.module.fail_json(msg='Error from rpm: %s: %s' % (cmd, err)) + if 'is not installed' in out: + out = '' + + pkgs = [p for p in out.replace('(none)', '0').split('\n') if p.strip()] if not pkgs and not is_pkg: - pkgs.extend(my.returnInstalledPackagesByDep(pkgspec)) - except Exception as e: - module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) + cmd = [rpmbin, '-q', '--qf', qf, '--whatprovides', pkgspec] + if self.installroot != '/': + cmd.extend(['--root', self.installroot]) + rc2, out2, err2 = self.module.run_command(cmd, environ_update=lang_env) + else: + rc2, out2, err2 = (0, '', '') - return [po_to_envra(p) for p in pkgs] + if rc2 != 0 and 'no package provides' not in out2: + self.module.fail_json(msg='Error from rpm: %s: %s' % (cmd, err + err2)) + if 'no package provides' in out2: + out2 = '' + pkgs += [p for p in out2.replace('(none)', '0').split('\n') if p.strip()] + return pkgs - else: - global rpmbin - if not rpmbin: - rpmbin = module.get_bin_path('rpm', required=True) + return [] - cmd = [rpmbin, '-q', '--qf', qf, pkgspec] - if installroot != '/': - cmd.extend(['--root', installroot]) - # rpm localizes messages and we're screen scraping so make sure we use - # the C locale - lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') - rc, out, err = module.run_command(cmd, environ_update=lang_env) - if rc != 0 and 'is not installed' not in out: - module.fail_json(msg='Error from rpm: %s: %s' % (cmd, err)) - if 'is not installed' in out: - out = '' + def is_available(self, repoq, pkgspec, qf=def_qf): + if not repoq: - pkgs = [p for p in out.replace('(none)', '0').split('\n') if p.strip()] - if not pkgs and not is_pkg: - cmd = [rpmbin, '-q', '--qf', qf, '--whatprovides', pkgspec] - if installroot != '/': - cmd.extend(['--root', installroot]) - rc2, out2, err2 = module.run_command(cmd, environ_update=lang_env) - else: - rc2, out2, err2 = (0, '', '') + pkgs = [] + try: + my = self.yum_base() + for rid in self.disablerepo: + my.repos.disableRepo(rid) + for rid in self.enablerepo: + my.repos.enableRepo(rid) - if rc2 != 0 and 'no package provides' not in out2: - module.fail_json(msg='Error from rpm: %s: %s' % (cmd, err + err2)) - if 'no package provides' in out2: - out2 = '' - pkgs += [p for p in out2.replace('(none)', '0').split('\n') if p.strip()] - return pkgs - - return [] - - -def is_available(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=None, dis_repos=None, en_plugins=None, dis_plugins=None, - installroot='/', disable_excludes=None): - if en_repos is None: - en_repos = [] - if dis_repos is None: - dis_repos = [] - - if not repoq: - - pkgs = [] - try: - my = yum_base(conf_file, installroot, en_plugins, dis_plugins, disable_excludes) - for rid in dis_repos: - my.repos.disableRepo(rid) - for rid in en_repos: - my.repos.enableRepo(rid) - - e, m, _ = my.pkgSack.matchPackageNames([pkgspec]) - pkgs = e + m - if not pkgs: - pkgs.extend(my.returnPackagesByDep(pkgspec)) - except Exception as e: - module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) - - return [po_to_envra(p) for p in pkgs] - - else: - myrepoq = list(repoq) - - r_cmd = ['--disablerepo', ','.join(dis_repos)] - myrepoq.extend(r_cmd) - - r_cmd = ['--enablerepo', ','.join(en_repos)] - myrepoq.extend(r_cmd) - - cmd = myrepoq + ["--qf", qf, pkgspec] - rc, out, err = module.run_command(cmd) - if rc == 0: - return [p for p in out.split('\n') if p.strip()] - else: - module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err)) - - return [] - - -def is_update(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=None, dis_repos=None, en_plugins=None, dis_plugins=None, - installroot='/', disable_excludes=None): - if en_repos is None: - en_repos = [] - if dis_repos is None: - dis_repos = [] - - if not repoq: - - pkgs = [] - updates = [] - - try: - my = yum_base(conf_file, installroot, en_plugins, dis_plugins, disable_excludes) - for rid in dis_repos: - my.repos.disableRepo(rid) - for rid in en_repos: - my.repos.enableRepo(rid) - - pkgs = my.returnPackagesByDep(pkgspec) + my.returnInstalledPackagesByDep(pkgspec) - if not pkgs: e, m, _ = my.pkgSack.matchPackageNames([pkgspec]) pkgs = e + m - updates = my.doPackageLists(pkgnarrow='updates').updates - except Exception as e: - module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) + if not pkgs: + pkgs.extend(my.returnPackagesByDep(pkgspec)) + except Exception as e: + self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) - retpkgs = (pkg for pkg in pkgs if pkg in updates) + return [self.po_to_envra(p) for p in pkgs] - return set(po_to_envra(p) for p in retpkgs) + else: + myrepoq = list(repoq) - else: - myrepoq = list(repoq) - r_cmd = ['--disablerepo', ','.join(dis_repos)] - myrepoq.extend(r_cmd) + r_cmd = ['--disablerepo', ','.join(self.disablerepo)] + myrepoq.extend(r_cmd) - r_cmd = ['--enablerepo', ','.join(en_repos)] - myrepoq.extend(r_cmd) + r_cmd = ['--enablerepo', ','.join(self.enablerepo)] + myrepoq.extend(r_cmd) - cmd = myrepoq + ["--pkgnarrow=updates", "--qf", qf, pkgspec] - rc, out, err = module.run_command(cmd) + cmd = myrepoq + ["--qf", qf, pkgspec] + rc, out, err = self.module.run_command(cmd) + if rc == 0: + return [p for p in out.split('\n') if p.strip()] + else: + self.module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err)) + return [] + + def is_update(self, repoq, pkgspec, qf=def_qf): + if not repoq: + + pkgs = [] + updates = [] + + try: + my = self.yum_base() + for rid in self.disablerepo: + my.repos.disableRepo(rid) + for rid in self.enablerepo: + my.repos.enableRepo(rid) + + pkgs = my.returnPackagesByDep(pkgspec) + my.returnInstalledPackagesByDep(pkgspec) + if not pkgs: + e, m, _ = my.pkgSack.matchPackageNames([pkgspec]) + pkgs = e + m + updates = my.doPackageLists(pkgnarrow='updates').updates + except Exception as e: + self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) + + retpkgs = (pkg for pkg in pkgs if pkg in updates) + + return set(self.po_to_envra(p) for p in retpkgs) + + else: + myrepoq = list(repoq) + r_cmd = ['--disablerepo', ','.join(self.disablerepo)] + myrepoq.extend(r_cmd) + + r_cmd = ['--enablerepo', ','.join(self.enablerepo)] + myrepoq.extend(r_cmd) + + cmd = myrepoq + ["--pkgnarrow=updates", "--qf", qf, pkgspec] + rc, out, err = self.module.run_command(cmd) + + if rc == 0: + return set(p for p in out.split('\n') if p.strip()) + else: + self.module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err)) + + return set() + + def what_provides(self, repoq, req_spec, qf=def_qf): + if not repoq: + + pkgs = [] + try: + my = self.yum_base() + for rid in self.disablerepo: + my.repos.disableRepo(rid) + for rid in self.enablerepo: + my.repos.enableRepo(rid) + + try: + pkgs = my.returnPackagesByDep(req_spec) + my.returnInstalledPackagesByDep(req_spec) + except Exception as e: + # If a repo with `repo_gpgcheck=1` is added and the repo GPG + # key was never accepted, quering this repo will throw an + # error: 'repomd.xml signature could not be verified'. In that + # situation we need to run `yum -y makecache` which will accept + # the key and try again. + if 'repomd.xml signature could not be verified' in to_native(e): + self.module.run_command(self.yum_basecmd + ['makecache']) + pkgs = my.returnPackagesByDep(req_spec) + my.returnInstalledPackagesByDep(req_spec) + else: + raise + if not pkgs: + e, m, _ = my.pkgSack.matchPackageNames([req_spec]) + pkgs.extend(e) + pkgs.extend(m) + e, m, _ = my.rpmdb.matchPackageNames([req_spec]) + pkgs.extend(e) + pkgs.extend(m) + except Exception as e: + self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) + + return set(self.po_to_envra(p) for p in pkgs) + + else: + myrepoq = list(repoq) + r_cmd = ['--disablerepo', ','.join(self.disablerepo)] + myrepoq.extend(r_cmd) + + r_cmd = ['--enablerepo', ','.join(self.enablerepo)] + myrepoq.extend(r_cmd) + + cmd = myrepoq + ["--qf", qf, "--whatprovides", req_spec] + rc, out, err = self.module.run_command(cmd) + cmd = myrepoq + ["--qf", qf, req_spec] + rc2, out2, err2 = self.module.run_command(cmd) + if rc == 0 and rc2 == 0: + out += out2 + pkgs = set([p for p in out.split('\n') if p.strip()]) + if not pkgs: + pkgs = self.is_installed(repoq, req_spec, qf=qf) + return pkgs + else: + self.module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2)) + + return set() + + def transaction_exists(self, pkglist): + """ + checks the package list to see if any packages are + involved in an incomplete transaction + """ + + conflicts = [] + if not transaction_helpers: + return conflicts + + # first, we create a list of the package 'nvreas' + # so we can compare the pieces later more easily + pkglist_nvreas = (splitFilename(pkg) for pkg in pkglist) + + # next, we build the list of packages that are + # contained within an unfinished transaction + unfinished_transactions = find_unfinished_transactions() + for trans in unfinished_transactions: + steps = find_ts_remaining(trans) + for step in steps: + # the action is install/erase/etc., but we only + # care about the package spec contained in the step + (action, step_spec) = step + (n, v, r, e, a) = splitFilename(step_spec) + # and see if that spec is in the list of packages + # requested for installation/updating + for pkg in pkglist_nvreas: + # if the name and arch match, we're going to assume + # this package is part of a pending transaction + # the label is just for display purposes + label = "%s-%s" % (n, a) + if n == pkg[0] and a == pkg[4]: + if label not in conflicts: + conflicts.append("%s-%s" % (n, a)) + break + return conflicts + + def local_envra(self, path): + """return envra of a local rpm passed in""" + + ts = rpm.TransactionSet() + ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) + fd = os.open(path, os.O_RDONLY) + try: + header = ts.hdrFromFdno(fd) + except rpm.error as e: + return None + finally: + os.close(fd) + + return '%s:%s-%s-%s.%s' % ( + header[rpm.RPMTAG_EPOCH] or '0', + header[rpm.RPMTAG_NAME], + header[rpm.RPMTAG_VERSION], + header[rpm.RPMTAG_RELEASE], + header[rpm.RPMTAG_ARCH] + ) + + @contextmanager + def set_env_proxy(self): + # setting system proxy environment and saving old, if exists + my = self.yum_base() + namepass = "" + scheme = ["http", "https"] + old_proxy_env = [os.getenv("http_proxy"), os.getenv("https_proxy")] + try: + if my.conf.proxy: + if my.conf.proxy_username: + namepass = namepass + my.conf.proxy_username + if my.conf.proxy_password: + namepass = namepass + ":" + my.conf.proxy_password + namepass = namepass + '@' + for item in scheme: + os.environ[item + "_proxy"] = re.sub( + r"(http://)", + r"\1" + namepass, my.conf.proxy + ) + yield + except yum.Errors.YumBaseError: + raise + finally: + # revert back to previously system configuration + for item in scheme: + if os.getenv("{0}_proxy".format(item)): + del os.environ["{0}_proxy".format(item)] + if old_proxy_env[0]: + os.environ["http_proxy"] = old_proxy_env[0] + if old_proxy_env[1]: + os.environ["https_proxy"] = old_proxy_env[1] + + def pkg_to_dict(self, pkgstr): + if pkgstr.strip(): + n, e, v, r, a, repo = pkgstr.split('|') + else: + return {'error_parsing': pkgstr} + + d = { + 'name': n, + 'arch': a, + 'epoch': e, + 'release': r, + 'version': v, + 'repo': repo, + 'envra': '%s:%s-%s-%s.%s' % (e, n, v, r, a) + } + + if repo == 'installed': + d['yumstate'] = 'installed' + else: + d['yumstate'] = 'available' + + return d + + def repolist(self, repoq, qf="%{repoid}"): + cmd = repoq + ["--qf", qf, "-a"] + rc, out, _ = self.module.run_command(cmd) if rc == 0: return set(p for p in out.split('\n') if p.strip()) else: - module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err)) - - return set() - - -def what_provides(module, repoq, yum_basecmd, req_spec, conf_file, qf=def_qf, - en_repos=None, dis_repos=None, en_plugins=None, - dis_plugins=None, installroot='/', disable_excludes=None): - if en_repos is None: - en_repos = [] - if dis_repos is None: - dis_repos = [] - - if not repoq: - - pkgs = [] - try: - my = yum_base(conf_file, installroot, en_plugins, dis_plugins, disable_excludes) - for rid in dis_repos: - my.repos.disableRepo(rid) - for rid in en_repos: - my.repos.enableRepo(rid) - - try: - pkgs = my.returnPackagesByDep(req_spec) + my.returnInstalledPackagesByDep(req_spec) - except Exception as e: - # If a repo with `repo_gpgcheck=1` is added and the repo GPG - # key was never accepted, quering this repo will throw an - # error: 'repomd.xml signature could not be verified'. In that - # situation we need to run `yum -y makecache` which will accept - # the key and try again. - if 'repomd.xml signature could not be verified' in to_native(e): - module.run_command(yum_basecmd + ['makecache']) - pkgs = my.returnPackagesByDep(req_spec) + my.returnInstalledPackagesByDep(req_spec) - else: - raise - if not pkgs: - e, m, _ = my.pkgSack.matchPackageNames([req_spec]) - pkgs.extend(e) - pkgs.extend(m) - e, m, _ = my.rpmdb.matchPackageNames([req_spec]) - pkgs.extend(e) - pkgs.extend(m) - except Exception as e: - module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) - - return set(po_to_envra(p) for p in pkgs) - - else: - myrepoq = list(repoq) - r_cmd = ['--disablerepo', ','.join(dis_repos)] - myrepoq.extend(r_cmd) - - r_cmd = ['--enablerepo', ','.join(en_repos)] - myrepoq.extend(r_cmd) - - cmd = myrepoq + ["--qf", qf, "--whatprovides", req_spec] - rc, out, err = module.run_command(cmd) - cmd = myrepoq + ["--qf", qf, req_spec] - rc2, out2, err2 = module.run_command(cmd) - if rc == 0 and rc2 == 0: - out += out2 - pkgs = set(p for p in out.split('\n') if p.strip()) - if not pkgs: - pkgs = is_installed(module, repoq, req_spec, conf_file, qf=qf, installroot=installroot, disable_excludes=disable_excludes) - return pkgs - else: - module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2)) - - return set() - - -def transaction_exists(pkglist): - """ - checks the package list to see if any packages are - involved in an incomplete transaction - """ - - conflicts = [] - if not transaction_helpers: - return conflicts - - # first, we create a list of the package 'nvreas' - # so we can compare the pieces later more easily - pkglist_nvreas = (splitFilename(pkg) for pkg in pkglist) - - # next, we build the list of packages that are - # contained within an unfinished transaction - unfinished_transactions = find_unfinished_transactions() - for trans in unfinished_transactions: - steps = find_ts_remaining(trans) - for step in steps: - # the action is install/erase/etc., but we only - # care about the package spec contained in the step - (action, step_spec) = step - (n, v, r, e, a) = splitFilename(step_spec) - # and see if that spec is in the list of packages - # requested for installation/updating - for pkg in pkglist_nvreas: - # if the name and arch match, we're going to assume - # this package is part of a pending transaction - # the label is just for display purposes - label = "%s-%s" % (n, a) - if n == pkg[0] and a == pkg[4]: - if label not in conflicts: - conflicts.append("%s-%s" % (n, a)) - break - return conflicts - - -def local_envra(path): - """return envra of a local rpm passed in""" - - ts = rpm.TransactionSet() - ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) - fd = os.open(path, os.O_RDONLY) - try: - header = ts.hdrFromFdno(fd) - except rpm.error as e: - return None - finally: - os.close(fd) - - return '%s:%s-%s-%s.%s' % (header[rpm.RPMTAG_EPOCH] or '0', - header[rpm.RPMTAG_NAME], - header[rpm.RPMTAG_VERSION], - header[rpm.RPMTAG_RELEASE], - header[rpm.RPMTAG_ARCH]) - - -@contextmanager -def set_env_proxy(conf_file, installroot): - # setting system proxy environment and saving old, if exists - my = yum_base(conf_file, installroot) - namepass = "" - scheme = ["http", "https"] - old_proxy_env = [os.getenv("http_proxy"), os.getenv("https_proxy")] - try: - if my.conf.proxy: - if my.conf.proxy_username: - namepass = namepass + my.conf.proxy_username - if my.conf.proxy_password: - namepass = namepass + ":" + my.conf.proxy_password - namepass = namepass + '@' - for item in scheme: - os.environ[item + "_proxy"] = re.sub(r"(http://)", - r"\1" + namepass, my.conf.proxy) - yield - except yum.Errors.YumBaseError: - raise - finally: - # revert back to previously system configuration - for item in scheme: - if os.getenv("{0}_proxy".format(item)): - del os.environ["{0}_proxy".format(item)] - if old_proxy_env[0]: - os.environ["http_proxy"] = old_proxy_env[0] - if old_proxy_env[1]: - os.environ["https_proxy"] = old_proxy_env[1] - - -def pkg_to_dict(pkgstr): - if pkgstr.strip(): - n, e, v, r, a, repo = pkgstr.split('|') - else: - return {'error_parsing': pkgstr} - - d = { - 'name': n, - 'arch': a, - 'epoch': e, - 'release': r, - 'version': v, - 'repo': repo, - 'envra': '%s:%s-%s-%s.%s' % (e, n, v, r, a) - } - - if repo == 'installed': - d['yumstate'] = 'installed' - else: - d['yumstate'] = 'available' - - return d - - -def repolist(module, repoq, qf="%{repoid}"): - cmd = repoq + ["--qf", qf, "-a"] - rc, out, _ = module.run_command(cmd) - if rc == 0: - return set(p for p in out.split('\n') if p.strip()) - else: - return [] - - -def list_stuff(module, repoquerybin, conf_file, stuff, installroot='/', disablerepo='', enablerepo='', disable_excludes=None): - - qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}" - # is_installed goes through rpm instead of repoquery so it needs a slightly different format - is_installed_qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|installed\n" - repoq = [repoquerybin, '--show-duplicates', '--plugins', '--quiet'] - if disablerepo: - repoq.extend(['--disablerepo', disablerepo]) - if enablerepo: - repoq.extend(['--enablerepo', enablerepo]) - if installroot != '/': - repoq.extend(['--installroot', installroot]) - if conf_file and os.path.exists(conf_file): - repoq += ['-c', conf_file] - - if stuff == 'installed': - return [pkg_to_dict(p) for p in sorted(is_installed(module, repoq, '-a', conf_file, qf=is_installed_qf, - installroot=installroot, disable_excludes=disable_excludes)) if p.strip()] - - if stuff == 'updates': - return [pkg_to_dict(p) for p in sorted(is_update(module, repoq, '-a', conf_file, qf=qf, - installroot=installroot, disable_excludes=disable_excludes)) if p.strip()] - - if stuff == 'available': - return [pkg_to_dict(p) for p in sorted(is_available(module, repoq, '-a', conf_file, qf=qf, - installroot=installroot, disable_excludes=disable_excludes)) if p.strip()] - - if stuff == 'repos': - return [dict(repoid=name, state='enabled') for name in sorted(repolist(module, repoq)) if name.strip()] - - return [pkg_to_dict(p) for p in sorted(is_installed(module, repoq, stuff, conf_file, qf=is_installed_qf, - installroot=installroot, disable_excludes=disable_excludes) + - is_available(module, repoq, stuff, conf_file, qf=qf, installroot=installroot, - disable_excludes=disable_excludes)) if p.strip()] - - -def exec_install(module, items, action, pkgs, res, yum_basecmd): - cmd = yum_basecmd + [action] + pkgs - - if module.check_mode: - module.exit_json(changed=True, results=res['results'], changes=dict(installed=pkgs)) - - lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') - rc, out, err = module.run_command(cmd, environ_update=lang_env) - - if rc == 1: - for spec in items: - # Fail on invalid urls: - if ('://' in spec and ('No package %s available.' % spec in out or 'Cannot open: %s. Skipping.' % spec in err)): - err = 'Package at %s could not be installed' % spec - module.fail_json(changed=False, msg=err, rc=rc) - - res['rc'] = rc - res['results'].append(out) - res['msg'] += err - res['changed'] = True - - if ('Nothing to do' in out and rc == 0) or ('does not have any packages' in err): - res['changed'] = False - - if rc != 0: - res['changed'] = False - module.fail_json(**res) - - # Fail if yum prints 'No space left on device' because that means some - # packages failed executing their post install scripts because of lack of - # free space (e.g. kernel package couldn't generate initramfs). Note that - # yum can still exit with rc=0 even if some post scripts didn't execute - # correctly. - if 'No space left on device' in (out or err): - res['changed'] = False - res['msg'] = 'No space left on device' - module.fail_json(**res) - - # FIXME - if we did an install - go and check the rpmdb to see if it actually installed - # look for each pkg in rpmdb - # look for each pkg via obsoletes - - return res - - -def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos, en_plugins, dis_plugins, installroot='/', - allow_downgrade=False, disable_excludes=None): - - pkgs = [] - downgrade_pkgs = [] - res = {} - res['results'] = [] - res['msg'] = '' - res['rc'] = 0 - res['changed'] = False - - for spec in items: - pkg = None - downgrade_candidate = False - - # check if pkgspec is installed (if possible for idempotence) - if spec.endswith('.rpm'): - if '://' not in spec and not os.path.exists(spec): - res['msg'] += "No RPM file matching '%s' found on system" % spec - res['results'].append("No RPM file matching '%s' found on system" % spec) - res['rc'] = 127 # Ensure the task fails in with-loop - module.fail_json(**res) - - if '://' in spec: - with set_env_proxy(conf_file, installroot): - package = fetch_rpm_from_url(spec, module=module) - else: - package = spec - - # most common case is the pkg is already installed - envra = local_envra(package) - if envra is None: - module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec) - installed_pkgs = is_installed(module, repoq, envra, conf_file, en_repos=en_repos, - dis_repos=dis_repos, en_plugins=en_plugins, - dis_plugins=dis_plugins, installroot=installroot, disable_excludes=disable_excludes) - if installed_pkgs: - res['results'].append('%s providing %s is already installed' % (installed_pkgs[0], package)) - continue - - (name, ver, rel, epoch, arch) = splitFilename(envra) - installed_pkgs = is_installed(module, repoq, name, conf_file, en_repos=en_repos, - dis_repos=dis_repos, en_plugins=en_plugins, dis_plugins=dis_plugins, - installroot=installroot, disable_excludes=disable_excludes) - - # case for two same envr but differrent archs like x86_64 and i686 - if len(installed_pkgs) == 2: - (cur_name0, cur_ver0, cur_rel0, cur_epoch0, cur_arch0) = splitFilename(installed_pkgs[0]) - (cur_name1, cur_ver1, cur_rel1, cur_epoch1, cur_arch1) = splitFilename(installed_pkgs[1]) - cur_epoch0 = cur_epoch0 or '0' - cur_epoch1 = cur_epoch1 or '0' - compare = compareEVR((cur_epoch0, cur_ver0, cur_rel0), (cur_epoch1, cur_ver1, cur_rel1)) - if compare == 0 and cur_arch0 != cur_arch1: - for installed_pkg in installed_pkgs: - if installed_pkg.endswith(arch): - installed_pkgs = [installed_pkg] - - if len(installed_pkgs) == 1: - installed_pkg = installed_pkgs[0] - (cur_name, cur_ver, cur_rel, cur_epoch, cur_arch) = splitFilename(installed_pkg) - cur_epoch = cur_epoch or '0' - compare = compareEVR((cur_epoch, cur_ver, cur_rel), (epoch, ver, rel)) - - # compare > 0 -> higher version is installed - # compare == 0 -> exact version is installed - # compare < 0 -> lower version is installed - if compare > 0 and allow_downgrade: - downgrade_candidate = True - elif compare >= 0: - continue - - # else: if there are more installed packages with the same name, that would mean - # kernel, gpg-pubkey or like, so just let yum deal with it and try to install it - - pkg = package - - # groups - elif spec.startswith('@'): - if is_group_env_installed(spec, conf_file, en_plugins=en_plugins, dis_plugins=dis_plugins, installroot=installroot, - disable_excludes=disable_excludes): - continue - - pkg = spec - - # range requires or file-requires or pkgname :( - else: - # most common case is the pkg is already installed and done - # short circuit all the bs - and search for it as a pkg in is_installed - # if you find it then we're done - if not set(['*', '?']).intersection(set(spec)): - installed_pkgs = is_installed(module, repoq, spec, conf_file, en_repos=en_repos, - dis_repos=dis_repos, en_plugins=en_plugins, - dis_plugins=dis_plugins, is_pkg=True, - installroot=installroot, disable_excludes=disable_excludes) - if installed_pkgs: - res['results'].append('%s providing %s is already installed' % (installed_pkgs[0], spec)) - continue - - # look up what pkgs provide this - pkglist = what_provides(module, repoq, yum_basecmd, spec, conf_file, en_repos=en_repos, - dis_repos=dis_repos, en_plugins=en_plugins, dis_plugins=dis_plugins, - installroot=installroot, disable_excludes=disable_excludes) - if not pkglist: - res['msg'] += "No package matching '%s' found available, installed or updated" % spec - res['results'].append("No package matching '%s' found available, installed or updated" % spec) - res['rc'] = 126 # Ensure the task fails in with-loop - module.fail_json(**res) - - # if any of the packages are involved in a transaction, fail now - # so that we don't hang on the yum operation later - conflicts = transaction_exists(pkglist) - if conflicts: - res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) - res['rc'] = 125 # Ensure the task fails in with-loop - module.fail_json(**res) - - # if any of them are installed - # then nothing to do - - found = False - for this in pkglist: - if is_installed(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos, - en_plugins=en_plugins, dis_plugins=dis_plugins, is_pkg=True, - installroot=installroot, disable_excludes=disable_excludes): - found = True - res['results'].append('%s providing %s is already installed' % (this, spec)) - break - - # if the version of the pkg you have installed is not in ANY repo, but there are - # other versions in the repos (both higher and lower) then the previous checks won't work. - # so we check one more time. This really only works for pkgname - not for file provides or virt provides - # but virt provides should be all caught in what_provides on its own. - # highly irritating - if not found: - if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos, - en_plugins=en_plugins, dis_plugins=dis_plugins, installroot=installroot, - disable_excludes=disable_excludes): - found = True - res['results'].append('package providing %s is already installed' % (spec)) - - if found: - continue - - # Downgrade - The yum install command will only install or upgrade to a spec version, it will - # not install an older version of an RPM even if specified by the install spec. So we need to - # determine if this is a downgrade, and then use the yum downgrade command to install the RPM. - if allow_downgrade: - for package in pkglist: - # Get the NEVRA of the requested package using pkglist instead of spec because pkglist - # contains consistently-formatted package names returned by yum, rather than user input - # that is often not parsed correctly by splitFilename(). - (name, ver, rel, epoch, arch) = splitFilename(package) - - # Check if any version of the requested package is installed - inst_pkgs = is_installed(module, repoq, name, conf_file, en_repos=en_repos, - dis_repos=dis_repos, en_plugins=en_plugins, - dis_plugins=dis_plugins, is_pkg=True, disable_excludes=disable_excludes) - if inst_pkgs: - (cur_name, cur_ver, cur_rel, cur_epoch, cur_arch) = splitFilename(inst_pkgs[0]) - compare = compareEVR((cur_epoch, cur_ver, cur_rel), (epoch, ver, rel)) - if compare > 0: - downgrade_candidate = True - else: - downgrade_candidate = False - break - - # If package needs to be installed/upgraded/downgraded, then pass in the spec - # we could get here if nothing provides it but that's not - # the error we're catching here - pkg = spec - - if downgrade_candidate and allow_downgrade: - downgrade_pkgs.append(pkg) - else: - pkgs.append(pkg) - - if downgrade_pkgs: - res = exec_install(module, items, 'downgrade', downgrade_pkgs, res, yum_basecmd) - - if pkgs: - res = exec_install(module, items, 'install', pkgs, res, yum_basecmd) - - return res - - -def remove(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos, en_plugins, dis_plugins, - installroot='/', disable_excludes=None): - - pkgs = [] - res = {} - res['results'] = [] - res['msg'] = '' - res['changed'] = False - res['rc'] = 0 - - for pkg in items: - if pkg.startswith('@'): - installed = is_group_env_installed(pkg, conf_file, en_plugins=en_plugins, dis_plugins=dis_plugins, installroot=installroot, - disable_excludes=disable_excludes) - else: - installed = is_installed(module, repoq, pkg, conf_file, en_repos=en_repos, dis_repos=dis_repos, en_plugins=en_plugins, - dis_plugins=dis_plugins, installroot=installroot, disable_excludes=disable_excludes) - - if installed: - pkgs.append(pkg) - else: - res['results'].append('%s is not installed' % pkg) - - if pkgs: - if module.check_mode: - module.exit_json(changed=True, results=res['results'], changes=dict(removed=pkgs)) - - # run an actual yum transaction - cmd = yum_basecmd + ["remove"] + pkgs - - rc, out, err = module.run_command(cmd) + return [] + + def list_stuff(self, repoquerybin, stuff): + + qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}" + # is_installed goes through rpm instead of repoquery so it needs a slightly different format + is_installed_qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|installed\n" + repoq = [repoquerybin, '--show-duplicates', '--plugins', '--quiet'] + if self.disablerepo: + repoq.extend(['--disablerepo', ','.join(self.disablerepo)]) + if self.enablerepo: + repoq.extend(['--enablerepo', ','.join(self.enablerepo)]) + if self.installroot != '/': + repoq.extend(['--installroot', self.installroot]) + if self.conf_file and os.path.exists(self.conf_file): + repoq += ['-c', self.conf_file] + + if stuff == 'installed': + return [self.pkg_to_dict(p) for p in sorted(self.is_installed(repoq, '-a', qf=is_installed_qf)) if p.strip()] + + if stuff == 'updates': + return [self.pkg_to_dict(p) for p in sorted(self.is_update(repoq, '-a', qf=qf)) if p.strip()] + + if stuff == 'available': + return [self.pkg_to_dict(p) for p in sorted(self.is_available(repoq, '-a', qf=qf)) if p.strip()] + + if stuff == 'repos': + return [dict(repoid=name, state='enabled') for name in sorted(self.repolist(repoq)) if name.strip()] + + return [ + self.pkg_to_dict(p) for p in + sorted(self.is_installed(repoq, stuff, qf=is_installed_qf) + self.is_available(repoq, stuff, qf=qf)) + if p.strip() + ] + + def exec_install(self, items, action, pkgs, res): + cmd = self.yum_basecmd + [action] + pkgs + + if self.module.check_mode: + self.module.exit_json(changed=True, results=res['results'], changes=dict(installed=pkgs)) + + lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') + rc, out, err = self.module.run_command(cmd, environ_update=lang_env) + + if rc == 1: + for spec in items: + # Fail on invalid urls: + if ('://' in spec and ('No package %s available.' % spec in out or 'Cannot open: %s. Skipping.' % spec in err)): + err = 'Package at %s could not be installed' % spec + self.module.fail_json(changed=False, msg=err, rc=rc) res['rc'] = rc res['results'].append(out) - res['msg'] = err - - if rc != 0: - module.fail_json(**res) - - # compile the results into one batch. If anything is changed - # then mark changed - # at the end - if we've end up failed then fail out of the rest - # of the process - - # at this point we check to see if the pkg is no longer present - for pkg in pkgs: - if pkg.startswith('@'): - installed = is_group_env_installed(pkg, conf_file, en_plugins=en_plugins, dis_plugins=dis_plugins, - installroot=installroot, disable_excludes=disable_excludes) - else: - installed = is_installed(module, repoq, pkg, conf_file, en_repos=en_repos, dis_repos=dis_repos, en_plugins=en_plugins, - dis_plugins=dis_plugins, installroot=installroot, disable_excludes=disable_excludes) - - if installed: - module.fail_json(**res) - + res['msg'] += err res['changed'] = True - return res + if ('Nothing to do' in out and rc == 0) or ('does not have any packages' in err): + res['changed'] = False + if rc != 0: + res['changed'] = False + self.module.fail_json(**res) -def run_check_update(module, yum_basecmd): - # run check-update to see if we have packages pending - rc, out, err = module.run_command(yum_basecmd + ['check-update']) - return rc, out, err + # Fail if yum prints 'No space left on device' because that means some + # packages failed executing their post install scripts because of lack of + # free space (e.g. kernel package couldn't generate initramfs). Note that + # yum can still exit with rc=0 even if some post scripts didn't execute + # correctly. + if 'No space left on device' in (out or err): + res['changed'] = False + res['msg'] = 'No space left on device' + self.module.fail_json(**res) + # FIXME - if we did an install - go and check the rpmdb to see if it actually installed + # look for each pkg in rpmdb + # look for each pkg via obsoletes -def parse_check_update(check_update_output): - updates = {} - - # remove incorrect new lines in longer columns in output from yum check-update - # yum line wrapping can move the repo to the next line - # - # Meant to filter out sets of lines like: - # some_looooooooooooooooooooooooooooooooooooong_package_name 1:1.2.3-1.el7 - # some-repo-label - # - # But it also needs to avoid catching lines like: - # Loading mirror speeds from cached hostfile - # - # ceph.x86_64 1:11.2.0-0.el7 ceph - - # preprocess string and filter out empty lines so the regex below works - out = re.sub(r'\n[^\w]\W+(.*)', r' \1', - check_update_output) - - available_updates = out.split('\n') - - # build update dictionary - for line in available_updates: - line = line.split() - # ignore irrelevant lines - # '*' in line matches lines like mirror lists: - # * base: mirror.corbina.net - # len(line) != 3 could be junk or a continuation - # - # FIXME: what is the '.' not in line conditional for? - - if '*' in line or len(line) != 3 or '.' not in line[0]: - continue - else: - pkg, version, repo = line - name, dist = pkg.rsplit('.', 1) - updates.update({name: {'version': version, 'dist': dist, 'repo': repo}}) - return updates - - -def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos, en_plugins, dis_plugins, update_only, - installroot='/', disable_excludes=None): - - res = {} - res['results'] = [] - res['msg'] = '' - res['changed'] = False - res['rc'] = 0 - pkgs = {} - pkgs['update'] = [] - pkgs['install'] = [] - updates = {} - update_all = False - cmd = None - - # determine if we're doing an update all - if '*' in items: - update_all = True - - rc, out, err = run_check_update(module, yum_basecmd) - - if rc == 0 and update_all: - res['results'].append('Nothing to do here, all packages are up to date') return res - elif rc == 100: - updates = parse_check_update(out) - elif rc == 1: - res['msg'] = err - res['rc'] = rc - module.fail_json(**res) - if update_all: - cmd = yum_basecmd + ['update'] - will_update = set(updates.keys()) - will_update_from_other_package = dict() - else: - will_update = set() - will_update_from_other_package = dict() + def install(self, items, repoq): + + pkgs = [] + downgrade_pkgs = [] + res = {} + res['results'] = [] + res['msg'] = '' + res['rc'] = 0 + res['changed'] = False + for spec in items: - # some guess work involved with groups. update @ will install the group if missing - if spec.startswith('@'): - pkgs['update'].append(spec) - will_update.add(spec) - continue + pkg = None + downgrade_candidate = False # check if pkgspec is installed (if possible for idempotence) - # localpkg - elif spec.endswith('.rpm') and '://' not in spec: - if not os.path.exists(spec): + if spec.endswith('.rpm'): + if '://' not in spec and not os.path.exists(spec): res['msg'] += "No RPM file matching '%s' found on system" % spec res['results'].append("No RPM file matching '%s' found on system" % spec) res['rc'] = 127 # Ensure the task fails in with-loop - module.fail_json(**res) + self.module.fail_json(**res) - # get the pkg e:name-v-r.arch - envra = local_envra(spec) - - if envra is None: - module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec) - - # local rpm files can't be updated - if not is_installed(module, repoq, envra, conf_file, en_repos=en_repos, dis_repos=dis_repos, en_plugins=en_plugins, - dis_plugins=dis_plugins, installroot=installroot, disable_excludes=disable_excludes): - pkgs['install'].append(spec) - continue - - # URL - elif '://' in spec: - # download package so that we can check if it's already installed - with set_env_proxy(conf_file, installroot): - package = fetch_rpm_from_url(spec, module=module) - envra = local_envra(package) - - if envra is None: - module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec) - - # local rpm files can't be updated - if not is_installed(module, repoq, envra, conf_file, en_repos=en_repos, dis_repos=dis_repos, en_plugins=en_plugins, - dis_plugins=dis_plugins, installroot=installroot, disable_excludes=disable_excludes): - pkgs['install'].append(package) - continue - - # dep/pkgname - find it - else: - if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos, - en_plugins=en_plugins, dis_plugins=dis_plugins, installroot=installroot, - disable_excludes=disable_excludes) or update_only: - pkgs['update'].append(spec) + if '://' in spec: + with self.set_env_proxy(): + package = self.fetch_rpm_from_url(spec) else: - pkgs['install'].append(spec) - pkglist = what_provides(module, repoq, yum_basecmd, spec, conf_file, en_repos=en_repos, - dis_repos=dis_repos, en_plugins=en_plugins, dis_plugins=dis_plugins, - installroot=installroot, disable_excludes=disable_excludes) - # FIXME..? may not be desirable to throw an exception here if a single package is missing - if not pkglist: - res['msg'] += "No package matching '%s' found available, installed or updated" % spec - res['results'].append("No package matching '%s' found available, installed or updated" % spec) - res['rc'] = 126 # Ensure the task fails in with-loop - module.fail_json(**res) + package = spec - nothing_to_do = True - for pkg in pkglist: - if spec in pkgs['install'] and is_available(module, repoq, pkg, conf_file, - en_repos=en_repos, dis_repos=dis_repos, - en_plugins=en_plugins, dis_plugins=dis_plugins, - installroot=installroot, disable_excludes=disable_excludes): - nothing_to_do = False - break + # most common case is the pkg is already installed + envra = self.local_envra(package) + if envra is None: + self.module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec) + installed_pkgs = self.is_installed(repoq, envra) + if installed_pkgs: + res['results'].append('%s providing %s is already installed' % (installed_pkgs[0], package)) + continue - # this contains the full NVR and spec could contain wildcards - # or virtual provides (like "python-*" or "smtp-daemon") while - # updates contains name only. - pkgname, _, _, _, _ = splitFilename(pkg) - if spec in pkgs['update'] and pkgname in updates: - nothing_to_do = False - will_update.add(spec) - # Massage the updates list - if spec != pkgname: - # For reporting what packages would be updated more - # succinctly - will_update_from_other_package[spec] = pkgname - break + (name, ver, rel, epoch, arch) = splitFilename(envra) + installed_pkgs = self.is_installed(repoq, name) - if not is_installed(module, repoq, spec, conf_file, en_repos=en_repos, - dis_repos=dis_repos, en_plugins=en_plugins, - dis_plugins=dis_plugins, installroot=installroot, disable_excludes=disable_excludes) and update_only: - res['results'].append("Packages providing %s not installed due to update_only specified" % spec) - continue - if nothing_to_do: - res['results'].append("All packages providing %s are up to date" % spec) - continue + # case for two same envr but differrent archs like x86_64 and i686 + if len(installed_pkgs) == 2: + (cur_name0, cur_ver0, cur_rel0, cur_epoch0, cur_arch0) = splitFilename(installed_pkgs[0]) + (cur_name1, cur_ver1, cur_rel1, cur_epoch1, cur_arch1) = splitFilename(installed_pkgs[1]) + cur_epoch0 = cur_epoch0 or '0' + cur_epoch1 = cur_epoch1 or '0' + compare = compareEVR((cur_epoch0, cur_ver0, cur_rel0), (cur_epoch1, cur_ver1, cur_rel1)) + if compare == 0 and cur_arch0 != cur_arch1: + for installed_pkg in installed_pkgs: + if installed_pkg.endswith(arch): + installed_pkgs = [installed_pkg] - # if any of the packages are involved in a transaction, fail now - # so that we don't hang on the yum operation later - conflicts = transaction_exists(pkglist) - if conflicts: - res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) - res['results'].append("The following packages have pending transactions: %s" % ", ".join(conflicts)) - res['rc'] = 128 # Ensure the task fails in with-loop - module.fail_json(**res) + if len(installed_pkgs) == 1: + installed_pkg = installed_pkgs[0] + (cur_name, cur_ver, cur_rel, cur_epoch, cur_arch) = splitFilename(installed_pkg) + cur_epoch = cur_epoch or '0' + compare = compareEVR((cur_epoch, cur_ver, cur_rel), (epoch, ver, rel)) - # check_mode output - if module.check_mode: - to_update = [] - for w in will_update: - if w.startswith('@'): - to_update.append((w, None)) - elif w not in updates: - other_pkg = will_update_from_other_package[w] - to_update.append((w, 'because of (at least) %s-%s.%s from %s' % (other_pkg, - updates[other_pkg]['version'], - updates[other_pkg]['dist'], - updates[other_pkg]['repo']))) + # compare > 0 -> higher version is installed + # compare == 0 -> exact version is installed + # compare < 0 -> lower version is installed + if compare > 0 and self.allow_downgrade: + downgrade_candidate = True + elif compare >= 0: + continue + + # else: if there are more installed packages with the same name, that would mean + # kernel, gpg-pubkey or like, so just let yum deal with it and try to install it + + pkg = package + + # groups + elif spec.startswith('@'): + if self.is_group_env_installed(spec): + continue + + pkg = spec + + # range requires or file-requires or pkgname :( else: - to_update.append((w, '%s.%s from %s' % (updates[w]['version'], updates[w]['dist'], updates[w]['repo']))) + # most common case is the pkg is already installed and done + # short circuit all the bs - and search for it as a pkg in is_installed + # if you find it then we're done + if not set(['*', '?']).intersection(set(spec)): + installed_pkgs = self.is_installed(repoq, spec, is_pkg=True) + if installed_pkgs: + res['results'].append('%s providing %s is already installed' % (installed_pkgs[0], spec)) + continue - res['changes'] = dict(installed=pkgs['install'], updated=to_update) + # look up what pkgs provide this + pkglist = self.what_provides(repoq, spec) + if not pkglist: + res['msg'] += "No package matching '%s' found available, installed or updated" % spec + res['results'].append("No package matching '%s' found available, installed or updated" % spec) + res['rc'] = 126 # Ensure the task fails in with-loop + self.module.fail_json(**res) + + # if any of the packages are involved in a transaction, fail now + # so that we don't hang on the yum operation later + conflicts = self.transaction_exists(pkglist) + if conflicts: + res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) + res['rc'] = 125 # Ensure the task fails in with-loop + self.module.fail_json(**res) + + # if any of them are installed + # then nothing to do + + found = False + for this in pkglist: + if self.is_installed(repoq, this, is_pkg=True): + found = True + res['results'].append('%s providing %s is already installed' % (this, spec)) + break + + # if the version of the pkg you have installed is not in ANY repo, but there are + # other versions in the repos (both higher and lower) then the previous checks won't work. + # so we check one more time. This really only works for pkgname - not for file provides or virt provides + # but virt provides should be all caught in what_provides on its own. + # highly irritating + if not found: + if self.is_installed(repoq, spec): + found = True + res['results'].append('package providing %s is already installed' % (spec)) + + if found: + continue + + # Downgrade - The yum install command will only install or upgrade to a spec version, it will + # not install an older version of an RPM even if specified by the install spec. So we need to + # determine if this is a downgrade, and then use the yum downgrade command to install the RPM. + if self.allow_downgrade: + for package in pkglist: + # Get the NEVRA of the requested package using pkglist instead of spec because pkglist + # contains consistently-formatted package names returned by yum, rather than user input + # that is often not parsed correctly by splitFilename(). + (name, ver, rel, epoch, arch) = splitFilename(package) + + # Check if any version of the requested package is installed + inst_pkgs = self.is_installed(repoq, name, is_pkg=True) + if inst_pkgs: + (cur_name, cur_ver, cur_rel, cur_epoch, cur_arch) = splitFilename(inst_pkgs[0]) + compare = compareEVR((cur_epoch, cur_ver, cur_rel), (epoch, ver, rel)) + if compare > 0: + downgrade_candidate = True + else: + downgrade_candidate = False + break + + # If package needs to be installed/upgraded/downgraded, then pass in the spec + # we could get here if nothing provides it but that's not + # the error we're catching here + pkg = spec + + if downgrade_candidate and self.allow_downgrade: + downgrade_pkgs.append(pkg) + else: + pkgs.append(pkg) + + if downgrade_pkgs: + res = self.exec_install(items, 'downgrade', downgrade_pkgs, res) + + if pkgs: + res = self.exec_install(items, 'install', pkgs, res) + + return res + + def remove(self, items, repoq): + + pkgs = [] + res = {} + res['results'] = [] + res['msg'] = '' + res['changed'] = False + res['rc'] = 0 + + for pkg in items: + if pkg.startswith('@'): + installed = self.is_group_env_installed(pkg) + else: + installed = self.is_installed(repoq, pkg) + + if installed: + pkgs.append(pkg) + else: + res['results'].append('%s is not installed' % pkg) + + if pkgs: + if self.module.check_mode: + self.module.exit_json(changed=True, results=res['results'], changes=dict(removed=pkgs)) + + # run an actual yum transaction + if self.autoremove: + cmd = self.yum_basecmd + ["autoremove"] + pkgs + else: + cmd = self.yum_basecmd + ["remove"] + pkgs + + rc, out, err = self.module.run_command(cmd) + + res['rc'] = rc + res['results'].append(out) + res['msg'] = err + + if rc != 0: + if self.autoremove: + if 'No such command' not in out: + self.module.fail_json(msg='Version of YUM too old for autoremove: Requires yum 3.4.3 (RHEL/CentOS 7+)') + else: + self.module.fail_json(**res) + + # compile the results into one batch. If anything is changed + # then mark changed + # at the end - if we've end up failed then fail out of the rest + # of the process + + # at this point we check to see if the pkg is no longer present + for pkg in pkgs: + if pkg.startswith('@'): + installed = self.is_group_env_installed(pkg) + else: + installed = self.is_installed(repoq, pkg) + + if installed: + self.module.fail_json(**res) - if will_update or pkgs['install']: res['changed'] = True return res - # run commands - if cmd: # update all - rc, out, err = module.run_command(cmd) - res['changed'] = True - elif pkgs['install'] or will_update: - cmd = yum_basecmd + ['install'] + pkgs['install'] + pkgs['update'] - lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') - rc, out, err = module.run_command(cmd, environ_update=lang_env) - out_lower = out.strip().lower() - if not out_lower.endswith("no packages marked for update") and \ - not out_lower.endswith("nothing to do"): + def run_check_update(self): + # run check-update to see if we have packages pending + rc, out, err = self.module.run_command(self.yum_basecmd + ['check-update']) + return rc, out, err + + @staticmethod + def parse_check_update(check_update_output): + updates = {} + + # remove incorrect new lines in longer columns in output from yum check-update + # yum line wrapping can move the repo to the next line + # + # Meant to filter out sets of lines like: + # some_looooooooooooooooooooooooooooooooooooong_package_name 1:1.2.3-1.el7 + # some-repo-label + # + # But it also needs to avoid catching lines like: + # Loading mirror speeds from cached hostfile + # + # ceph.x86_64 1:11.2.0-0.el7 ceph + + # preprocess string and filter out empty lines so the regex below works + out = re.sub(r'\n[^\w]\W+(.*)', r' \1', check_update_output) + + available_updates = out.split('\n') + + # build update dictionary + for line in available_updates: + line = line.split() + # ignore irrelevant lines + # '*' in line matches lines like mirror lists: + # * base: mirror.corbina.net + # len(line) != 3 could be junk or a continuation + # + # FIXME: what is the '.' not in line conditional for? + + if '*' in line or len(line) != 3 or '.' not in line[0]: + continue + else: + pkg, version, repo = line + name, dist = pkg.rsplit('.', 1) + updates.update({name: {'version': version, 'dist': dist, 'repo': repo}}) + return updates + + def latest(self, items, repoq): + + res = {} + res['results'] = [] + res['msg'] = '' + res['changed'] = False + res['rc'] = 0 + pkgs = {} + pkgs['update'] = [] + pkgs['install'] = [] + updates = {} + update_all = False + cmd = None + + # determine if we're doing an update all + if '*' in items: + update_all = True + + rc, out, err = self.run_check_update() + + if rc == 0 and update_all: + res['results'].append('Nothing to do here, all packages are up to date') + return res + elif rc == 100: + updates = self.parse_check_update(out) + elif rc == 1: + res['msg'] = err + res['rc'] = rc + self.module.fail_json(**res) + + if update_all: + cmd = self.yum_basecmd + ['update'] + will_update = set(updates.keys()) + will_update_from_other_package = dict() + else: + will_update = set() + will_update_from_other_package = dict() + for spec in items: + # some guess work involved with groups. update @ will install the group if missing + if spec.startswith('@'): + pkgs['update'].append(spec) + will_update.add(spec) + continue + + # check if pkgspec is installed (if possible for idempotence) + # localpkg + elif spec.endswith('.rpm') and '://' not in spec: + if not os.path.exists(spec): + res['msg'] += "No RPM file matching '%s' found on system" % spec + res['results'].append("No RPM file matching '%s' found on system" % spec) + res['rc'] = 127 # Ensure the task fails in with-loop + self.module.fail_json(**res) + + # get the pkg e:name-v-r.arch + envra = self.local_envra(spec) + + if envra is None: + self.module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec) + + # local rpm files can't be updated + if not self.is_installed(repoq, envra): + pkgs['install'].append(spec) + continue + + # URL + elif '://' in spec: + # download package so that we can check if it's already installed + with self.set_env_proxy(): + package = self.fetch_rpm_from_url(spec) + envra = self.local_envra(package) + + if envra is None: + self.module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec) + + # local rpm files can't be updated + if not self.is_installed(repoq, envra): + pkgs['install'].append(package) + continue + + # dep/pkgname - find it + else: + if self.is_installed(repoq, spec) or self.update_only: + pkgs['update'].append(spec) + else: + pkgs['install'].append(spec) + pkglist = self.what_provides(repoq, spec) + # FIXME..? may not be desirable to throw an exception here if a single package is missing + if not pkglist: + res['msg'] += "No package matching '%s' found available, installed or updated" % spec + res['results'].append("No package matching '%s' found available, installed or updated" % spec) + res['rc'] = 126 # Ensure the task fails in with-loop + self.module.fail_json(**res) + + nothing_to_do = True + for pkg in pkglist: + if spec in pkgs['install'] and self.is_available(repoq, pkg): + nothing_to_do = False + break + + # this contains the full NVR and spec could contain wildcards + # or virtual provides (like "python-*" or "smtp-daemon") while + # updates contains name only. + pkgname, _, _, _, _ = splitFilename(pkg) + if spec in pkgs['update'] and pkgname in updates: + nothing_to_do = False + will_update.add(spec) + # Massage the updates list + if spec != pkgname: + # For reporting what packages would be updated more + # succinctly + will_update_from_other_package[spec] = pkgname + break + + if not self.is_installed(repoq, spec) and self.update_only: + res['results'].append("Packages providing %s not installed due to update_only specified" % spec) + continue + if nothing_to_do: + res['results'].append("All packages providing %s are up to date" % spec) + continue + + # if any of the packages are involved in a transaction, fail now + # so that we don't hang on the yum operation later + conflicts = self.transaction_exists(pkglist) + if conflicts: + res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) + res['results'].append("The following packages have pending transactions: %s" % ", ".join(conflicts)) + res['rc'] = 128 # Ensure the task fails in with-loop + self.module.fail_json(**res) + + # check_mode output + if self.module.check_mode: + to_update = [] + for w in will_update: + if w.startswith('@'): + to_update.append((w, None)) + elif w not in updates: + other_pkg = will_update_from_other_package[w] + to_update.append( + ( + w, + 'because of (at least) %s-%s.%s from %s' % ( + other_pkg, + updates[other_pkg]['version'], + updates[other_pkg]['dist'], + updates[other_pkg]['repo'] + ) + ) + ) + else: + to_update.append((w, '%s.%s from %s' % (updates[w]['version'], updates[w]['dist'], updates[w]['repo']))) + + res['changes'] = dict(installed=pkgs['install'], updated=to_update) + + if will_update or pkgs['install']: + res['changed'] = True + + return res + + # run commands + if cmd: # update all + rc, out, err = self.module.run_command(cmd) res['changed'] = True - else: - rc, out, err = [0, '', ''] + elif pkgs['install'] or will_update: + cmd = self.yum_basecmd + ['install'] + pkgs['install'] + pkgs['update'] + lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') + rc, out, err = self.module.run_command(cmd, environ_update=lang_env) + out_lower = out.strip().lower() + if not out_lower.endswith("no packages marked for update") and \ + not out_lower.endswith("nothing to do"): + res['changed'] = True + else: + rc, out, err = [0, '', ''] - res['rc'] = rc - res['msg'] += err - res['results'].append(out) + res['rc'] = rc + res['msg'] += err + res['results'].append(out) - if rc: - res['failed'] = True + if rc: + res['failed'] = True - return res + return res + def ensure(self, repoq): -def ensure(module, state, pkgs, conf_file, enablerepo, disablerepo, - disable_gpg_check, exclude, repoq, skip_broken, update_only, security, - bugfix, installroot='/', allow_downgrade=False, disable_plugin=None, - enable_plugin=None, disable_excludes=None, download_only=False): + pkgs = self.names - # fedora will redirect yum to dnf, which has incompatibilities - # with how this module expects yum to operate. If yum-deprecated - # is available, use that instead to emulate the old behaviors. - if module.get_bin_path('yum-deprecated'): - yumbin = module.get_bin_path('yum-deprecated') - else: - yumbin = module.get_bin_path('yum') + # autoremove was provided without `name` + if not self.names and self.autoremove: + pkgs = [] + self.state = 'absent' - # need debug level 2 to get 'Nothing to do' for groupinstall. - yum_basecmd = [yumbin, '-d', '2', '-y'] + if self.conf_file and os.path.exists(self.conf_file): + self.yum_basecmd += ['-c', self.conf_file] - if conf_file and os.path.exists(conf_file): - yum_basecmd += ['-c', conf_file] if repoq: - repoq += ['-c', conf_file] + repoq += ['-c', self.conf_file] - dis_repos = [] - en_repos = [] + if self.skip_broken: + self.yum_basecmd.extend(['--skip-broken']) - if skip_broken: - yum_basecmd.extend(['--skip-broken']) + if self.disablerepo: + self.yum_basecmd.extend(['--disablerepo=%s' % ','.join(self.disablerepo)]) - if disablerepo: - dis_repos = disablerepo.split(',') - r_cmd = ['--disablerepo=%s' % disablerepo] - yum_basecmd.extend(r_cmd) - if enablerepo: - en_repos = enablerepo.split(',') - r_cmd = ['--enablerepo=%s' % enablerepo] - yum_basecmd.extend(r_cmd) + if self.enablerepo: + self.yum_basecmd.extend(['--enablerepo=%s' % ','.join(self.enablerepo)]) - if enable_plugin: - yum_basecmd.extend(['--enableplugin', ','.join(enable_plugin)]) + if self.enable_plugin: + self.yum_basecmd.extend(['--enableplugin', ','.join(self.enable_plugin)]) - if disable_plugin: - yum_basecmd.extend(['--disableplugin', ','.join(disable_plugin)]) + if self.disable_plugin: + self.yum_basecmd.extend(['--disableplugin', ','.join(self.disable_plugin)]) - if exclude: - e_cmd = ['--exclude=%s' % exclude] - yum_basecmd.extend(e_cmd) + if self.exclude: + e_cmd = ['--exclude=%s' % ','.join(self.exclude)] + self.yum_basecmd.extend(e_cmd) - if disable_excludes: - yum_basecmd.extend(['--disableexcludes=%s' % disable_excludes]) + if self.disable_excludes: + self.yum_basecmd.extend(['--disableexcludes=%s' % self.disable_excludes]) - if download_only: - yum_basecmd.extend(['--downloadonly']) + if self.download_only: + self.yum_basecmd.extend(['--downloadonly']) - if installroot != '/': - # do not setup installroot by default, because of error - # CRITICAL:yum.cli:Config Error: Error accessing file for config file:////etc/yum.conf - # in old yum version (like in CentOS 6.6) - e_cmd = ['--installroot=%s' % installroot] - yum_basecmd.extend(e_cmd) + if self.installroot != '/': + # do not setup installroot by default, because of error + # CRITICAL:yum.cli:Config Error: Error accessing file for config file:////etc/yum.conf + # in old yum version (like in CentOS 6.6) + e_cmd = ['--installroot=%s' % self.installroot] + self.yum_basecmd.extend(e_cmd) - if state in ('installed', 'present', 'latest'): - """ The need of this entire if conditional has to be chalanged - this function is the ensure function that is called - in the main section. + if self.state in ('installed', 'present', 'latest'): + """ The need of this entire if conditional has to be chalanged + this function is the ensure function that is called + in the main section. - This conditional tends to disable/enable repo for - install present latest action, same actually - can be done for remove and absent action + This conditional tends to disable/enable repo for + install present latest action, same actually + can be done for remove and absent action - As solution I would advice to cal - try: my.repos.disableRepo(disablerepo) - and - try: my.repos.enableRepo(enablerepo) - right before any yum_cmd is actually called regardless - of yum action. + As solution I would advice to cal + try: my.repos.disableRepo(disablerepo) + and + try: my.repos.enableRepo(enablerepo) + right before any yum_cmd is actually called regardless + of yum action. - Please note that enable/disablerepo options are general - options, this means that we can call those with any action - option. https://linux.die.net/man/8/yum + Please note that enable/disablerepo options are general + options, this means that we can call those with any action + option. https://linux.die.net/man/8/yum - This docstring will be removed together when issue: #21619 - will be solved. + This docstring will be removed together when issue: #21619 + will be solved. - This has been triggered by: #19587 + This has been triggered by: #19587 + """ + + if self.update_cache: + self.module.run_command(self.yum_basecmd + ['clean', 'expire-cache']) + + my = self.yum_base() + try: + if self.disablerepo: + my.repos.disableRepo(self.disablerepo) + current_repos = my.repos.repos.keys() + if self.enablerepo: + try: + for rid in self.enablerepo: + my.repos.enableRepo(rid) + new_repos = my.repos.repos.keys() + for i in new_repos: + if i not in current_repos: + rid = my.repos.getRepo(i) + a = rid.repoXML.repoid # nopep8 - https://github.com/ansible/ansible/pull/21475#pullrequestreview-22404868 + current_repos = new_repos + except yum.Errors.YumBaseError as e: + self.module.fail_json(msg="Error setting/accessing repos: %s" % to_native(e)) + except yum.Errors.YumBaseError as e: + self.module.fail_json(msg="Error accessing repos: %s" % to_native(e)) + if self.state in ('installed', 'present'): + if self.disable_gpg_check: + self.yum_basecmd.append('--nogpgcheck') + res = self.install(pkgs, repoq) + elif self.state in ('removed', 'absent'): + res = self.remove(pkgs, repoq) + elif self.state == 'latest': + if self.disable_gpg_check: + self.yum_basecmd.append('--nogpgcheck') + if self.security: + self.yum_basecmd.append('--security') + if self.bugfix: + self.yum_basecmd.append('--bugfix') + res = self.latest(pkgs, repoq) + else: + # should be caught by AnsibleModule argument_spec + self.module.fail_json( + msg="we should never get here unless this all failed", + changed=False, + results='', + errors='unexpected state' + ) + return res + + @staticmethod + def has_yum(): + return HAS_YUM_PYTHON + + def run(self): + """ + actually execute the module code backend """ - if module.params.get('update_cache'): - module.run_command(yum_basecmd + ['clean', 'expire-cache']) + error_msgs = [] + if not HAS_RPM_PYTHON: + error_msgs.append('The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.') + if not HAS_YUM_PYTHON: + error_msgs.append('The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.') - my = yum_base(conf_file, installroot, enable_plugin, disable_plugin, disable_excludes) - try: - if disablerepo: - my.repos.disableRepo(disablerepo) - current_repos = my.repos.repos.keys() - if enablerepo: - try: - my.repos.enableRepo(enablerepo) - new_repos = my.repos.repos.keys() - for i in new_repos: - if i not in current_repos: - rid = my.repos.getRepo(i) - a = rid.repoXML.repoid # nopep8 - https://github.com/ansible/ansible/pull/21475#pullrequestreview-22404868 - current_repos = new_repos - except yum.Errors.YumBaseError as e: - module.fail_json(msg="Error setting/accessing repos: %s" % to_native(e)) - except yum.Errors.YumBaseError as e: - module.fail_json(msg="Error accessing repos: %s" % to_native(e)) - if state in ['installed', 'present']: - if disable_gpg_check: - yum_basecmd.append('--nogpgcheck') - res = install(module, pkgs, repoq, yum_basecmd, conf_file, en_repos, dis_repos, - enable_plugin, disable_plugin, installroot=installroot, - allow_downgrade=allow_downgrade, disable_excludes=disable_excludes) - elif state in ['removed', 'absent']: - res = remove(module, pkgs, repoq, yum_basecmd, conf_file, en_repos, dis_repos, enable_plugin, disable_plugin, - installroot=installroot, disable_excludes=disable_excludes) - elif state == 'latest': - if disable_gpg_check: - yum_basecmd.append('--nogpgcheck') - if security: - yum_basecmd.append('--security') - if bugfix: - yum_basecmd.append('--bugfix') - res = latest(module, pkgs, repoq, yum_basecmd, conf_file, en_repos, dis_repos, enable_plugin, disable_plugin, update_only, - installroot=installroot, disable_excludes=disable_excludes) - else: - # should be caught by AnsibleModule argument_spec - module.fail_json(msg="we should never get here unless this all failed", - changed=False, results='', errors='unexpected state') - return res + if self.disable_excludes and yum.__version_info__ < (3, 4): + self.module.fail_json(msg="'disable_includes' is available in yum version 3.4 and onwards.") + + if error_msgs: + self.module.fail_json(msg='. '.join(error_msgs)) + + # fedora will redirect yum to dnf, which has incompatibilities + # with how this module expects yum to operate. If yum-deprecated + # is available, use that instead to emulate the old behaviors. + if self.module.get_bin_path('yum-deprecated'): + yumbin = self.module.get_bin_path('yum-deprecated') + else: + yumbin = self.module.get_bin_path('yum') + + # need debug level 2 to get 'Nothing to do' for groupinstall. + self.yum_basecmd = [yumbin, '-d', '2', '-y'] + + repoquerybin = self.module.get_bin_path('repoquery', required=False) + + if self.install_repoquery and not repoquerybin and not self.module.check_mode: + yum_path = self.module.get_bin_path('yum') + if yum_path: + self.module.run_command('%s -y install yum-utils' % yum_path) + repoquerybin = self.module.get_bin_path('repoquery', required=False) + + if self.list: + if not repoquerybin: + self.module.fail_json(msg="repoquery is required to use list= with this module. Please install the yum-utils package.") + results = {'results': self.list_stuff(repoquerybin, self.list)} + else: + # If rhn-plugin is installed and no rhn-certificate is available on + # the system then users will see an error message using the yum API. + # Use repoquery in those cases. + + my = self.yum_base() + # A sideeffect of accessing conf is that the configuration is + # loaded and plugins are discovered + my.conf + repoquery = None + try: + yum_plugins = my.plugins._plugins + except AttributeError: + pass + else: + if 'rhnplugin' in yum_plugins: + if repoquerybin: + repoquery = [repoquerybin, '--show-duplicates', '--plugins', '--quiet'] + if self.installroot != '/': + repoquery.extend(['--installroot', self.installroot]) + + results = self.ensure(repoquery) + if repoquery: + results['msg'] = '%s %s' % ( + results.get('msg', ''), + 'Warning: Due to potential bad behaviour with rhnplugin and certificates, used slower repoquery calls instead of Yum API.' + ) + + self.module.exit_json(**results) def main(): @@ -1471,104 +1502,11 @@ def main(): # list=pkgspec module = AnsibleModule( - argument_spec=dict( - name=dict(type='list', aliases=['pkg']), - exclude=dict(type='str'), - # removed==absent, installed==present, these are accepted as aliases - state=dict(type='str', default='installed', choices=['absent', 'installed', 'latest', 'present', 'removed']), - enablerepo=dict(type='str'), - disablerepo=dict(type='str'), - list=dict(type='str'), - conf_file=dict(type='str'), - disable_gpg_check=dict(type='bool', default=False), - skip_broken=dict(type='bool', default=False), - update_cache=dict(type='bool', default=False, aliases=['expire-cache']), - validate_certs=dict(type='bool', default=True), - installroot=dict(type='str', default="/"), - update_only=dict(required=False, default="no", type='bool'), - # this should not be needed, but exists as a failsafe - install_repoquery=dict(type='bool', default=True), - allow_downgrade=dict(type='bool', default=False), - security=dict(type='bool', default=False), - bugfix=dict(required=False, type='bool', default=False), - enable_plugin=dict(type='list', default=[]), - disable_plugin=dict(type='list', default=[]), - disable_excludes=dict(type='str', default=None, choices=['all', 'main', 'repoid']), - download_only=dict(type='bool', default=False), - ), - required_one_of=[['name', 'list']], - mutually_exclusive=[['name', 'list']], - supports_check_mode=True, + **yumdnf_argument_spec ) - error_msgs = [] - if not HAS_RPM_PYTHON: - error_msgs.append('The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.') - if not HAS_YUM_PYTHON: - error_msgs.append('The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.') - - if error_msgs: - module.fail_json(msg='. '.join(error_msgs)) - - params = module.params - enable_plugin = params.get('enable_plugin') - disable_plugin = params.get('disable_plugin') - if params['disable_excludes'] and yum.__version_info__ < (3, 4): - module.fail_json(msg="'disable_includes' is available in yum version 3.4 and onwards.") - - if params['list']: - repoquerybin = ensure_yum_utils(module) - if not repoquerybin: - module.fail_json(msg="repoquery is required to use list= with this module. Please install the yum-utils package.") - results = {'results': list_stuff(module, repoquerybin, params['conf_file'], - params['list'], params['installroot'], - params['disablerepo'], params['enablerepo'], params['disable_excludes'])} - else: - # If rhn-plugin is installed and no rhn-certificate is available on - # the system then users will see an error message using the yum API. - # Use repoquery in those cases. - - my = yum_base(params['conf_file'], params['installroot'], enable_plugin, disable_plugin, params['disable_excludes']) - # A sideeffect of accessing conf is that the configuration is - # loaded and plugins are discovered - my.conf - repoquery = None - try: - yum_plugins = my.plugins._plugins - except AttributeError: - pass - else: - if 'rhnplugin' in yum_plugins: - repoquerybin = ensure_yum_utils(module) - if repoquerybin: - repoquery = [repoquerybin, '--show-duplicates', '--plugins', '--quiet'] - if params['installroot'] != '/': - repoquery.extend(['--installroot', params['installroot']]) - - pkg = [p.strip() for p in params['name']] - exclude = params['exclude'] - state = params['state'] - enablerepo = params.get('enablerepo', '') - disablerepo = params.get('disablerepo', '') - disable_gpg_check = params['disable_gpg_check'] - skip_broken = params['skip_broken'] - update_only = params['update_only'] - security = params['security'] - bugfix = params['bugfix'] - allow_downgrade = params['allow_downgrade'] - download_only = params['download_only'] - results = ensure(module, state, pkg, params['conf_file'], enablerepo, - disablerepo, disable_gpg_check, exclude, repoquery, - skip_broken, update_only, security, bugfix, params['installroot'], allow_downgrade, - disable_plugin=disable_plugin, enable_plugin=enable_plugin, - disable_excludes=params['disable_excludes'], download_only=download_only) - if repoquery: - results['msg'] = '%s %s' % ( - results.get('msg', ''), - 'Warning: Due to potential bad behaviour with rhnplugin and certificates, used slower repoquery calls instead of Yum API.' - ) - - module.exit_json(**results) + module_implementation = YumModule(module) + module_implementation.run() if __name__ == '__main__': diff --git a/test/integration/targets/dnf/tasks/dnf.yml b/test/integration/targets/dnf/tasks/dnf.yml index 811e8475e5..83f0dcb5e4 100644 --- a/test/integration/targets/dnf/tasks/dnf.yml +++ b/test/integration/targets/dnf/tasks/dnf.yml @@ -232,6 +232,37 @@ dnf: name=sos installroot='/' register: dnf_result +# Test download_only +- name: uninstall sos for downloadonly test + dnf: + name: sos + state: absent + +- name: install sos + dnf: + name: sos + state: latest + download_only: true + register: dnf_result + +- name: verify download of sos (part 1 -- dnf "install" succeeded) + assert: + that: + - "dnf_result is success" + - "dnf_result is changed" + +- name: uninstall sos (noop) + dnf: + name: sos + state: absent + register: dnf_result + +- name: verify download of sos (part 2 -- nothing removed during uninstall) + assert: + that: + - "dnf_result is success" + - "not dnf_result is changed" + # GROUP INSTALL # Using 'Books and Guides' because it is only 5 packages and a 7.3 M download on Fedora 26. # It also doesn't install anything that will tamper with our Python environment. @@ -308,7 +339,8 @@ - "'msg' in dnf_result" # cleanup until https://github.com/ansible/ansible/issues/27377 is resolved -- shell: dnf -y group install "Books and Guides" && dnf -y group remove "Books and Guides" +- shell: 'dnf -y group install "Books and Guides" && dnf -y group remove "Books and Guides"' + register: shell_dnf_result # GROUP UPGRADE - this will go to the same method as group install # but through group_update - it is its invocation we're testing here @@ -426,3 +458,188 @@ - "'non-existent-rpm' in dnf_result['failures'][0]" - "'no package matched' in dnf_result['failures'][0]" - "'Failed to install some of the specified packages' in dnf_result['msg']" + +- name: use latest to install httpd + dnf: + name: httpd + state: latest + register: dnf_result + +- name: verify httpd was installed + assert: + that: + - "'changed' in dnf_result" + +- name: uninstall httpd + dnf: + name: httpd + state: removed + +- name: update httpd only if it exists + dnf: + name: httpd + state: latest + update_only: yes + register: dnf_result + +- name: verify httpd not installed + assert: + that: + - "not dnf_result is changed" + +- name: try to install not compatible arch rpm, should fail + dnf: + name: http://download.fedoraproject.org/pub/epel/7/ppc64le/Packages/b/banner-1.3.4-3.el7.ppc64le.rpm + state: present + register: dnf_result + ignore_errors: True + +- name: verify that dnf failed + assert: + that: + - "not dnf_result is changed" + - "dnf_result is failed" + +# setup for testing installing an RPM from url + +- set_fact: + pkg_name: fpaste + +- name: cleanup + dnf: + name: "{{ pkg_name }}" + state: absent + +- set_fact: + pkg_url: https://download.fedoraproject.org/pub/fedora/linux/releases/27/Everything/x86_64/os/Packages/f/fpaste-0.3.9.1-1.fc27.noarch.rpm +# setup end + +- name: download an rpm + get_url: + url: "{{ pkg_url }}" + dest: "/tmp/{{ pkg_name }}.rpm" + +- name: install the downloaded rpm + dnf: + name: "/tmp/{{ pkg_name }}.rpm" + state: present + register: dnf_result + +- name: verify installation + assert: + that: + - "dnf_result is success" + - "dnf_result is changed" + +- name: install the downloaded rpm again + dnf: + name: "/tmp/{{ pkg_name }}.rpm" + state: present + register: dnf_result + +- name: verify installation + assert: + that: + - "dnf_result is success" + - "not dnf_result is changed" + +- name: clean up + dnf: + name: "{{ pkg_name }}" + state: absent + +- name: install from url + dnf: + name: "{{ pkg_url }}" + state: present + register: dnf_result + +- name: verify installation + assert: + that: + - "dnf_result is success" + - "dnf_result is changed" + - "dnf_result is not failed" + +- name: verify dnf module outputs + assert: + that: + - "'changed' in dnf_result" + - "'results' in dnf_result" + +- name: Create a temp RPM file which does not contain nevra information + file: + name: "/tmp/non_existent_pkg.rpm" + state: touch + +- name: Try installing RPM file which does not contain nevra information + dnf: + name: "/tmp/non_existent_pkg.rpm" + state: present + register: no_nevra_info_result + ignore_errors: yes + +- name: Verify RPM failed to install + assert: + that: + - "'changed' in no_nevra_info_result" + - "'msg' in no_nevra_info_result" + +- name: Delete a temp RPM file + file: + name: "/tmp/non_existent_pkg.rpm" + state: absent + +- name: uninstall lsof + dnf: + name: lsof + state: removed + +- name: check lsof with rpm + shell: rpm -q lsof + ignore_errors: True + register: rpm_lsof_result + +- name: verify lsof is uninstalled + assert: + that: + - "rpm_lsof_result is failed" + +- name: exclude lsof + lineinfile: + dest: /etc/dnf/dnf.conf + regexp: (^exclude=)(.)* + line: "exclude=lsof*" + state: present + +# begin test case where disable_excludes is supported +- name: Try install lsof without disable_excludes + dnf: name=lsof state=latest + register: dnf_lsof_result + ignore_errors: True + +- name: verify lsof did not install because it is in exclude list + assert: + that: + - "dnf_lsof_result is failed" + +- name: install lsof with disable_excludes + dnf: name=lsof state=latest disable_excludes=all + register: dnf_lsof_result_using_excludes + +- name: verify lsof did install using disable_excludes=all + assert: + that: + - "dnf_lsof_result_using_excludes is success" + - "dnf_lsof_result_using_excludes is changed" + - "dnf_lsof_result_using_excludes is not failed" + +- name: remove exclude lsof (cleanup dnf.conf) + lineinfile: + dest: /etc/dnf/dnf.conf + regexp: (^exclude=lsof*) + line: "exclude=" + state: present + + +# end test case where disable_excludes is supported diff --git a/test/units/modules/packaging/os/test_yum.py b/test/units/modules/packaging/os/test_yum.py index 59c22411b9..ced0c4fc56 100644 --- a/test/units/modules/packaging/os/test_yum.py +++ b/test/units/modules/packaging/os/test_yum.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- from ansible.compat.tests import unittest -from ansible.modules.packaging.os import yum +from ansible.modules.packaging.os.yum import YumModule yum_plugin_load_error = """ @@ -141,34 +141,34 @@ class TestYumUpdateCheckParse(unittest.TestCase): self.assertIsInstance(result, dict) def test_empty_output(self): - res = yum.parse_check_update("") + res = YumModule.parse_check_update("") expected_pkgs = [] self._assert_expected(expected_pkgs, res) def test_longname(self): - res = yum.parse_check_update(longname) + res = YumModule.parse_check_update(longname) expected_pkgs = ['xxxxxxxxxxxxxxxxxxxxxxxxxx', 'glibc'] self._assert_expected(expected_pkgs, res) def test_plugin_load_error(self): - res = yum.parse_check_update(yum_plugin_load_error) + res = YumModule.parse_check_update(yum_plugin_load_error) expected_pkgs = [] self._assert_expected(expected_pkgs, res) def test_wrapped_output_1(self): - res = yum.parse_check_update(wrapped_output_1) + res = YumModule.parse_check_update(wrapped_output_1) expected_pkgs = ["vms-agent"] self._assert_expected(expected_pkgs, res) def test_wrapped_output_2(self): - res = yum.parse_check_update(wrapped_output_2) + res = YumModule.parse_check_update(wrapped_output_2) expected_pkgs = ["empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty", "libtiff"] self._assert_expected(expected_pkgs, res) def test_wrapped_output_3(self): - res = yum.parse_check_update(wrapped_output_3) + res = YumModule.parse_check_update(wrapped_output_3) expected_pkgs = ["ceph", "ceph-base", "ceph-common", "ceph-mds", "ceph-mon", "ceph-osd", "ceph-selinux", "libcephfs1", "librados2", "libradosstriper1", "librbd1", "librgw2", @@ -176,16 +176,16 @@ class TestYumUpdateCheckParse(unittest.TestCase): self._assert_expected(expected_pkgs, res) def test_wrapped_output_4(self): - res = yum.parse_check_update(wrapped_output_4) + res = YumModule.parse_check_update(wrapped_output_4) expected_pkgs = ["ipxe-roms-qemu", "quota", "quota-nls", "rdma", "screen", "sos", "sssd-client"] self._assert_expected(expected_pkgs, res) def test_wrapped_output_rhel7(self): - res = yum.parse_check_update(unwrapped_output_rhel7) + res = YumModule.parse_check_update(unwrapped_output_rhel7) self._assert_expected(unwrapped_output_rhel7_expected_pkgs, res) def test_wrapped_output_rhel7_obsoletes(self): - res = yum.parse_check_update(unwrapped_output_rhel7_obsoletes) + res = YumModule.parse_check_update(unwrapped_output_rhel7_obsoletes) self._assert_expected(unwrapped_output_rhel7_expected_pkgs, res)