Repository: ansible/ansible

Issue:
Kernel initramfs failed to build - module completed successfully
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Feature Request


##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
module: yum

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.3.2.0
  config file = /ifs/ansible.cfg
  configured module search path = [u'/opt/ansible/ansible_boot/.virtenv/ifs/lib/python2.7/site-packages/ansible/modules', u'/ifs/library']
  python version = 2.7.5 (default, Oct 11 2015, 17:47:16) [GCC 4.8.3 20140911 (Red Hat 4.8.3-9)]
```

##### CONFIGURATION
<!---
If using Ansible 2.4 or above, paste the results of "ansible-config dump --only-changed"

Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).

-->

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
RHEL7 & RHEL7
##### SUMMARY
<!--- Explain the problem briefly -->

When upgrading the packages, even if yum/dracut can't build the kernel initramfs due to too not enough space in `/boot`, the command completes successfully. This is revealed on reboot, as the kernel panics because no initramfs was built.

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

As long as the `/boot` partition is too small to build a new kernel initramfs for the newly installed kernel, yum will show dracut failing, ansible will not fail the task.

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- hosts: all
  tasks:
        
    - name: (pre_event) Free space in /boot partition is greater than 40MB
      shell: df -P /boot | grep -e "^/dev" | tr -s ' ' | cut -d ' ' -f 4
      register: boot_partition_space
      # 40MB
      failed_when: boot_partition_space.stdout|int >= 40000

    # Kernel has to be an update - else name=kernel
    - name: (pre_event) Update all packages on the system
      yum:
        name: '*'
        state: latest
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->

The second task should fail when dracut fails to build the kernel initramfs

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

After the following script runs, and a reboot is issued, the server cannot boot up, as it always encounters a kernel panic.

<!--- Paste verbatim command output between quotes below -->
Stdout:
```
[...]
  Cleanup    : ncurses-base-5.9-13.20130511.el7.noarch                1012/1013
  Cleanup    : libgcc-4.8.5-11.el7.x86_64                                          1013/1013
2661 blocks

gzip: stdout: No space left on device
dracut: creation of /boot/initramfs-3.10.0-693.2.2.el7.x86_64.img failed
mkinitrd failed
warning: %posttrans(kernel-3.10.0-693.2.2.el7.x86_64) scriptlet failed, exit status 1
Uploading Package Profile
  Verifying  : zlib-1.2.7-17.el7.i686                                                1/1013
  Verifying  : yum-rhn-plugin-2.0.1-9.el7.noarch                         2/1013
[...]
```

From our custom logging callback plugin:
```
  "changed": true,
  "msg": "Warning: RPMDB altered outside of yum.\n** Found 1 pre-existing rpmdb problem(s), 'yum check' output follows:\nkatello-ca-consumer-rhelsatsy01exdu.ent.oclc.org-1.0-12.noarch is a duplicate with katello-ca-consumer-rhelsatsy01exdu.ent.oclc.org-1.0-7.noarch\nNon-fatal POSTTRANS scriptlet failure in rpm package kernel-3.10.0-693.2.2.el7.x86_64\n",
  "rc": 0,
