Repository: ansible/ansible

Issue:
Support dnf autoremove
_From @font on July 28, 2016 15:57_

<!--- Verify first that your issue/request is not already reported in GitHub -->
##### ISSUE TYPE

<!--- Pick one below and delete the rest: -->
- Feature Idea
##### COMPONENT NAME

<!--- Name of the plugin/module/task -->
- dnf
##### ANSIBLE VERSION

<!--- Paste verbatim output from “ansible --version” between quotes below -->

```
ansible 2.1.0.0
```

<!---##### CONFIGURATION

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.
-->
- Managing Linux
##### SUMMARY

<!--- Explain the problem briefly -->

At times it is necessary, particularly for cleanup plays, to remove all unneeded packages that were originally installed as dependencies. Currently this can be done via a `command: dnf -y autoremove`. However, it would be best to incorporate it into the dnf module.

<!---
##### STEPS TO REPRODUCE
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below 
```

```
-->

<!--- You can also paste gist.github.com links for larger files -->

<!---##### EXPECTED RESULTS
 What did you expect to happen when running the steps above? -->

<!---##### ACTUAL RESULTS
 What actually happened? If possible run with high verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below 
```

```
-->


_Copied from original issue: ansible/ansible-modules-extras#2629_


Discussion:
_From @ansibot on July 28, 2016 17:51_

<!-- @font: Thanks for filing this feature idea! -->

@ignatenkobrain, @DJMuggs, ping. This issue is waiting on your response.
[click here for bot help](https://github.com/jctanner/ansibullbot/blob/ISSUE_TRIAGE_CLASS/ISSUE_HELP.md)

<!--- boilerplate: issue_bug_report --->

_From @ignatenkobrain on July 28, 2016 17:56_

good idea!

_From @ignatenkobrain on July 28, 2016 17:57_

needs_contributor

_From @ansibot on December 6, 2016 23:5_

This repository has been locked. All new issues and pull requests should be filed in https://github.com/ansible/ansible

Please read through the <a href="http://docs.ansible.com/ansible/dev_guide/repomerge.html#move-issues-and-prs-to-new-repo">repomerge</a> page in the dev guide. The guide contains links to tools which automatically move your issue or pull request to the ansible/ansible repo.

<!-- boilerplate: repomerge -->

Resolving patch:
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
@@ -97,7 +97,16 @@
     version_added: "2.3"
     default: "/"
 
-notes: []
+  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)
+    required: false
+    choices: [ "yes", "no" ]
+    version_added: "2.4"
+
+notes: ["autoremove requires dnf >= 2.0.1"]
 # informational: requirements for nodes
 requirements:
   - "python >= 2.6"
@@ -144,11 +153,20 @@
   dnf:
     name: '@Development tools'
     state: present
+
+- name: Autoremove unneeded packages installed as dependencies
+  dnf:
+    autoremove: yes
+
+- name: Uninstall httpd but keep its dependencies
+  dnf:
+    name: httpd
+    state: absent
+    autoremove: no
 '''
 import os
 
 try:
-    import dnf
     import dnf
     import dnf.cli
     import dnf.const
@@ -161,6 +179,7 @@
 
 from ansible.module_utils.basic import AnsibleModule
 from ansible.module_utils.six import PY2
+from distutils.version import LooseVersion
 
 
 def _ensure_dnf(module):
@@ -314,11 +333,18 @@ def _install_remote_rpms(base, filenames):
         base.package_install(pkg)
 
 
-def ensure(module, base, state, names):
+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 is not None:
+        names = []
+        state = 'absent'
+
     if names == ['*'] and state == 'latest':
         base.upgrade_all()
     else:
@@ -398,6 +424,9 @@ def ensure(module, base, state, names):
 
         else:
             # state == absent
+            if autoremove is not None:
+                base.conf.clean_requirements_on_remove = autoremove
+
             if filenames:
                 module.fail_json(
                     msg="Cannot remove paths -- please specify package name.")
@@ -425,6 +454,9 @@ def ensure(module, base, state, names):
             # 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 '
@@ -460,7 +492,6 @@ def main():
         argument_spec=dict(
             name=dict(aliases=['pkg'], type='list'),
             state=dict(
-                default='installed',
                 choices=[
                     'absent', 'present', 'installed', 'removed', 'latest']),
             enablerepo=dict(type='list', default=[]),
@@ -469,14 +500,28 @@ def main():
             conf_file=dict(default=None, type='path'),
             disable_gpg_check=dict(default=False, type='bool'),
             installroot=dict(default='/', type='path'),
+            autoremove=dict(type='bool'),
         ),
-        required_one_of=[['name', 'list']],
-        mutually_exclusive=[['name', 'list']],
+        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'] is not None:
+        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'],
@@ -491,7 +536,7 @@ def main():
             module, params['conf_file'], params['disable_gpg_check'],
             params['disablerepo'], params['enablerepo'], params['installroot'])
 
-        ensure(module, base, params['state'], params['name'])
+        ensure(module, base, params['state'], params['name'], params['autoremove'])
 
 
 if __name__ == '__main__':


Test patch: