Repository: ansible/ansible

Issue:
Ansible makes it difficult to copy files recursively and preserve permissions
##### Issue Type:
- Bug Report
##### Ansible Version:

ansible 2.0.1.0
  config file = 
  configured module search path = Default w/o overrides
##### Ansible Configuration:

n/a
##### Environment:

n/a (Alpine Linux)
##### Summary:

The `copy` module does not preserve file permissions, and the `synchronize` module does not respect `ansible_connection`.
##### Steps To Reproduce:

Let's say you have a role `myrole` that contains:

   $ pwd
   .../roles/myrole
   $ find . -type f
   ./tasks/main.yml
   ./files/adirectory/ascript.sh
   ./files/adirectory/afile.txt
   $ ls -l files/adirectory/ascript.sh
   -rwx-r-xr-x ... 

Try to copy the files using `copy`:
- copy:
     src: adirectory/
     dest: /tmp/adirectory/

Result:

The files are copied, but the script is mode `644`.

Try to copy the files using the `synchronize` module:

```
- synchronize:
    src: adirectory/
    dest: /tmp/adirectory/
```

Result:

```
fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "Failed to find required executable ssh"}
```

Well of _course_ it failed; I'm using a `local` connection because
`ssh` isn't even installed.
##### Expected Results:

I expected one of the above mechanisms to successfully copy a directory tree while preserving permissions.
##### Actual Results:

Sadness and surprise.



Discussion:
I see that 2.1.x seems to have fixed the `synchronize` module for `local` connections.  Yay!  But I still think `copy` should be able to do this.

Well, nice to see fix in the `larsks/ansible` repository! But guys, how about fix this in upstream?

```
192.168.31.110 | FAILED! => {
    "changed": false, 
    "failed": true, 
    "msg": "Failed to find required executable ssh"
}
```
This error, we need to install openssh-clients. I have solved it.
```
yum -y install openssh-clients
```
```
ansible test_hosts -m synchronize -a 'src=/tmp/helloworld.txt dest=/tmp/'
192.168.31.110 | SUCCESS => {
    "changed": true, 
    "cmd": "/usr/bin/rsync --delay-updates -F --compress --archive --rsh 'ssh  -S none -o StrictHostKeyChecking=no' --out-format='<<CHANGED>>%i %n%L' \"/tmp/helloworld.txt\" \"root@192.168.31.110:/tmp/\"", 
    "msg": "<f+++++++++ helloworld.txt\n", 
    "rc": 0, 
    "stdout_lines": [
        "<f+++++++++ helloworld.txt"
    ]
}
```
@larsks 
@larsks 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 --->

Resolving patch:
diff --git a/lib/ansible/modules/files/synchronize.py b/lib/ansible/modules/files/synchronize.py
--- a/lib/ansible/modules/files/synchronize.py
+++ b/lib/ansible/modules/files/synchronize.py
@@ -347,6 +347,14 @@ def substitute_controller(path):
     return path
 
 
+def is_rsh_needed(source, dest):
+    if source.startswith('rsync://') or dest.startswith('rsync://'):
+        return False
+    if ':' in source or ':' in dest:
+        return True
+    return False
+
+
 def main():
     module = AnsibleModule(
         argument_spec = dict(
@@ -465,7 +473,7 @@ def main():
     if source.startswith('rsync://') and dest.startswith('rsync://'):
         module.fail_json(msg='either src or dest must be a localhost', rc=1)
 
-    if not source.startswith('rsync://') and not dest.startswith('rsync://'):
+    if is_rsh_needed(source, dest):
         ssh_cmd = [module.get_bin_path('ssh', required=True), '-S', 'none']
         if private_key is not None:
             ssh_cmd.extend(['-i', private_key])
diff --git a/lib/ansible/plugins/action/synchronize.py b/lib/ansible/plugins/action/synchronize.py
--- a/lib/ansible/plugins/action/synchronize.py
+++ b/lib/ansible/plugins/action/synchronize.py
@@ -216,6 +216,11 @@ def run(self, tmp=None, task_vars=None):
         except KeyError:
             dest_host = dest_host_inventory_vars.get('ansible_ssh_host', inventory_hostname)
 
+        dest_host_ids = [hostid for hostid in (dest_host_inventory_vars.get('inventory_hostname'),
+                                               dest_host_inventory_vars.get('ansible_host'),
+                                               dest_host_inventory_vars.get('ansible_ssh_host'))
+                         if hostid is not None]
+
         localhost_ports = set()
         for host in C.LOCALHOST:
             localhost_vars = task_vars['hostvars'].get(host, {})
@@ -231,9 +236,9 @@ def run(self, tmp=None, task_vars=None):
         # host rsync puts the files on.  This is about *rsync's connection*,
         # not about the ansible connection to run the module.
         dest_is_local = False
-        if not delegate_to and remote_transport is False:
+        if delegate_to is None and remote_transport is False:
             dest_is_local = True
-        elif delegate_to and delegate_to == dest_host:
+        elif delegate_to is not None and delegate_to in dest_host_ids:
             dest_is_local = True
 
         # CHECK FOR NON-DEFAULT SSH PORT
@@ -245,7 +250,7 @@ def run(self, tmp=None, task_vars=None):
         # Set use_delegate if we are going to run rsync on a delegated host
         # instead of localhost
         use_delegate = False
-        if dest_host == delegate_to:
+        if delegate_to is not None and delegate_to in dest_host_ids:
             # edge case: explicit delegate and dest_host are the same
             # so we run rsync on the remote machine targeting its localhost
             # (itself)


Test patch: