From 4f664f8ff6b3647e681ed1f74d96eace2ee7f114 Mon Sep 17 00:00:00 2001
From: Toshio Kuratomi <a.badger@gmail.com>
Date: Thu, 3 May 2018 17:50:43 -0700
Subject: [PATCH] Fix for file module with symlinks to nonexistent target
 (#39635)

* Fix for file module with symlinks to nonexistent target

When creating a symlink to a nonexistent target, creating the symlink
would work but subsequent runs of the task would fail because it was
trying to operate on the target instead of the symlink.

Fixes #39558
---
 changelogs/fragments/file-nonexistent-link.yaml |  5 +++++
 lib/ansible/modules/files/file.py               | 13 +++++++++++--
 test/integration/targets/file/tasks/main.yml    |  9 +++++++++
 3 files changed, 25 insertions(+), 2 deletions(-)
 create mode 100644 changelogs/fragments/file-nonexistent-link.yaml

diff --git a/changelogs/fragments/file-nonexistent-link.yaml b/changelogs/fragments/file-nonexistent-link.yaml
new file mode 100644
index 00000000000..67a799b1402
--- /dev/null
+++ b/changelogs/fragments/file-nonexistent-link.yaml
@@ -0,0 +1,5 @@
+---
+bugfixes:
+  - file module - Fix error when running a task which assures a symlink to
+    a nonexistent file exists for the second and subsequent times
+    (https://github.com/ansible/ansible/issues/39558)
diff --git a/lib/ansible/modules/files/file.py b/lib/ansible/modules/files/file.py
index a861690bf4f..608ab83a615 100644
--- a/lib/ansible/modules/files/file.py
+++ b/lib/ansible/modules/files/file.py
@@ -354,7 +354,6 @@ def main():
         module.exit_json(path=path, changed=changed, diff=diff)
 
     elif state in ('link', 'hard'):
-
         if not os.path.islink(b_path) and os.path.isdir(b_path):
             relpath = path
         else:
@@ -442,7 +441,16 @@ def main():
         if module.check_mode and not os.path.exists(b_path):
             module.exit_json(dest=path, src=src, changed=changed, diff=diff)
 
-        changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
+        # Whenever we create a link to a nonexistent target we know that the nonexistent target
+        # cannot have any permissions set on it.  Skip setting those and emit a warning (the user
+        # can set follow=False to remove the warning)
+        if (state == 'link' and params['follow'] and os.path.islink(params['path']) and
+                not os.path.exists(file_args['path'])):
+            module.warn('Cannot set fs attributes on a non-existent symlink target. follow should be'
+                        ' set to False to avoid this.')
+        else:
+            changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
+
         module.exit_json(dest=path, src=src, changed=changed, diff=diff)
 
     elif state == 'touch':
@@ -475,5 +483,6 @@ def main():
 
     module.fail_json(path=path, msg='unexpected position reached')
 
+
 if __name__ == '__main__':
     main()
diff --git a/test/integration/targets/file/tasks/main.yml b/test/integration/targets/file/tasks/main.yml
index 67d3a892242..4db6637e74a 100644
--- a/test/integration/targets/file/tasks/main.yml
+++ b/test/integration/targets/file/tasks/main.yml
@@ -303,6 +303,15 @@
     that:
       - "file13_result.changed == true"
 
+- name: Prove idempotence of force creation soft link to non existent
+  file: src=/noneexistent dest={{output_dir}}/soft2.txt state=link force=yes
+  register: file13a_result
+
+- name: verify that the link to nonexistent is idempotent
+  assert:
+    that:
+      - "file13a_result.changed == false"
+
 - name: remove directory foobar
   file: path={{output_dir}}/foobar state=absent
   register: file14_result