Repository: ansible/ansible

Issue:
Template module broken when modifying symlinks
If you attempt to template a file that's a symlink and the file is already up to date, the code path for the template module attempts to invoke the file module. This is done right here: https://github.com/ansible/ansible/blob/devel/lib/ansible/runner/action_plugins/template.py#L158

You can see right before the module invocation it attempts to wipe the `src` parameter from the module arguments. Unfortunately, if you use complex_args, it doesn't wipe that attribute from there. So the `src` parameter continues to be passed to the file module. This then ends up hitting the following error: https://github.com/ansible/ansible-modules-core/blob/release1.8.2/files/file.py#L264

I've tested this on ansible 1.8.2 and 1.7.2 and it appears to be present in both.



Discussion:
@jsternberg Are you able to test whether https://github.com/ansible/ansible/commit/8fba66b4b541007ef9805bb7254fa3433ae42f3b fixes things for you?  I didn't try to reproduce your error conditions, just took a look at the code with your analysis in mind and thought that this patch should fix it.  I'll try and work up an integration test to go along with this tonight but I'd love independent confirmation that I'm on the right track.

It looks like that will fix it. Give me a bit to try it out.

Looks good! The fix works with my test case.


Resolving patch:
diff --git a/lib/ansible/runner/action_plugins/template.py b/lib/ansible/runner/action_plugins/template.py
--- a/lib/ansible/runner/action_plugins/template.py
+++ b/lib/ansible/runner/action_plugins/template.py
@@ -145,6 +145,7 @@ def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **
             # the module to follow links.  When doing that, we have to set
             # original_basename to the template just in case the dest is
             # a directory.
+            module_args = ''
             new_module_args = dict(
                 src=None,
                 original_basename=os.path.basename(source),
@@ -154,6 +155,6 @@ def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **
             # rely on the file module to report its changed status
             if self.runner.noop_on_check(inject):
                 new_module_args['CHECKMODE'] = True
-            module_args = utils.merge_module_args(module_args, new_module_args)
-            return self.runner._execute_module(conn, tmp, 'file', module_args, inject=inject, complex_args=complex_args)
+            options.update(new_module_args)
+            return self.runner._execute_module(conn, tmp, 'file', module_args, inject=inject, complex_args=options)
 


Test patch:
diff --git a/test/integration/roles/test_template/tasks/main.yml b/test/integration/roles/test_template/tasks/main.yml
--- a/test/integration/roles/test_template/tasks/main.yml
+++ b/test/integration/roles/test_template/tasks/main.yml
@@ -96,3 +96,42 @@
       - "dir_attrs.stat.uid != 0"
       - "dir_attrs.stat.pw_name == 'nobody'"
       - "dir_attrs.stat.mode == '0755'"
+
+- name: make a symlink to the templated file
+  file:
+    path: '{{ output_dir }}/foo.symlink'
+    src: '{{ output_dir }}/foo.templated'
+    state: link
+
+- name: check that templating the symlink results in the file being templated
+  template:
+    src: foo.j2
+    dest: '{{output_dir}}/foo.symlink'
+    mode: 0600
+    follow: True
+  register: template_result
+
+- assert:
+    that:
+      - "template_result.changed == True"
+
+- name: check that the file has the correct attributes
+  stat: path={{output_dir | expanduser}}/template-dir/foo.j2
+  register: file_attrs
+
+- assert:
+    that:
+      - "file_attrs.stat.mode == '0600'"
+
+- name: check that templating the symlink again makes no changes
+  template:
+    src: foo.j2
+    dest: '{{output_dir}}/foo.symlink'
+    mode: 0600
+    follow: True
+  register: template_result
+
+- assert:
+    that:
+      - "template_result.changed == False"
+