Repository: ansible/ansible

Issue:
apt-get autoremove doesn't remove obsolete packages

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
apt
##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
2.2.1
```

##### 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.
-->
Ubuntu
##### SUMMARY
<!--- Explain the problem briefly -->

##### 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.
-->
Use this playbook and then run "apt-get autoremove":
```yaml
- hosts: localhost
  tasks:
    - apt:
        autoremove: yes
```
It shows no change:
```
PLAY RECAP ************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0
```
Then the command 'apt-get autoremove' still show some packages:
```
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages will be REMOVED:
  linux-headers-4.4.0-59 linux-headers-4.4.0-59-generic linux-headers-4.4.0-62 linux-headers-4.4.0-62-generic linux-image-4.4.0-34-generic linux-image-4.4.0-59-generic linux-image-4.4.0-62-generic
0 upgraded, 0 newly installed, 7 to remove and 24 not upgraded.
After this operation, 341 MB disk space will be freed.
Do you want to continue? [Y/n]
```


<!--- 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 obsolete packages should be removed.
##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
The obsolete packages was not removed.
<!--- Paste verbatim command output between quotes below -->
```

```



Discussion:
#22154 
fixed by #22232 
It's working for me but it always say changed even if there are no pkg removed 
I can confirm that apt: with autoremove: yes does not actually remove or clean those packages that it should. I'm on Ansible 2.2.0 (ansible-2.2.2.0-1ppa-xenial in Ubuntu 16.04 control host) and my playbook is targeting Ubuntu 16.04 host where the Ansible/apt-module is used to update and autoremove packages.
Tried also with the Ansible 2.4.0 (git://github.com/ansible/ansible.git) devel-branch after doing: "git checkout devel", and "make", and "sudo make install" and using that to playbook, which has:
```
- name: srvenv - Autoclean and update apt cache
  become: yes
  apt:
    autoremove: yes
    purge: yes
    force: yes
    update_cache: yes
```

still the apt-module does not delete, remove or autoclean the obsolete packages, but they remain in the system:
 ```
$ sudo apt-get -f install
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following packages were automatically installed and are no longer required:
  linux-headers-4.4.0-45 linux-headers-4.4.0-59 linux-headers-4.4.0-62 linux-headers-4.4.0-63 linux-headers-4.4.0-64 linux-headers-4.4.0-65
  linux-headers-4.4.0-66 linux-headers-4.4.0-66-generic linux-image-4.4.0-66-generic linux-image-extra-4.4.0-66-generic
Use 'sudo apt autoremove' to remove them.
```
I can confirm this behaviour, running Ansible 2.3.0.0 from the repo on Ubuntu 16.04.

Wether `autoclean` nor `autoremove` seem to have any effect on the OS.
Same here, Ansible v2.3.0.0 from Ubuntu 14.04 onto Ubuntu 14.04: apt module doesn't find old kernels when ```/usr/bin/apt-get autoremove``` does.

BTW autoclean should not be an alias for autoremove, considering the apt-get autoremove and autoclean options are totally different from each other.

Resolving patch:
diff --git a/lib/ansible/modules/packaging/os/apt.py b/lib/ansible/modules/packaging/os/apt.py
--- a/lib/ansible/modules/packaging/os/apt.py
+++ b/lib/ansible/modules/packaging/os/apt.py
@@ -115,11 +115,18 @@
   autoremove:
     description:
       - If C(yes), remove unused dependency packages for all module states except I(build-dep). It can also be used as the only option.
+      - Previous to version 2.4, autoclean was also an alias for autoremove, now it is its own separate command. See documentation for further information.
     required: false
     default: no
     choices: [ "yes", "no" ]
-    aliases: [ 'autoclean']
     version_added: "2.1"
+  autoclean:
+    description:
+      - If C(yes), cleans the local repository of retrieved package files that can no longer be downloaded.
+    required: false
+    default: no
+    choices: [ "yes", "no" ]
+    version_added: "2.4"
   only_upgrade:
     description:
       - Only upgrade a package if it is already installed.
@@ -206,6 +213,14 @@
 - name: Install a .deb package from the internet.
   apt:
     deb: https://example.com/python-ppq_0.1-1_all.deb
+
+- name: Remove useless packages from the cache
+  apt:
+    autoclean: yes
+
+- name: Remove dependencies that are no longer required
+  apt:
+    autoremove: yes
 '''
 
 RETURN = '''
@@ -668,6 +683,35 @@ def remove(m, pkgspec, cache, purge=False, force=False,
         m.exit_json(changed=True, stdout=out, stderr=err, diff=diff)
 
 
+def cleanup(m, purge=False, force=False, operation=None,
+            dpkg_options=expand_dpkg_options(DPKG_OPTIONS)):
+    if force:
+        force_yes = '--force-yes'
+    else:
+        force_yes = ''
+
+    if purge:
+        purge = '--purge'
+    else:
+        purge = ''
+
+    if m.check_mode:
+        check_arg = '--simulate'
+    else:
+        check_arg = ''
+
+    cmd = "%s -y %s %s %s %s %s" % (APT_GET_CMD, dpkg_options, purge, force_yes, operation, check_arg)
+
+    rc, out, err = m.run_command(cmd)
+    if m._diff:
+        diff = parse_diff(out)
+    else:
+        diff = {}
+    if rc:
+        m.fail_json(msg="'apt-get %s' failed: %s" % (operation, err), stdout=out, stderr=err, rc=rc)
+    m.exit_json(changed=bool(len(diff)), stdout=out, stderr=err, diff=diff)
+
+
 def upgrade(m, mode="yes", force=False, default_release=None,
             dpkg_options=expand_dpkg_options(DPKG_OPTIONS)):
     if m.check_mode:
@@ -815,7 +859,8 @@ def main():
             force=dict(default='no', type='bool'),
             upgrade=dict(choices=['no', 'yes', 'safe', 'full', 'dist']),
             dpkg_options=dict(default=DPKG_OPTIONS),
-            autoremove=dict(type='bool', aliases=['autoclean']),
+            autoremove=dict(type='bool', default='no'),
+            autoclean=dict(type='bool', default='no'),
             only_upgrade=dict(type='bool', default=False),
             allow_unauthenticated=dict(default='no', aliases=['allow-unauthenticated'], type='bool'),
         ),
@@ -860,6 +905,7 @@ def main():
     allow_unauthenticated = p['allow_unauthenticated']
     dpkg_options = expand_dpkg_options(p['dpkg_options'])
     autoremove = p['autoremove']
+    autoclean = p['autoclean']
 
     # Deal with deprecated aliases
     if p['state'] == 'installed':
@@ -924,8 +970,9 @@ def main():
                         allow_unauthenticated=allow_unauthenticated,
                         force=force_yes, dpkg_options=p['dpkg_options'])
 
-        packages = [package for package in p['package'] if package != '*']
-        all_installed = '*' in p['package']
+        unfiltered_packages = p['package'] or ()
+        packages = [package for package in unfiltered_packages if package != '*']
+        all_installed = '*' in unfiltered_packages
         latest = p['state'] == 'latest'
 
         if latest and all_installed:
@@ -940,6 +987,12 @@ def main():
                 if latest and '=' in package:
                     module.fail_json(msg='version number inconsistent with state=latest: %s' % package)
 
+        if not packages:
+            if autoclean:
+                cleanup(module, p['purge'], force=force_yes, operation='autoclean', dpkg_options=dpkg_options)
+            if autoremove:
+                cleanup(module, p['purge'], force=force_yes, operation='autoremove', dpkg_options=dpkg_options)
+
         if p['state'] in ('latest', 'present', 'build-dep'):
             state_upgrade = False
             state_builddep = False


Test patch:
diff --git a/test/integration/targets/apt/tasks/apt.yml b/test/integration/targets/apt/tasks/apt.yml
--- a/test/integration/targets/apt/tasks/apt.yml
+++ b/test/integration/targets/apt/tasks/apt.yml
@@ -160,3 +160,12 @@
     that:
         - "not apt_result.changed"
         - "apt_result.failed"
+
+- name: autoclean during install
+  apt: pkg=hello state=present autoclean=yes
+
+- name: autoclean
+  apt: autoclean=yes
+
+- name: autoremove
+  apt: autoremove=yes