Repository: ansible/ansible-modules-core

Issue:
apt module: update_cache=yes does nothing
Hello,
I was trying to use the update_cache feature of the apt module to run apt-get update without installing a package. So I've got this basic task:

```
- name: Updating APT cache
   apt: update_cache=yes
```

When the task runs, it says 'ok' but the cache isn't updated at all.
There seem to be the same behavior if update_cache=yes is added with an actual package installation.

I'm using ansible v1.9.1 on CentOS 7 to manage hosts running Ubuntu 14.04LTS.



Discussion:
Bump, this issue looks like it effects all current v1.9.x versions including v1.9.2-0.1.rc1: 
- https://github.com/ansible/ansible-modules-core/blob/016ad8a1cb29d54353d14503fc07c7f98c4cb853/packaging/os/apt.py#L558-L589
- https://github.com/ansible/ansible-modules-core/blob/d56c219a158b4893303f126c5e3dfba89e908cd8/packaging/os/apt.py#L558-L589

Hey there,

Thanks for the fix :) :+1: 

Could be nice to also add the default value for `cache_valid_time` in the [docs](http://docs.ansible.com/apt_module.html). Was wondering what it was when trying to use it earlier.

@Bengrunt  I updated the PR to now document that the `cache_valid_time` is 0 by default.

Awesome :)
I think it's gonna help a lot of people.

Hi,

I have a similar problem in Ansible 1.9.3. When I start a fresh instance in AWS EC2 and try to install packages (Ubuntu 14.04) using:

```
- name: Install packages
    apt: pkg={{item}} state=present update_cache=True
    with_items:
    - python-virtualenv
    ...
```

It always fails on the first try with saying:

```
msg: No package matching 'python-virtualenv' is available
```

When I start that playbook the second time from that task it succeeds. If I run:

```
- name: Manually update packages list
    apt: update_caches=True
```

it does nothing and the package list is not refreshed. The only scenario when refreshing packages works on the first try is when I run it explicitly:

```
- name: Manually update packages list (problem with update_cache)
    command: apt-get update
```

@wysekm if you don't mind pulling in a patch, this change fixed the issue for us https://github.com/ansible/ansible-modules-core/pull/1517 -- it's currently a pending PR to core.

Same issue with 1.9.4, running on Mac, with Ubuntu 14.04 instances

The apt module has undergone a few revisions since 1.9.1 and the cache updating mechanisms have been port of those revisions.

If the system does not have python-apt, the module will attempt to run 'apt-get update && apt-get install python-apt -y -q' before proceeding to evaluate the rest of the module args. That command could alter the expected timestamp on the cache because it doesn't track the previous timestamp.

If python-apt was installed, the module then does a stat on /var/lib/apt/periodic/update-success-stamp. Failing that, a stat is done on /var/lib/apt/lists. If a timestamp is obtained and the current time is less than the timestamp + the cache valid time, apt.cache.update() is called. The update() returns True, and will cause the timestamp on /var/lib/apt/lists to change. The call to .update is tracked by the 'cache_updated' key in the results dictionary but does not factor into the overall change state of the module. 

The current behavior described above seems logical as the module is intended to install packages and updating the cache is a tangential feature. If someone disagrees with that logic, we are open to changing it but would require that to go through the ansible proposals process.

If you believe this ticket is not resolved, or have further questions, please let us know by stopping by one of the two mailing lists, as appropriate:
- https://groups.google.com/forum/#!forum/ansible-project - for user questions, tips, and tricks
- https://groups.google.com/forum/#!forum/ansible-devel - for strategy, future planning, and questions about writing code

Because this project is very active, we're unlikely to see comments on closed tickets, though the mailing list is a great way to get involved or discuss this one further.

Thanks!

