Repository: ansible/ansible

Issue:
file touch always 'changed' - [was: need a separate touch module]
##### ISSUE TYPE

feature idea

##### COMPONENT NAME

file module

##### ANSIBLE VERSION

ansible 1.7.2

##### OS / ENVIRONMENT

N/A

##### SUMMARY

`file` `touch` is useless, because it always completes with `changed` status

##### STEPS TO REPRODUCE

```
file: path=/var/log/php.log state=touch mode=0666
```

##### EXPECTED RESULTS

```
touch: path=/var/log/php.log mode=0666 timestamp=no
```

##### ACTUAL RESULTS

```
# doesn't work correctly, ignores ACL
copy: dest=/var/log/php.log content="" mode=0666 force=no
```


Copied from original issue: ansible/ansible-modules-core#170


file touch always 'changed' - [was: need a separate touch module]
##### ISSUE TYPE

feature idea

##### COMPONENT NAME

file module

##### ANSIBLE VERSION

ansible 1.7.2

##### OS / ENVIRONMENT

N/A

##### SUMMARY

`file` `touch` is useless, because it always completes with `changed` status

##### STEPS TO REPRODUCE

```
file: path=/var/log/php.log state=touch mode=0666
```

##### EXPECTED RESULTS

```
touch: path=/var/log/php.log mode=0666 timestamp=no
```

##### ACTUAL RESULTS

```
# doesn't work correctly, ignores ACL
copy: dest=/var/log/php.log content="" mode=0666 force=no
```


Copied from original issue: ansible/ansible-modules-core#170




Discussion:
From @techtonik on 2014-10-10T15:43:58Z

@ansibot Fixed.

From @mpdehaan on 2014-10-10T15:43:58Z

I don't believe we need a seperate module, and in fact as you have above, a parameter shown that decides whether to update the timestamp could be appropriate.

You could also do:
- shell: touch /path/to/file creates=/path/to/file

If you wanted to control the changed state.

From @tedder on 2014-10-10T15:43:58Z

seems like "file: state=touch" might work well for this?

From @techtonik on 2014-10-10T15:43:58Z

@tedder, it is already in description of a problem.

@mpdehaan, file is overburdened with parameters. It is not an easy module anymore.

BTW, I discovered one more way to do the same thing:

```
- file: path=/var/log/php.log state=touch mode=0666
  changed_when: False
```

From @mpdehaan on 2014-10-10T15:43:58Z

Strongly disagree with the need for a different module.

From @techtonik on 2014-10-10T15:43:58Z

`copy` ignores ACL, so its not a solution - see #192. Fixing description.

From @techtonik on 2014-10-10T15:43:58Z

```
- file: path=/var/log/php.log state=touch creates=/var/log/php.log
```

Another failure.

From @techtonik on 2014-10-10T15:43:58Z

```
- shell: touch /var/log/php.log creates=/var/log/php.log
```

file touch is useless.

From @mpdehaan on 2014-10-10T15:43:58Z

I'm not sure what useless means or what the "another failure" is ... Can you clarify?

From @techtonik on 2014-10-10T15:43:58Z

I mean that I need to create a file when it is not created. Honoring all default ACL and permissions set for the directory in which it happens.

From @techtonik on 2014-10-10T15:43:58Z

Only the shell command with creates argument worked correctly, and file touch is useless, because it doesn't allow to reach the default (expected) behaviour of shell command.

From @rbu on 2014-10-10T15:43:58Z