```

So even though the return code is 0 for the yum command, - I would appreciate if the yum module would be able to catch this.


Discussion:
cc @akasurde @berenddeschouwer @mkrizek @verm666
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@infrascripting Thanks for reporting this issue.

yum module depends upon `yum` command and its return code. In above scenario, `yum` command returns 0 as `posttrans` script failure is non-fatal and considered as warning, so yum module returns 0.

I am not sure how to handle this kind of scenario.  Any ideas or suggestions, much appreciated. 
cc @kustodian
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
It looks like the only thing we could do is search for `No space left on device` in the Yum output and if we find it, return an error about space. 
I'm not sure if it's stdout or stderr.
I added a simple fix for this in https://github.com/ansible/ansible/pull/40737
resolved_by_pr #40737 
@infrascripting Could you please check if the above mentioned PR works for you ? 
@Akasurde I'll take some time to test it this week.

Resolving patch:
diff --git a/lib/ansible/module_utils/yumdnf.py b/lib/ansible/module_utils/yumdnf.py
new file mode 100644
--- /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 <skvidal at fedoraproject.org>
+# 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
--- 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 <cristian at cvee.org>
 # Copyright 2015 Igor Gnatenko <i.gnatenko.brain@gmail.com>
+# Copyright 2018 Adam Miller <admiller@redhat.com>
 #
 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
 
@@ -88,6 +89,95 @@
     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 @@
   - '"Igor Gnatenko (@ignatenkobrain)" <i.gnatenko.brain@gmail.com>'
   - '"Cristian van Ee (@DJMuggs)" <cristian at cvee.org>'
   - "Berend De Schouwer (github.com/berenddeschouwer)"
+  - '"Adam Miller (@maxamillion)" <admiller@redhat.com>"'
 '''
 
 EXAMPLES = '''
@@ -147,7 +238,9 @@
     state: absent
     autoremove: no
 '''
+
 import os
+import tempfile
 
 try:
     import dnf
@@ -160,378 +253,476 @@
 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
+
+# 64k.  Number of bytes to read at a time when manually downloading pkgs via a url
+BUFSIZE = 65536
+
+
+class DnfModule(YumDnf):
+    """
+    DNF Ansible module back-end implementation
+    """
 
-def _ensure_dnf(module):
-    if not HAS_DNF:
-        if PY2:
-            package = 'python2-dnf'
+    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:
+            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 _ensure_dnf(self):
+        if not HAS_DNF:
+            if PY2:
+                package = 'python2-dnf'
+            else:
+                package = 'python3-dnf'
+
+            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)
+                )
+
+            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)
+                )
+
+    def _configure_base(self, base, conf_file, disable_gpg_check, installroot='/'):
+        """Configure the dnf Base object."""
+
+        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)
+
+        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 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:
-            package = 'python3-dnf'
+            packages = dnf.subject.Subject(command).get_best_query(self.base.sack)
+            results = [self._package_dict(package) for package in packages]
 
-        if module.check_mode:
-            module.fail_json(msg="`{0}` is not installed, but it is required"
-                             "for the Ansible dnf module.".format(package))
+        self.module.exit_json(results=results)
 
-        module.run_command(['dnf', 'install', '-y', package], check_rc=True)
-        global dnf
+    def _mark_package_install(self, pkg_spec):
+        """Mark the package for install."""
         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))
-
-
-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)
+            self.base.install(pkg_spec)
+        except dnf.exceptions.MarkingError:
+            self.module.fail_json(msg="No package {0} available.".format(pkg_spec))
+
+    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
+
+    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)
+
+    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:
-            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:])
+            pkgs = []
+            for filename in filenames:
+                pkgs.append(self.base.add_remote_rpm(filename))
+        if self.update_only:
+            self._update_only(pkgs)
         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)
-            else:
-                environment = base.comps.environment_by_pattern(group_spec)
-                if environment:
-                    environments.append(environment.id)
+            for pkg in pkgs:
+                self.base.package_install(pkg)
+
+    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
+
+        # 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:
-                    module.fail_json(
-                        msg="No group {0} available.".format(group_spec))
-
-        if state in ['installed', 'present']:
-            # Install files.
-            _install_remote_rpms(base, filenames)
-
-            # 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)))
-
-            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)))
-
-            # Install packages.
-            for pkg_spec in pkg_specs:
-                _mark_package_install(module, base, pkg_spec)
-
-        elif state == 'latest':
-            # "latest" is same as "installed" for filenames.
-            _install_remote_rpms(base, filenames)
-
-            for group in groups:
-                try:
+                    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:
-                        base.group_upgrade(group)
+                        self.base.group_remove(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)))
+                        # Group is already uninstalled.
+                        pass
 
-            for environment in environments:
-                try:
+                for environment in environments:
                     try:
-                        base.environment_upgrade(environment)
+                        self.base.environment_remove(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)))
-
-            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)))
+                        # 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:
+                self.module.fail_json(
+                    msg='Failed to install some of the specified packages',
+                    failures=failures
+                )
+            self.module.exit_json(msg="Nothing to do")
         else:
-            # state == absent
-            if autoremove:
-                base.conf.clean_requirements_on_remove = autoremove
-
-            if filenames:
-                module.fail_json(
-                    msg="Cannot remove paths -- please specify package name.")
-
-            for group in groups:
-                try:
-                    base.group_remove(group)
-                except dnf.exceptions.CompsError:
-                    # Group is already uninstalled.
-                    pass
-
-            for environment in environments:
-                try:
-                    base.environment_remove(environment)
-                except dnf.exceptions.CompsError:
-                    # Environment is already uninstalled.
-                    pass
-
-            installed = base.sack.query().installed()
-            for pkg_spec in pkg_specs:
-                if installed.filter(name=pkg_spec):
-                    base.remove(pkg_spec)
-
-            # Like the dnf CLI we want to allow recursive removal of dependent
-            # packages
-            allow_erasing = True
-
-            if autoremove:
-                base.autoremove()
-
-    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:
+            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)
+
+            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)))
+
+            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:
-                module.fail_json(msg='Failed to install some of the '
-                                     'specified packages',
-                                 failures=failures)
-            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))
-
-        if failures:
-            module.fail_json(msg='Failed to install some of the '
-                                 'specified packages',
-                             failures=failures)
-        module.exit_json(**response)
+                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
-
-    _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'])
+        **yumdnf_argument_spec
+    )
+
+    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
--- a/lib/ansible/modules/packaging/os/yum.py
+++ b/lib/ansible/modules/packaging/os/yum.py
@@ -53,12 +53,16 @@
       - 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 @@
         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 @@
     - Eduard Snesarev (@verm666)
     - Berend De Schouwer (@berenddeschouwer)
     - Abhijeet Kasurde (@Akasurde)
+    - Adam Miller (@maxamillion)
 '''
 
 EXAMPLES = '''
@@ -285,6 +306,11 @@
     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 @@
 
 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
-
-    return my
-
-
-def ensure_yum_utils(module):
-    repoquerybin = module.get_bin_path('repoquery', required=False)
-
-    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)
+    def __init__(self, module):
+
+        # 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 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:
+            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)))
-        else:
-            raise 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 package_file.name
+        return my
 
+    def po_to_envra(self, po):
+        if hasattr(po, 'ui_envra'):
+            return po.ui_envra
 
-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)
 
-    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:
+            groups_list = my.doGroupLists()
 
-def is_group_env_installed(name, conf_file, en_plugins=None, dis_plugins=None,
-                           installroot='/', disable_excludes=None):
-    name_lower = name.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
 
-    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()
+        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
 
-    # 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
+    def is_installed(self, repoq, pkgspec, qf=None, is_pkg=False):
+        if qf is None:
+            qf = "%{epoch}:%{name}-%{version}-%{release}.%{arch}\n"
 
-    return False
+        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
+                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))
 
-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"
+            return [self.po_to_envra(p) for p in pkgs]
 
-    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.rpmdb.matchPackageNames([pkgspec])
-            pkgs = e + m
+        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 = ''
-
-        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, '', '')
+    def is_available(self, repoq, pkgspec, qf=def_qf):
+        if not repoq:
 
-        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
+            pkgs = []
+            try:
+                my = self.yum_base()
+                for rid in self.disablerepo:
+                    my.repos.disableRepo(rid)
+                for rid in self.enablerepo:
+                    my.repos.enableRepo(rid)
 
-    return []
+                e, m, _ = my.pkgSack.matchPackageNames([pkgspec])
+                pkgs = e + m
+                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))
 
+            return [self.po_to_envra(p) for p in pkgs]
 
-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 = []
+        else:
+            myrepoq = list(repoq)
 
-    if not repoq:
+            r_cmd = ['--disablerepo', ','.join(self.disablerepo)]
+            myrepoq.extend(r_cmd)
 
-        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))
+            r_cmd = ['--enablerepo', ','.join(self.enablerepo)]
+            myrepoq.extend(r_cmd)
 
-        return [po_to_envra(p) for p in pkgs]
+            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))
 
-    else:
-        myrepoq = list(repoq)
+        return []
 
-        r_cmd = ['--disablerepo', ','.join(dis_repos)]
-        myrepoq.extend(r_cmd)
+    def is_update(self, repoq, pkgspec, qf=def_qf):
+        if not repoq:
 
-        r_cmd = ['--enablerepo', ','.join(en_repos)]
-        myrepoq.extend(r_cmd)
+            pkgs = []
+            updates = []
 
-        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))
+            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))
 
-    return []
+            retpkgs = (pkg for pkg in pkgs if pkg in updates)
 
+            return set(self.po_to_envra(p) for p in retpkgs)
 
-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 = []
+        else:
+            myrepoq = list(repoq)
+            r_cmd = ['--disablerepo', ','.join(self.disablerepo)]
+            myrepoq.extend(r_cmd)
 
-    if not repoq:
+            r_cmd = ['--enablerepo', ','.join(self.enablerepo)]
+            myrepoq.extend(r_cmd)
 
-        pkgs = []
-        updates = []
+            cmd = myrepoq + ["--pkgnarrow=updates", "--qf", qf, pkgspec]
+            rc, out, err = self.module.run_command(cmd)
 
-        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 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))
 
-        retpkgs = (pkg for pkg in pkgs if pkg in updates)
+        return set()
 
-        return set(po_to_envra(p) for p in retpkgs)
+    def what_provides(self, repoq, req_spec, qf=def_qf):
+        if not repoq:
 
-    else:
-        myrepoq = list(repoq)
-        r_cmd = ['--disablerepo', ','.join(dis_repos)]
-        myrepoq.extend(r_cmd)
+            pkgs = []
+            try:
+                my = self.yum_base()
+                for rid in self.disablerepo:
+                    my.repos.disableRepo(rid)
+                for rid in self.enablerepo:
+                    my.repos.enableRepo(rid)
 
-        r_cmd = ['--enablerepo', ','.join(en_repos)]
-        myrepoq.extend(r_cmd)
+                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))
 
-        cmd = myrepoq + ["--pkgnarrow=updates", "--qf", qf, pkgspec]
-        rc, out, err = module.run_command(cmd)
+            return set(self.po_to_envra(p) for p in pkgs)
 
-        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))
+            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()
+        return set()
 
+    def transaction_exists(self, pkglist):
+        """
+        checks the package list to see if any packages are
+        involved in an incomplete transaction
+        """
 
-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 = []
+        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
 
-    if not repoq:
+    def local_envra(self, path):
+        """return envra of a local rpm passed in"""
 
-        pkgs = []
+        ts = rpm.TransactionSet()
+        ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
+        fd = os.open(path, os.O_RDONLY)
         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)
+            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'
 
-            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
+        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 + err2))
+            return []
 
-    return set()
+    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]
 
-def transaction_exists(pkglist):
-    """
-    checks the package list to see if any packages are
-    involved in an incomplete transaction
-    """
+        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()]
 
-    conflicts = []
-    if not transaction_helpers:
-        return conflicts
+        if stuff == 'updates':
+            return [self.pkg_to_dict(p) for p in sorted(self.is_update(repoq, '-a', qf=qf)) if p.strip()]
 
-    # 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 []
+        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()]
 
-def list_stuff(module, repoquerybin, conf_file, stuff, installroot='/', disablerepo='', enablerepo='', disable_excludes=None):
+        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()
+        ]
 
-    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]
+    def exec_install(self, items, action, pkgs, res):
+        cmd = self.yum_basecmd + [action] + pkgs
 
-    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 self.module.check_mode:
+            self.module.exit_json(changed=True, results=res['results'], changes=dict(installed=pkgs))
 
-    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()]
+        lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
+        rc, out, err = self.module.run_command(cmd, environ_update=lang_env)
 
-    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 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)
 
-    if stuff == 'repos':
-        return [dict(repoid=name, state='enabled') for name in sorted(repolist(module, repoq)) if name.strip()]
+        res['rc'] = rc
+        res['results'].append(out)
+        res['msg'] += err
+        res['changed'] = True
 
-    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()]
+        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)
+
+        # 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 exec_install(module, items, action, pkgs, res, yum_basecmd):
-    cmd = yum_basecmd + [action] + pkgs
+        return res
 
-    if module.check_mode:
-        module.exit_json(changed=True, results=res['results'], changes=dict(installed=pkgs))
+    def install(self, items, repoq):
 
-    lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
-    rc, out, err = module.run_command(cmd, environ_update=lang_env)
+        pkgs = []
+        downgrade_pkgs = []
+        res = {}
+        res['results'] = []
+        res['msg'] = ''
+        res['rc'] = 0
+        res['changed'] = False
 
-    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)
+            pkg = None
+            downgrade_candidate = False
 
-    res['rc'] = rc
-    res['results'].append(out)
-    res['msg'] += err
-    res['changed'] = True
+            # 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
+                    self.module.fail_json(**res)
 
-    if ('Nothing to do' in out and rc == 0) or ('does not have any packages' in err):
-        res['changed'] = False
+                if '://' in spec:
+                    with self.set_env_proxy():
+                        package = self.fetch_rpm_from_url(spec)
+                else:
+                    package = spec
 
-    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
+                # 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
 
-            (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:
+                (name, ver, rel, epoch, arch) = splitFilename(envra)
+                installed_pkgs = self.is_installed(repoq, name)
+
+                # 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 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
 
-            # 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 = spec
 
-            pkg = package
+            # 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 = 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
+
+                # 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
 
-        # 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
+                # 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)
 
-            pkg = spec
+        if downgrade_pkgs:
+            res = self.exec_install(items, 'downgrade', downgrade_pkgs, res)
 
-        # 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
+        if pkgs:
+            res = self.exec_install(items, 'install', pkgs, res)
 
-            # 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
+        return res
 
-            # 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)
+    def remove(self, items, repoq):
 
-    if downgrade_pkgs:
-        res = exec_install(module, items, 'downgrade', downgrade_pkgs, res, yum_basecmd)
+        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 pkgs:
-        res = exec_install(module, items, 'install', pkgs, res, yum_basecmd)
+            if installed:
+                pkgs.append(pkg)
+            else:
+                res['results'].append('%s is not installed' % pkg)
 
-    return res
+        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
 
-def remove(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos, en_plugins, dis_plugins,
-           installroot='/', disable_excludes=None):
+            rc, out, err = self.module.run_command(cmd)
 
-    pkgs = []
-    res = {}
-    res['results'] = []
-    res['msg'] = ''
-    res['changed'] = False
-    res['rc'] = 0
+            res['rc'] = rc
+            res['results'].append(out)
+            res['msg'] = err
 
-    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 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)
 
-        if installed:
-            pkgs.append(pkg)
-        else:
-            res['results'].append('%s is not installed' % pkg)
+            # 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
 
-    if pkgs:
-        if module.check_mode:
-            module.exit_json(changed=True, results=res['results'], changes=dict(removed=pkgs))
+            # 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)
 
-        # run an actual yum transaction
-        cmd = yum_basecmd + ["remove"] + pkgs
+                if installed:
+                    self.module.fail_json(**res)
 
-        rc, out, err = module.run_command(cmd)
+            res['changed'] = True
 
-        res['rc'] = rc
-        res['results'].append(out)
-        res['msg'] = err
+        return res
 
-        if rc != 0:
-            module.fail_json(**res)
+    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
 
-        # 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
+    @staticmethod
+    def parse_check_update(check_update_output):
+        updates = {}
 
-        # 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)
+        # 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:
-                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)
+                pkg, version, repo = line
+                name, dist = pkg.rsplit('.', 1)
+                updates.update({name: {'version': version, 'dist': dist, 'repo': repo}})
+        return updates
 
-            if installed:
-                module.fail_json(**res)
+    def latest(self, items, repoq):
 
-        res['changed'] = True
+        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 @<group> will install the group if missing
+                if spec.startswith('@'):
+                    pkgs['update'].append(spec)
+                    will_update.add(spec)
+                    continue
 
-    return res
+                # 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)
 
-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
+                    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
 
-def parse_check_update(check_update_output):
-    updates = {}
+                # 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)
 
-    # 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
+                    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
 
-    # preprocess string and filter out empty lines so the regex below works
-    out = re.sub(r'\n[^\w]\W+(.*)', r' \1',
-                 check_update_output)
+                # 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'])))
 
-    available_updates = out.split('\n')
+            res['changes'] = dict(installed=pkgs['install'], updated=to_update)
 
-    # 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 will_update or pkgs['install']:
+                res['changed'] = True
+
+            return res
 
-        if '*' in line or len(line) != 3 or '.' not in line[0]:
-            continue
+        # run commands
+        if cmd:     # update all
+            rc, out, err = self.module.run_command(cmd)
+            res['changed'] = True
+        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:
-            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
+            rc, out, err = [0, '', '']
+
         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()
-        for spec in items:
-            # some guess work involved with groups. update @<group> will install the group if missing
-            if spec.startswith('@'):
-                pkgs['update'].append(spec)
-                will_update.add(spec)
-                continue
+        res['msg'] += err
+        res['results'].append(out)
 
-            # 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
-                    module.fail_json(**res)
+        if rc:
+            res['failed'] = True
 
-                # get the pkg e:name-v-r.arch
-                envra = local_envra(spec)
+        return res
 
-                if envra is None:
-                    module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec)
+    def ensure(self, repoq):
 
-                # 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
+        pkgs = self.names
 
-            # 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)
+        # autoremove was provided without `name`
+        if not self.names and self.autoremove:
+            pkgs = []
+            self.state = 'absent'
 
-                if envra is None:
-                    module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec)
+        if self.conf_file and os.path.exists(self.conf_file):
+            self.yum_basecmd += ['-c', self.conf_file]
 
-                # 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
+        if repoq:
+            repoq += ['-c', self.conf_file]
 
-            # 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)
-                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)
-
-            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
-
-                # 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 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
+        if self.skip_broken:
+            self.yum_basecmd.extend(['--skip-broken'])
 
-            # 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)
-
-    # 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'])))
-            else:
-                to_update.append((w, '%s.%s from %s' % (updates[w]['version'], updates[w]['dist'], updates[w]['repo'])))
+        if self.disablerepo:
+            self.yum_basecmd.extend(['--disablerepo=%s' % ','.join(self.disablerepo)])
 
-        res['changes'] = dict(installed=pkgs['install'], updated=to_update)
+        if self.enablerepo:
+            self.yum_basecmd.extend(['--enablerepo=%s' % ','.join(self.enablerepo)])
 
-        if will_update or pkgs['install']:
-            res['changed'] = True
+        if self.enable_plugin:
+            self.yum_basecmd.extend(['--enableplugin', ','.join(self.enable_plugin)])
 
-        return res
+        if self.disable_plugin:
+            self.yum_basecmd.extend(['--disableplugin', ','.join(self.disable_plugin)])
 
-    # 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"):
-            res['changed'] = True
-    else:
-        rc, out, err = [0, '', '']
+        if self.exclude:
+            e_cmd = ['--exclude=%s' % ','.join(self.exclude)]
+            self.yum_basecmd.extend(e_cmd)
 
-    res['rc'] = rc
-    res['msg'] += err
-    res['results'].append(out)
+        if self.disable_excludes:
+            self.yum_basecmd.extend(['--disableexcludes=%s' % self.disable_excludes])
 
-    if rc:
-        res['failed'] = True
+        if self.download_only:
+            self.yum_basecmd.extend(['--downloadonly'])
 
-    return res
+        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 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.
 
-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):
+                This conditional tends to disable/enable repo for
+                install present latest action, same actually
+                can be done for remove and absent action
 
-    # 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')
+                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.
 
-    # need debug level 2 to get 'Nothing to do' for groupinstall.
-    yum_basecmd = [yumbin, '-d', '2', '-y']
+                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
 
-    if conf_file and os.path.exists(conf_file):
-        yum_basecmd += ['-c', conf_file]
-        if repoq:
-            repoq += ['-c', conf_file]
-
-    dis_repos = []
-    en_repos = []
-
-    if skip_broken:
-        yum_basecmd.extend(['--skip-broken'])
-
-    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 enable_plugin:
-        yum_basecmd.extend(['--enableplugin', ','.join(enable_plugin)])
-
-    if disable_plugin:
-        yum_basecmd.extend(['--disableplugin', ','.join(disable_plugin)])
-
-    if exclude:
-        e_cmd = ['--exclude=%s' % exclude]
-        yum_basecmd.extend(e_cmd)
-
-    if disable_excludes:
-        yum_basecmd.extend(['--disableexcludes=%s' % disable_excludes])
-
-    if download_only:
-        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 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
-
-            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
-
-            This docstring will be removed together when issue: #21619
-            will be solved.
-
-            This has been triggered by: #19587
+                This docstring will be removed together when issue: #21619
+                will be solved.
+
+                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__':


Test patch:
diff --git a/test/integration/targets/dnf/tasks/dnf.yml b/test/integration/targets/dnf/tasks/dnf.yml
--- 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
--- 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 @@ def _assert_expected(self, expected_pkgs, result):
         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 @@ def test_wrapped_output_3(self):
         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)