diff --git a/changelogs/fragments/file-touch-non-owner.yaml b/changelogs/fragments/file-touch-non-owner.yaml new file mode 100644 index 0000000000..b70f61c93e --- /dev/null +++ b/changelogs/fragments/file-touch-non-owner.yaml @@ -0,0 +1,2 @@ +bugfixes: +- file - Allow state=touch on file the user does not own https://github.com/ansible/ansible/issues/50943 diff --git a/lib/ansible/modules/files/file.py b/lib/ansible/modules/files/file.py index c61318bcc2..1c4c837566 100644 --- a/lib/ansible/modules/files/file.py +++ b/lib/ansible/modules/files/file.py @@ -194,6 +194,11 @@ class ParameterError(AnsibleModuleError): pass +class Sentinel(object): + def __new__(cls, *args, **kwargs): + return cls + + def _ansible_excepthook(exc_type, exc_value, tb): # Using an exception allows us to catch it if the calling code knows it can recover if issubclass(exc_type, AnsibleModuleError): @@ -332,8 +337,7 @@ 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 + return Sentinel else: try: struct = time.strptime(formatted_time, time_format) @@ -346,25 +350,43 @@ def get_timestamp_for_time(formatted_time, time_format): 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 + # When mtime and atime are set to 'now', rely on utime(path, None) which does not require ownership of the file + # https://github.com/ansible/ansible/issues/50943 + if mtime is Sentinel and atime is Sentinel: + # It's not exact but we can't rely on os.stat(path).st_mtime after setting os.utime(path, None) as it may + # not be updated. Just use the current time for the diff values + mtime = atime = time.time() - if mtime is None: - mtime = previous_mtime + previous_mtime = os.stat(path).st_mtime + previous_atime = os.stat(path).st_atime - if atime is None: - atime = previous_atime + set_time = None + else: + # If both parameters are None 'preserve', nothing to do + if mtime is None and atime is None: + return False - # If both timestamps are already ok, nothing to do - if mtime == previous_mtime and atime == previous_atime: - return False + previous_mtime = os.stat(path).st_mtime + previous_atime = os.stat(path).st_atime - os.utime(path, (atime, mtime)) + if mtime is None: + mtime = previous_mtime + elif mtime is Sentinel: + mtime = time.time() + + if atime is None: + atime = previous_atime + elif atime is Sentinel: + atime = time.time() + + # If both timestamps are already ok, nothing to do + if mtime == previous_mtime and atime == previous_atime: + return False + + set_time = (atime, mtime) + + os.utime(path, set_time) if diff is not None: if 'before' not in diff: diff --git a/test/integration/targets/file/tasks/main.yml b/test/integration/targets/file/tasks/main.yml index a8e903f4be..e8ac8a1c4d 100644 --- a/test/integration/targets/file/tasks/main.yml +++ b/test/integration/targets/file/tasks/main.yml @@ -488,6 +488,37 @@ that: - result.mode == '0444' +# https://github.com/ansible/ansible/issues/50943 +# Need to use /tmp as nobody can't access output_dir at all +- name: create file as root with all write permissions + file: dest=/tmp/write_utime state=touch mode=0666 owner={{ansible_user}} + +- block: + - name: get previous time + stat: path=/tmp/write_utime + register: previous_time + + - name: touch file as nobody + file: dest=/tmp/write_utime state=touch + become: True + become_user: nobody + register: result + + - name: get new time + stat: path=/tmp/write_utime + register: current_time + + always: + - name: remove test utime file + file: path=/tmp/write_utime state=absent + +- name: assert touch file as nobody + assert: + that: + - result is changed + - current_time.stat.atime > previous_time.stat.atime + - current_time.stat.mtime > previous_time.stat.mtime + # Follow + recursive tests - name: create a toplevel directory file: path={{output_dir}}/test_follow_rec state=directory mode=0755