Do I understand correctly that this bug report is about differentiating between
- "touch file on every run (and set changed=True)" (current behavior of "state=touch") versus
- "ensure file exists and set Changed if the file did not exist and was created" (desired behavior of, let's say, "state=exists")?

From @techtonik on 2014-10-10T15:43:58Z

@rbu, yes.

From @Petrox on 2014-10-10T15:43:58Z

So what is the official solution, if I want
- a logfile to exist with zero or more bytes in it
- given ownership and rights
- change when the file ownership or rights needs to be changed 
- no touching (changed) if no change happened to those rights or ownership or file existence

Also it would be nice not to hack...

From @jmehnle on 2014-10-10T15:43:58Z

BTW, @mpdehaan, what's the reason `state=file` does not create the file? Unless there's a legitimate use case for that that I don't see, the behavior of that could be changed to create the file if it doesn't exist, but does _not_ update its timestamp (like `state=touch` does) if the file _does_ exist.

My use case is to ensure some log files exist (with whatever content), with the right ownership and permissions, so certain processes can write to the files but can't create _new_ files in the same directory. Currently it seems the most idiomatic way to do that is:

```
- file: path=/var/log/mylogfile state=touch owner=… group=… mode='ug=rw,o=r'
  changed_when: false
    # suppress bogus changed notifications.
```

Having to use `changed_when: false` seems bad style because it doesn't report a change on initial creation.

From @larstobi on 2014-10-10T15:43:58Z

I have the same scenario as @jmehnle. When I try the suggested solution of @mpdehaan of using a shell task with a creates parameter to create the file and then a file task to set permissions, I get a warning with a suggestion to use file with state=touch instead.

```
TASK [logstash : Create /folder/debug.log file unless already exists] ***
changed: [log0]
 [WARNING]: Consider using file module with state=touch rather than running touch 

TASK [logstash : Ensure permissions on /folder/debug.log] ***********
 changed: [log0]
```

On subsequent playbook runs, it prints ok without the warning:

```
TASK [logstash : Create /config/cloudstack-debug.log file unless already exists] ***
ok: [log0]

TASK [logstash : Ensure permissions on /config/cloudstack-debug.log] ***********
ok: [log0]
```

Here is the Ansible part:

```
- name: Create /folder/debug.log file unless already exists
  shell: touch /folder/debug.log creates=/folder/debug.log

- name: Ensure permissions on /folder/debug.log
  file: path=/folder/debug.log state=file
        owner=user group=user mode="0660"
```

How can I achieve this with the expected result and console output without the warning?

From @larstobi on 2014-10-10T15:43:58Z

About @jmehnle's suggestion to change `state=file` to create the file if not exists, that would be similar to how Puppet's _file_ type with the `ensure => file` metaparameter works. It would be the same with @rbu's `file=exists`.

> file will make sure it’s a normal file, and enables use of the content or source attribute.

https://docs.puppetlabs.com/puppet/latest/reference/type.html#file

From @supertylerc on 2014-10-10T15:43:58Z

This issue makes idempotent file creation operations exceedingly difficult.  Idempotent operations are a requirement for many where immutable infrastructure is not feasible.  I'm okay with the current functionality when I use Packer to build an image, but I'm not okay with it when I'm managing long-running hosts that must be managed with idempotent operations.

I could use `changed_when: false`, but this feels like a silly hack to work around a legitimate concern.  I could also use the `shell` module, or maybe even `command` for better sanity, but then there are those pesky warnings that I should be using the `file` module (and let's face it, I should be).

Would a pull request to make the `state` attribute not update the timestamps when the value is `touch` be accepted?  I know that there's probably some conflicting feelings about this--I want to have idempotent operations, others might want to preserve the functionality of the POSIX `touch` command, which is what the source code says it does.

From @supertylerc on 2014-10-10T15:43:58Z

I think making an `exists` value for `state` would also be acceptable and not (potentially) break existing customers' playbooks/roles.

So, I amend my previous query: would you be open to a pull request that implements `state=exists`, which would only set `changed` to `True` if the file was created, but otherwise not modify the file if it does not exist?

From @aporcano on 2014-10-10T15:43:58Z

I am using the following instead of changed_when in case it's helpful to anyone.

```
---
- hosts: "{{host_var}}"

  vars:
    redis_ports:
      - 6379
      - 6380
      - 6381
    redis_log: { basepath: '/var/log', owner: 'redis' }

  tasks:
    - stat: path={{ redis_log.basepath }}/redis_{{ item }}.log
      register: logstat
      with_items: "{{ redis_ports }}"
    - name: touch redis log
      file: path={{ redis_log.basepath }}/redis_{{ item.item }}.log state=touch owner={{ redis_log.owner }}
      when: item.stat.exists == False
      with_items: "{{ logstat.results }}"
```

From @LHSnow on 2014-10-10T15:43:58Z

Alternatively, use the content + force options of the copy module. Leaves the file untouched if it does exist, creates it empty if it doesn't (triggering a change).

`- copy: dest=/some/path content="" force=no`

From @bmalynovytch on 2014-10-10T15:43:58Z

A nicer way would be:

```
- file: path=/var/log/php.log mode=0666 state=touch
  register: touch_log
  changed_when: touch_log.diff.before.state != "file"
```

Change will only be reported if file is created by touch.

From @vamsifeb27 on 2014-10-10T15:43:58Z

- file: path=/var/log/php.log mode=0666 state=touch
  register: touch_log
  changed_when: touch_log.diff.before.state != "file"

This works but if permissions was changed it says (ok)  instead of (changed) 
which is not good
@ansibot Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information. 

Here are the items we could not find in your description:
- component name

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->
Hi, 
This issue is still open, is anyone has an explanation about using systematic "changed=True" in module.exit_json(dest=path, changed=True, diff=diff) at the end of the case touch in the file module ? 

https://github.com/ansible/ansible/blob/28b86b11485444d2dd46ed60dc8ae27fd2b81303/lib/ansible/modules/files/file.py

Is it possible, acceptable, correct, to set changed to false if the touch does not modify anything (except the access and modification time) ? like : 
<pre>
            elif prev_state in ('file', 'directory', 'hard'):
                try:
                    os.utime(b_path, None)
                except OSError as e:
                    module.fail_json(path=path, msg='Error while touching existing target: %s' % to_native(e, nonstring='simplerepr'))
>>>>>>          changed=False
            else:
</pre>

If not, I suggest a new feature for file module : touch a file if does not exists, change permissions if needed, do nothing otherwise.

cheers, 
lazouz


This is still bugged. This seems like it'd take 5 seconds to fix since I fixed it myself with a workaround.

Here it is. Add this to your touch task:

```
  register: touch_file
  changed_when: touch_file.size == 0

```
Well this is not an entire workaround, because if you keep creating the same empty file (a .gitkeep for example) it will still print "changed" but if you add 1 character into that file, then it won't display as changed anymore
Well, this module still needs some work, i'll try something this summer, but feel free if you have some time.
From @techtonik on 2014-10-10T15:43:58Z

@ansibot Fixed.

From @mpdehaan on 2014-10-10T15:43:58Z

I don't believe we need a seperate module, and in fact as you have above, a parameter shown that decides whether to update the timestamp could be appropriate.

You could also do:
- shell: touch /path/to/file creates=/path/to/file

If you wanted to control the changed state.

From @tedder on 2014-10-10T15:43:58Z

seems like "file: state=touch" might work well for this?

From @techtonik on 2014-10-10T15:43:58Z

@tedder, it is already in description of a problem.

@mpdehaan, file is overburdened with parameters. It is not an easy module anymore.

BTW, I discovered one more way to do the same thing:

```
- file: path=/var/log/php.log state=touch mode=0666
  changed_when: False
```

From @mpdehaan on 2014-10-10T15:43:58Z

Strongly disagree with the need for a different module.

From @techtonik on 2014-10-10T15:43:58Z

`copy` ignores ACL, so its not a solution - see #192. Fixing description.

From @techtonik on 2014-10-10T15:43:58Z

```
- file: path=/var/log/php.log state=touch creates=/var/log/php.log
```

Another failure.

From @techtonik on 2014-10-10T15:43:58Z

```
- shell: touch /var/log/php.log creates=/var/log/php.log
```

file touch is useless.

From @mpdehaan on 2014-10-10T15:43:58Z

I'm not sure what useless means or what the "another failure" is ... Can you clarify?

From @techtonik on 2014-10-10T15:43:58Z

I mean that I need to create a file when it is not created. Honoring all default ACL and permissions set for the directory in which it happens.

From @techtonik on 2014-10-10T15:43:58Z

Only the shell command with creates argument worked correctly, and file touch is useless, because it doesn't allow to reach the default (expected) behaviour of shell command.

From @rbu on 2014-10-10T15:43:58Z

Do I understand correctly that this bug report is about differentiating between
- "touch file on every run (and set changed=True)" (current behavior of "state=touch") versus
- "ensure file exists and set Changed if the file did not exist and was created" (desired behavior of, let's say, "state=exists")?

From @techtonik on 2014-10-10T15:43:58Z

@rbu, yes.

From @Petrox on 2014-10-10T15:43:58Z

So what is the official solution, if I want
- a logfile to exist with zero or more bytes in it
- given ownership and rights
- change when the file ownership or rights needs to be changed 
- no touching (changed) if no change happened to those rights or ownership or file existence

Also it would be nice not to hack...

From @jmehnle on 2014-10-10T15:43:58Z

BTW, @mpdehaan, what's the reason `state=file` does not create the file? Unless there's a legitimate use case for that that I don't see, the behavior of that could be changed to create the file if it doesn't exist, but does _not_ update its timestamp (like `state=touch` does) if the file _does_ exist.

My use case is to ensure some log files exist (with whatever content), with the right ownership and permissions, so certain processes can write to the files but can't create _new_ files in the same directory. Currently it seems the most idiomatic way to do that is:

```
- file: path=/var/log/mylogfile state=touch owner=… group=… mode='ug=rw,o=r'
  changed_when: false
    # suppress bogus changed notifications.
```

Having to use `changed_when: false` seems bad style because it doesn't report a change on initial creation.

From @larstobi on 2014-10-10T15:43:58Z

I have the same scenario as @jmehnle. When I try the suggested solution of @mpdehaan of using a shell task with a creates parameter to create the file and then a file task to set permissions, I get a warning with a suggestion to use file with state=touch instead.

```
TASK [logstash : Create /folder/debug.log file unless already exists] ***
changed: [log0]
 [WARNING]: Consider using file module with state=touch rather than running touch 

TASK [logstash : Ensure permissions on /folder/debug.log] ***********
 changed: [log0]
```

On subsequent playbook runs, it prints ok without the warning:

```
TASK [logstash : Create /config/cloudstack-debug.log file unless already exists] ***
ok: [log0]

TASK [logstash : Ensure permissions on /config/cloudstack-debug.log] ***********
ok: [log0]
```

Here is the Ansible part:

```
- name: Create /folder/debug.log file unless already exists
  shell: touch /folder/debug.log creates=/folder/debug.log

- name: Ensure permissions on /folder/debug.log
  file: path=/folder/debug.log state=file
        owner=user group=user mode="0660"
```

How can I achieve this with the expected result and console output without the warning?

From @larstobi on 2014-10-10T15:43:58Z

About @jmehnle's suggestion to change `state=file` to create the file if not exists, that would be similar to how Puppet's _file_ type with the `ensure => file` metaparameter works. It would be the same with @rbu's `file=exists`.

> file will make sure it’s a normal file, and enables use of the content or source attribute.

https://docs.puppetlabs.com/puppet/latest/reference/type.html#file

From @supertylerc on 2014-10-10T15:43:58Z

This issue makes idempotent file creation operations exceedingly difficult.  Idempotent operations are a requirement for many where immutable infrastructure is not feasible.  I'm okay with the current functionality when I use Packer to build an image, but I'm not okay with it when I'm managing long-running hosts that must be managed with idempotent operations.

I could use `changed_when: false`, but this feels like a silly hack to work around a legitimate concern.  I could also use the `shell` module, or maybe even `command` for better sanity, but then there are those pesky warnings that I should be using the `file` module (and let's face it, I should be).

Would a pull request to make the `state` attribute not update the timestamps when the value is `touch` be accepted?  I know that there's probably some conflicting feelings about this--I want to have idempotent operations, others might want to preserve the functionality of the POSIX `touch` command, which is what the source code says it does.

From @supertylerc on 2014-10-10T15:43:58Z

I think making an `exists` value for `state` would also be acceptable and not (potentially) break existing customers' playbooks/roles.

So, I amend my previous query: would you be open to a pull request that implements `state=exists`, which would only set `changed` to `True` if the file was created, but otherwise not modify the file if it does not exist?

From @aporcano on 2014-10-10T15:43:58Z

I am using the following instead of changed_when in case it's helpful to anyone.

```
---
- hosts: "{{host_var}}"

  vars:
    redis_ports:
      - 6379
      - 6380
      - 6381
    redis_log: { basepath: '/var/log', owner: 'redis' }

  tasks:
    - stat: path={{ redis_log.basepath }}/redis_{{ item }}.log
      register: logstat
      with_items: "{{ redis_ports }}"
    - name: touch redis log
      file: path={{ redis_log.basepath }}/redis_{{ item.item }}.log state=touch owner={{ redis_log.owner }}
      when: item.stat.exists == False
      with_items: "{{ logstat.results }}"
```

From @LHSnow on 2014-10-10T15:43:58Z

Alternatively, use the content + force options of the copy module. Leaves the file untouched if it does exist, creates it empty if it doesn't (triggering a change).

`- copy: dest=/some/path content="" force=no`

From @bmalynovytch on 2014-10-10T15:43:58Z

A nicer way would be:

```
- file: path=/var/log/php.log mode=0666 state=touch
  register: touch_log
  changed_when: touch_log.diff.before.state != "file"
```

Change will only be reported if file is created by touch.

From @vamsifeb27 on 2014-10-10T15:43:58Z

- file: path=/var/log/php.log mode=0666 state=touch
  register: touch_log
  changed_when: touch_log.diff.before.state != "file"

This works but if permissions was changed it says (ok)  instead of (changed) 
which is not good
@ansibot Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information. 

Here are the items we could not find in your description:
- component name

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->
Hi, 
This issue is still open, is anyone has an explanation about using systematic "changed=True" in module.exit_json(dest=path, changed=True, diff=diff) at the end of the case touch in the file module ? 

https://github.com/ansible/ansible/blob/28b86b11485444d2dd46ed60dc8ae27fd2b81303/lib/ansible/modules/files/file.py

Is it possible, acceptable, correct, to set changed to false if the touch does not modify anything (except the access and modification time) ? like : 
<pre>
            elif prev_state in ('file', 'directory', 'hard'):
                try:
                    os.utime(b_path, None)
                except OSError as e:
                    module.fail_json(path=path, msg='Error while touching existing target: %s' % to_native(e, nonstring='simplerepr'))
>>>>>>          changed=False
            else:
</pre>

If not, I suggest a new feature for file module : touch a file if does not exists, change permissions if needed, do nothing otherwise.

cheers, 
lazouz


This is still bugged. This seems like it'd take 5 seconds to fix since I fixed it myself with a workaround.

Here it is. Add this to your touch task:

```
  register: touch_file
  changed_when: touch_file.size == 0

```
Well this is not an entire workaround, because if you keep creating the same empty file (a .gitkeep for example) it will still print "changed" but if you add 1 character into that file, then it won't display as changed anymore
Well, this module still needs some work, i'll try something this summer, but feel free if you have some time.

Resolving patch:
diff --git a/lib/ansible/modules/files/file.py b/lib/ansible/modules/files/file.py
--- a/lib/ansible/modules/files/file.py
+++ b/lib/ansible/modules/files/file.py
@@ -76,6 +76,30 @@
     type: bool
     default: 'yes'
     version_added: "1.8"
+  modification_time:
+    description:
+      - This parameter indicates the time the file's modification time should be set to
+      - 'Should be C(preserve) when no modification is required, C(YYYYMMDDHHMM.SS) when using default time format, or C(now)'
+      - 'Default is None meaning that C(preserve) is the default for C(state=[file,directory,link,hard]) and C(now) is default for C(state=touch)'
+    version_added: "2.7"
+  modification_time_format:
+    description:
+      - 'When used with C(modification_time), indicates the time format that must be used.'
+      - 'Based on default Python format (see time.strftime doc)'
+    default: "%Y%m%d%H%M.%S"
+    version_added: "2.7"
+  access_time:
+    description:
+      - This parameter indicates the time the file's access time should be set to
+      - 'Should be C(preserve) when no modification is required, C(YYYYMMDDHHMM.SS) when using default time format, or C(now)'
+      - 'Default is None meaning that C(preserve) is the default for C(state=[file,directory,link,hard]) and C(now) is default for C(state=touch)'
+    version_added: "2.7"
+  access_time_format:
+    description:
+      - 'When used with C(access_time), indicates the time format that must be used.'
+      - 'Based on default Python format (see time.strftime doc)'
+    default: "%Y%m%d%H%M.%S"
+    version_added: "2.7"
 '''
 
 EXAMPLES = '''
@@ -116,11 +140,31 @@
     state: touch
     mode: "u+rw,g-wx,o-rwx"
 
+# touch again the same file, but dont change times
+# this makes the task idempotents
+- file:
+    path: /etc/foo.conf
+    state: touch
+    mode: "u+rw,g-wx,o-rwx"
+    modification_time: "preserve"
+    access_time: "preserve"
+
 # create a directory if it doesn't exist
 - file:
     path: /etc/some_directory
     state: directory
     mode: 0755
+
+# updates modification and access time of given file
+- file:
+    path: /etc/some_file
+    state: file
+    mode: 0755
+    modification_time: now
+    access_time: now
+'''
+RETURN = '''
+
 '''
 
 import errno
@@ -233,7 +277,7 @@ def get_state(path):
 
 
 # This should be moved into the common file utilities
-def recursive_set_attributes(b_path, follow, file_args):
+def recursive_set_attributes(b_path, follow, file_args, mtime, atime):
     changed = False
     for b_root, b_dirs, b_files in os.walk(b_path):
         for b_fsobj in b_dirs + b_files:
@@ -242,11 +286,14 @@ def recursive_set_attributes(b_path, follow, file_args):
                 tmp_file_args = file_args.copy()
                 tmp_file_args['path'] = to_native(b_fsname, errors='surrogate_or_strict')
                 changed |= module.set_fs_attributes_if_different(tmp_file_args, changed, expand=False)
+                changed |= update_timestamp_for_file(tmp_file_args['path'], mtime, atime)
+
             else:
                 # Change perms on the link
                 tmp_file_args = file_args.copy()
                 tmp_file_args['path'] = to_native(b_fsname, errors='surrogate_or_strict')
                 changed |= module.set_fs_attributes_if_different(tmp_file_args, changed, expand=False)
+                changed |= update_timestamp_for_file(tmp_file_args['path'], mtime, atime)
 
                 if follow:
                     b_fsname = os.path.join(b_root, os.readlink(b_fsname))
@@ -254,12 +301,13 @@ def recursive_set_attributes(b_path, follow, file_args):
                     if os.path.exists(b_fsname):
                         if os.path.isdir(b_fsname):
                             # Link is a directory so change perms on the directory's contents
-                            changed |= recursive_set_attributes(b_fsname, follow, file_args)
+                            changed |= recursive_set_attributes(b_fsname, follow, file_args, mtime, atime)
 
                         # Change perms on the file pointed to by the link
                         tmp_file_args = file_args.copy()
                         tmp_file_args['path'] = to_native(b_fsname, errors='surrogate_or_strict')
                         changed |= module.set_fs_attributes_if_different(tmp_file_args, changed, expand=False)
+                        changed |= update_timestamp_for_file(tmp_file_args['path'], mtime, atime)
     return changed
 
 
@@ -279,6 +327,70 @@ def initial_diff(path, state, prev_state):
 #
 
 
+def get_timestamp_for_time(formatted_time, time_format):
+    if formatted_time == 'preserve':
+        return None
+    elif formatted_time == 'now':
+        current_time = time.time()
+        return current_time
+    else:
+        try:
+            struct = time.strptime(formatted_time, time_format)
+            struct_time = time.mktime(struct)
+        except (ValueError, OverflowError) as e:
+            raise AnsibleModuleError(results={'msg': 'Error while obtaining timestamp for time %s using format %s: %s'
+                                              % (formatted_time, time_format, to_native(e, nonstring='simplerepr'))})
+
+        return struct_time
+
+
+def update_timestamp_for_file(path, mtime, atime, diff=None):
+    # If both parameters are None, nothing to do
+    if mtime is None and atime is None:
+        return False
+
+    try:
+        previous_mtime = os.stat(path).st_mtime
+        previous_atime = os.stat(path).st_atime
+
+        if mtime is None:
+            mtime = previous_mtime
+
+        if atime is None:
+            atime = previous_atime
+
+        # If both timestamps are already ok, nothing to do
+        if mtime == previous_mtime and atime == previous_atime:
+            return False
+
+        os.utime(path, (atime, mtime))
+
+        if diff is not None:
+            if 'before' not in diff:
+                diff['before'] = {}
+            if 'after' not in diff:
+                diff['after'] = {}
+            if mtime != previous_mtime:
+                diff['before']['mtime'] = previous_mtime
+                diff['after']['mtime'] = mtime
+            if atime != previous_atime:
+                diff['before']['atime'] = previous_atime
+                diff['after']['atime'] = atime
+    except OSError as e:
+        raise AnsibleModuleError(results={'msg': 'Error while updating modification or access time: %s'
+                                          % to_native(e, nonstring='simplerepr'), 'path': path})
+    return True
+
+
+def keep_backward_compatibility_on_timestamps(parameter, state):
+    if state in ['file', 'hard', 'directory', 'link'] and parameter is None:
+        return 'preserve'
+    elif state == 'touch' and parameter is None:
+        return 'now'
+    else:
+        return parameter
+
+
 def execute_diff_peek(path):
     """Take a guess as to whether a file is a binary file"""
     b_path = to_bytes(path, errors='surrogate_or_strict')
@@ -324,51 +436,31 @@ def ensure_absent(path):
     return result
 
 
-def execute_touch(path, follow):
+def execute_touch(path, follow, timestamps):
     b_path = to_bytes(path, errors='surrogate_or_strict')
     prev_state = get_state(b_path)
-
-    # Unfortunately, touch always changes the file because it updates file's timestamp
-    result = {'dest': path, 'changed': True}
+    changed = False
+    result = {'dest': path}
+    mtime = get_timestamp_for_time(timestamps['modification_time'], timestamps['modification_time_format'])
+    atime = get_timestamp_for_time(timestamps['access_time'], timestamps['access_time_format'])
 
     if not module.check_mode:
         if prev_state == 'absent':
             # Create an empty file if the filename did not already exist
             try:
                 open(b_path, 'wb').close()
+                changed = True
             except (OSError, IOError) as e:
                 raise AnsibleModuleError(results={'msg': 'Error, could not touch target: %s'
                                                          % to_native(e, nonstring='simplerepr'),
                                                   'path': path})
 
-        elif prev_state in ('file', 'directory', 'hard'):
-            # Update the timestamp if the file already existed
-            try:
-                os.utime(b_path, None)
-            except OSError as e:
-                raise AnsibleModuleError(results={'msg': 'Error while touching existing target: %s'
-                                                         % to_native(e, nonstring='simplerepr'),
-                                                  'path': path})
-
-        elif prev_state == 'link' and follow:
-            # Update the timestamp of the pointed to file
-            b_link_target = os.readlink(b_path)
-            try:
-                os.utime(b_link_target, None)
-            except OSError as e:
-                raise AnsibleModuleError(results={'msg': 'Error while touching existing target: %s'
-                                                         % to_native(e, nonstring='simplerepr'),
-                                                  'path': path})
-
-        else:
-            raise AnsibleModuleError(results={'msg': 'Can only touch files, directories, and'
-                                                     ' hardlinks (%s is %s)' % (path, prev_state)})
-
         # Update the attributes on the file
         diff = initial_diff(path, 'touch', prev_state)
         file_args = module.load_file_common_arguments(module.params)
         try:
-            module.set_fs_attributes_if_different(file_args, True, diff, expand=False)
+            changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
+            changed |= update_timestamp_for_file(file_args['path'], mtime, atime, diff)
         except SystemExit as e:
             if e.code:
                 # We take this to mean that fail_json() was called from
@@ -378,14 +470,17 @@ def execute_touch(path, follow):
                     os.remove(b_path)
             raise
 
+        result['changed'] = changed
         result['diff'] = diff
     return result
 
 
-def ensure_file_attributes(path, follow):
+def ensure_file_attributes(path, follow, timestamps):
     b_path = to_bytes(path, errors='surrogate_or_strict')
     prev_state = get_state(b_path)
     file_args = module.load_file_common_arguments(module.params)
+    mtime = get_timestamp_for_time(timestamps['modification_time'], timestamps['modification_time_format'])
+    atime = get_timestamp_for_time(timestamps['access_time'], timestamps['access_time_format'])
 
     if prev_state != 'file':
         if follow and prev_state == 'link':
@@ -402,13 +497,16 @@ def ensure_file_attributes(path, follow):
 
     diff = initial_diff(path, 'file', prev_state)
     changed = module.set_fs_attributes_if_different(file_args, False, diff, expand=False)
+    changed |= update_timestamp_for_file(file_args['path'], mtime, atime, diff)
     return {'path': path, 'changed': changed, 'diff': diff}
 
 
-def ensure_directory(path, follow, recurse):
+def ensure_directory(path, follow, recurse, timestamps):
     b_path = to_bytes(path, errors='surrogate_or_strict')
     prev_state = get_state(b_path)
     file_args = module.load_file_common_arguments(module.params)
+    mtime = get_timestamp_for_time(timestamps['modification_time'], timestamps['modification_time_format'])
+    atime = get_timestamp_for_time(timestamps['access_time'], timestamps['access_time_format'])
 
     # For followed symlinks, we need to operate on the target of the link
     if follow and prev_state == 'link':
@@ -450,6 +548,7 @@ def ensure_directory(path, follow, recurse):
                     tmp_file_args = file_args.copy()
                     tmp_file_args['path'] = curpath
                     changed = module.set_fs_attributes_if_different(tmp_file_args, changed, diff, expand=False)
+                    changed |= update_timestamp_for_file(file_args['path'], mtime, atime, diff)
         except Exception as e:
             raise AnsibleModuleError(results={'msg': 'There was an issue creating %s as requested:'
                                                      ' %s' % (curpath, to_native(e)),
@@ -466,19 +565,20 @@ def ensure_directory(path, follow, recurse):
     #
 
     changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
-
+    changed |= update_timestamp_for_file(file_args['path'], mtime, atime, diff)
     if recurse:
-        changed |= recursive_set_attributes(b_path, follow, file_args)
+        changed |= recursive_set_attributes(b_path, follow, file_args, mtime, atime)
 
     return {'path': path, 'changed': changed, 'diff': diff}
 
 
-def ensure_symlink(path, src, follow, force):
+def ensure_symlink(path, src, follow, force, timestamps):
     b_path = to_bytes(path, errors='surrogate_or_strict')
     b_src = to_bytes(src, errors='surrogate_or_strict')
     prev_state = get_state(b_path)
     file_args = module.load_file_common_arguments(module.params)
-
+    mtime = get_timestamp_for_time(timestamps['modification_time'], timestamps['modification_time_format'])
+    atime = get_timestamp_for_time(timestamps['access_time'], timestamps['access_time_format'])
     # source is both the source of a symlink or an informational passing of the src for a template module
     # or copy module, even if this module never uses it, it is needed to key off some things
     if src is None:
@@ -581,15 +681,18 @@ def ensure_symlink(path, src, follow, force):
                     ' set to False to avoid this.')
     else:
         changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
+        changed |= update_timestamp_for_file(file_args['path'], mtime, atime, diff)
 
     return {'dest': path, 'src': src, 'changed': changed, 'diff': diff}
 
 
-def ensure_hardlink(path, src, follow, force):
+def ensure_hardlink(path, src, follow, force, timestamps):
     b_path = to_bytes(path, errors='surrogate_or_strict')
     b_src = to_bytes(src, errors='surrogate_or_strict')
     prev_state = get_state(b_path)
     file_args = module.load_file_common_arguments(module.params)
+    mtime = get_timestamp_for_time(timestamps['modification_time'], timestamps['modification_time_format'])
+    atime = get_timestamp_for_time(timestamps['access_time'], timestamps['access_time_format'])
 
     # src is the source of a hardlink.  We require it if we are creating a new hardlink
     if src is None and not os.path.exists(b_path):
@@ -688,6 +791,7 @@ def ensure_hardlink(path, src, follow, force):
         return {'dest': path, 'src': src, 'changed': changed, 'diff': diff}
 
     changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
+    changed |= update_timestamp_for_file(file_args['path'], mtime, atime, diff)
 
     return {'dest': path, 'src': src, 'changed': changed, 'diff': diff}
 
@@ -706,6 +810,10 @@ def main():
             follow=dict(required=False, default=True, type='bool'),  # Note: Different default than file_common_args
             _diff_peek=dict(default=None),  # Internal use only, for internal checks in the action plugins
             src=dict(required=False, default=None, type='path'),  # Note: Should not be in file_common_args in future
+            modification_time=dict(required=False, default=None),
+            modification_time_format=dict(required=False, default='%Y%m%d%H%M.%S'),
+            access_time=dict(required=False, default=None),
+            access_time_format=dict(required=False, default='%Y%m%d%H%M.%S'),
         ),
         add_file_common_args=True,
         supports_check_mode=True
@@ -723,21 +831,27 @@ def main():
     path = params['path']
     src = params['src']
 
+    timestamps = {}
+    timestamps['modification_time'] = keep_backward_compatibility_on_timestamps(params['modification_time'], state)
+    timestamps['modification_time_format'] = params['modification_time_format']
+    timestamps['access_time'] = keep_backward_compatibility_on_timestamps(params['access_time'], state)
+    timestamps['access_time_format'] = params['access_time_format']
+
     # short-circuit for diff_peek
     if params['_diff_peek'] is not None:
         appears_binary = execute_diff_peek(to_bytes(path, errors='surrogate_or_strict'))
         module.exit_json(path=path, changed=False, appears_binary=appears_binary)
 
     if state == 'file':
-        result = ensure_file_attributes(path, follow)
+        result = ensure_file_attributes(path, follow, timestamps)
     elif state == 'directory':
-        result = ensure_directory(path, follow, recurse)
+        result = ensure_directory(path, follow, recurse, timestamps)
     elif state == 'link':
-        result = ensure_symlink(path, src, follow, force)
+        result = ensure_symlink(path, src, follow, force, timestamps)
     elif state == 'hard':
-        result = ensure_hardlink(path, src, follow, force)
+        result = ensure_hardlink(path, src, follow, force, timestamps)
     elif state == 'touch':
-        result = execute_touch(path, follow)
+        result = execute_touch(path, follow, timestamps)
     elif state == 'absent':
         result = ensure_absent(path)
 


Test patch:
diff --git a/test/integration/targets/file/tasks/directory_as_dest.yml b/test/integration/targets/file/tasks/directory_as_dest.yml
--- a/test/integration/targets/file/tasks/directory_as_dest.yml
+++ b/test/integration/targets/file/tasks/directory_as_dest.yml
@@ -262,6 +262,35 @@
       - 'file8_dir_stat["stat"].isdir'
       - 'file8_dir_stat["stat"]["mtime"] != file8_initial_dir_stat["stat"]["mtime"]'
 
+- name: Get initial stat info to compare with later
+  stat:
+    path: '{{ output_dir }}/sub1'
+    follow: False
+  register: file11_initial_dir_stat
+
+- name: Use touch with directory as dest and keep mtime and atime
+  file:
+    dest: '{{output_dir}}/sub1'
+    state: touch
+    force: False
+    modification_time: preserve
+    access_time: preserve
+  register: file11_result
+
+- name: Get stat info to show the directory has not been changed
+  stat:
+    path: '{{ output_dir }}/sub1'
+    follow: False
+  register: file11_dir_stat
+
+- name: verify that the directory has not been updated
+  assert:
+    that:
+      - 'file11_result is not changed'
+      - 'file11_dir_stat["stat"].isdir'
+      - 'file11_dir_stat["stat"]["mtime"] == file11_initial_dir_stat["stat"]["mtime"]'
+      - 'file11_dir_stat["stat"]["atime"] == file11_initial_dir_stat["stat"]["atime"]'
+
 #
 # State=directory realizes that the directory already exists and does nothing
 #
diff --git a/test/integration/targets/file/tasks/main.yml b/test/integration/targets/file/tasks/main.yml
--- a/test/integration/targets/file/tasks/main.yml
+++ b/test/integration/targets/file/tasks/main.yml
@@ -117,6 +117,24 @@
 - name: change ownership and group
   file: path={{output_dir}}/baz.txt owner=1234 group=1234
 
+- name: Get stat info to check atime later
+  stat: path={{output_dir}}/baz.txt
+  register: file_attributes_result_5_before
+
+- name: updates access time
+  file: path={{output_dir}}/baz.txt access_time=now
+  register: file_attributes_result_5
+
+- name: Get stat info to check atime later
+  stat: path={{output_dir}}/baz.txt
+  register: file_attributes_result_5_after
+
+- name: verify that the file was marked as changed and atime changed
+  assert:
+    that:
+      - "file_attributes_result_5 is changed"
+      - "file_attributes_result_5_after['stat']['atime'] != file_attributes_result_5_before['stat']['atime']"
+
 - name: setup a tmp-like directory for ownership test
   file: path=/tmp/worldwritable mode=1777 state=directory