@jctanner I don't believe this issue should be closed as this is still a problem. If you or the core team have time I'd appreciate review on [ https://github.com/ansible/ansible-modules-core/pull/1517 ]. That pr has been in limbo for some time without review and actually addresses this issue by properly capturing the apt update time.


Resolving patch:
diff --git a/packaging/os/apt.py b/packaging/os/apt.py
--- a/packaging/os/apt.py
+++ b/packaging/os/apt.py
@@ -47,9 +47,9 @@
     choices: [ "yes", "no" ]
   cache_valid_time:
     description:
-      - If C(update_cache) is specified and the last run is less or equal than I(cache_valid_time) seconds ago, the C(update_cache) gets skipped.
+      - Update the apt cache if its older than the I(cache_valid_time). This option is set in seconds.
     required: false
-    default: no
+    default: 0
   purge:
     description:
      - Will force purging of configuration files if the module state is set to I(absent).
@@ -696,12 +696,37 @@ def download(module, deb):
     return deb
 
 
+def get_cache_mtime():
+    """Return mtime of a valid apt cache file.
+    Stat the apt cache file and if no cache file is found return 0
+    :returns: ``int``
+    """
+    if os.path.exists(APT_UPDATE_SUCCESS_STAMP_PATH):
+        return os.stat(APT_UPDATE_SUCCESS_STAMP_PATH).st_mtime
+    elif os.path.exists(APT_LISTS_PATH):
+        return os.stat(APT_LISTS_PATH).st_mtime
+    else:
+        return 0
+
+
+def get_updated_cache_time():
+    """Return the mtime time stamp and the updated cache time.
+    Always retrieve the mtime of the apt cache or set the `cache_mtime`
+    variable to 0
+    :returns: ``tuple``
+    """
+    cache_mtime = get_cache_mtime()
+    mtimestamp = datetime.datetime.fromtimestamp(cache_mtime)
+    updated_cache_time = int(time.mktime(mtimestamp.timetuple()))
+    return mtimestamp, updated_cache_time
+
+
 def main():
     module = AnsibleModule(
         argument_spec = dict(
             state = dict(default='present', choices=['installed', 'latest', 'removed', 'absent', 'present', 'build-dep']),
             update_cache = dict(default=False, aliases=['update-cache'], type='bool'),
-            cache_valid_time = dict(type='int'),
+            cache_valid_time = dict(type='int', default=0),
             purge = dict(default=False, type='bool'),
             package = dict(default=None, aliases=['pkg', 'name'], type='list'),
             deb = dict(default=None, type='path'),
@@ -772,30 +797,16 @@ def main():
             # reopen cache w/ modified config
             cache.open(progress=None)
 
+
+        mtimestamp, updated_cache_time = get_updated_cache_time()
+        # Cache valid time is default 0, which will update the cache if
+        #  needed and `update_cache` was set to true
+        updated_cache = False
         if p['update_cache']:
-            # Default is: always update the cache
-            cache_valid = False
             now = datetime.datetime.now()
-            if p.get('cache_valid_time', False):
-                try:
-                    mtime = os.stat(APT_UPDATE_SUCCESS_STAMP_PATH).st_mtime
-                except:
-                    # Looks like the update-success-stamp is not available
-                    # Fallback: Checking the mtime of the lists
-                    try:
-                        mtime = os.stat(APT_LISTS_PATH).st_mtime
-                    except:
-                        # No mtime could be read. We update the cache to be safe
-                        mtime = False
-
-                if mtime:
-                    tdelta = datetime.timedelta(seconds=p['cache_valid_time'])
-                    mtimestamp = datetime.datetime.fromtimestamp(mtime)
-                    if mtimestamp + tdelta >= now:
-                        cache_valid = True
-                        updated_cache_time = int(time.mktime(mtimestamp.timetuple()))
-
-            if cache_valid is not True:
+            tdelta = datetime.timedelta(seconds=p['cache_valid_time'])
+            if not mtimestamp + tdelta >= now:
+                # Retry to update the cache up to 3 times
                 for retry in range(3):
                     try:
                         cache.update()
@@ -806,12 +817,16 @@ def main():
                     module.fail_json(msg='Failed to update apt cache.')
                 cache.open(progress=None)
                 updated_cache = True
-                updated_cache_time = int(time.mktime(now.timetuple()))
+                mtimestamp, updated_cache_time = get_updated_cache_time()
+
+            # If theres nothing else to do exit. This will set state as
+            #  changed based on if the cache was updated.
             if not p['package'] and not p['upgrade'] and not p['deb']:
-                module.exit_json(changed=False, cache_updated=updated_cache, cache_update_time=updated_cache_time)
-        else:
-            updated_cache = False
-            updated_cache_time = 0
+                module.exit_json(
+                    changed=updated_cache,
+                    cache_updated=updated_cache,
+                    cache_update_time=updated_cache_time
+                )
 
         force_yes = p['force']
 
@@ -843,16 +858,33 @@ def main():
                 state_upgrade = True
             if p['state'] == 'build-dep':
                 state_builddep = True
-            result = install(module, packages, cache, upgrade=state_upgrade,
-                    default_release=p['default_release'],
-                    install_recommends=install_recommends,
-                    force=force_yes, dpkg_options=dpkg_options,
-                    build_dep=state_builddep, autoremove=autoremove,
-                    only_upgrade=p['only_upgrade'],
-                    allow_unauthenticated=allow_unauthenticated)
-            (success, retvals) = result
-            retvals['cache_updated']=updated_cache
-            retvals['cache_update_time']=updated_cache_time
+
+            success, retvals = install(
+                module,
+                packages,
+                cache,
+                upgrade=state_upgrade,
+                default_release=p['default_release'],
+                install_recommends=install_recommends,
+                force=force_yes,
+                dpkg_options=dpkg_options,
+                build_dep=state_builddep,
+                autoremove=autoremove,
+                only_upgrade=p['only_upgrade'],
+                allow_unauthenticated=allow_unauthenticated
+            )
+
+            # Store if the cache has been updated
+            retvals['cache_updated'] = updated_cache
+            # Store when the update time was last
+            retvals['cache_update_time'] = updated_cache_time
+            # If the cache was updated and the general state change was set to
+            #  False make sure that the change in cache state is acurately
+            #  updated by setting the general changed state to the same as
+            #  the cache state.
+            if updated_cache and not retvals['changed']:
+                retvals['changed'] = updated_cache
+
             if success:
                 module.exit_json(**retvals)
             else:


Test patch: