Repository: ansible/ansible

Issue:
template module doesn't show diffs if dest is a directory but doesn't end in "/"
##### ISSUE TYPE

 - Bug Report

##### COMPONENT NAME

template

##### ANSIBLE VERSION

```
ansible 2.4.0 (devel e1d896052d) last updated 2017/05/09 13:24:40 (GMT -400)
  config file = /ansible/dom/ansible.cfg
  configured module search path = [u'/home/jsmift/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/jsmift/src/ansible/lib/ansible
  executable location = /home/jsmift/src/ansible/bin/ansible
  python version = 2.7.12 (default, Sep  1 2016, 22:14:00) [GCC 4.8.3 20140911 (Red Hat 4.8.3-9)]
```

##### CONFIGURATION

None that seem relevant.

##### OS / ENVIRONMENT

Amazon Linux AMI release 2016.09

##### SUMMARY

If you use the template module and the dest parameter specifies a directory, and the directory name doesn't end with a slash, and you run the playbook with --diff, no diffs are displayed.

##### STEPS TO REPRODUCE

Create a file 'foo.txt' containing this one line:

```{{ ansible_date_time['epoch'] }}```

Run this playbook with --diff:

```yaml
- hosts: localhost
  tasks:
    - template: src=foo.txt dest=/tmp
    - pause: seconds=1
    - setup:
    - template: src=foo.txt dest=/tmp/
    - pause: seconds=1
    - setup:
    - template: src=foo.txt dest=/tmp/foo.txt
```

##### EXPECTED RESULTS

The output for each task should show that /tmp/foo.txt will be changed, and a diff showing the previous contents of the file and the current timestamp.

##### ACTUAL RESULTS

The output for the first 'template' task (where the dest parameter is a directory but doesn't end in a slash) says the file will be changed, but doesn't show a diff; the second and third 'template' tasks show the diffs as expected.

```
PLAY [localhost] ***************************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [template] ****************************************************************
changed: [localhost]

TASK [pause] *******************************************************************
Pausing for 1 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

TASK [setup] *******************************************************************
ok: [localhost]

TASK [template] ****************************************************************
--- before: /tmp/foo.txt
+++ after: dynamically generated
@@ -1 +1 @@
-1494350696
+1494350698

changed: [localhost]

TASK [pause] *******************************************************************
Pausing for 1 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

TASK [setup] *******************************************************************
ok: [localhost]

TASK [template] ****************************************************************
--- before: /tmp/foo.txt
+++ after: dynamically generated
@@ -1 +1 @@
-1494350698
+1494350700

changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=8    changed=3    unreachable=0    failed=0
```



Discussion:

Resolving patch:
diff --git a/lib/ansible/plugins/action/template.py b/lib/ansible/plugins/action/template.py
--- a/lib/ansible/plugins/action/template.py
+++ b/lib/ansible/plugins/action/template.py
@@ -97,9 +97,14 @@ def run(self, tmp=None, task_vars=None):
 
         directory_prepended = False
         if dest.endswith(os.sep):
+            # Optimization.  trailing slash means we know it's a directory
             directory_prepended = True
-            base = os.path.basename(source)
-            dest = os.path.join(dest, base)
+            dest = self._connection._shell.join_path(dest, os.path.basename(source))
+        else:
+            # Find out if it's a directory
+            dest_stat = self._execute_remote_stat(dest, task_vars, True, tmp=tmp)
+            if dest_stat['exists'] and dest_stat['isdir']:
+                dest = self._connection._shell.join_path(dest, os.path.basename(source))
 
         # template the source data locally & get ready to transfer
         b_source = to_bytes(source)


Test patch: