Repository: ansible/ansible

Issue:
Ansible 2.0 Breaks synchronize functionality, and completely changed the behavior.
Issue type:
Bug report

Ansible version:
devel

Environment:

Unrelated but
Host: Mac 10.10.3 
Target: Ubuntu 10.4

The first issue is:
In 1.9, when I want to sync two folders on the remote server, I do:

```
- name: Sync app folder
  synchronize:
    src: "/home/ubuntu/test1"
    dest:  "/home/ubuntu/test2"
  delegate_to: "{{ inventory_hostname }}"
  sudo: yes
```

Now, it keeps giving me permission denied error.

The second issue is:
It looks like now it defaults to sync two folder on the remote server even if I don't specify delegate_to, in 1.9.2, without delegate_to, src will be my local, and dest will be my remote.

Thanks



Discussion:
Confirmed the second issue.  Here's a small playbook that shows the problems:

```
---
# Localhost is a fedora box
- hosts: rhel6-test
  vars:
    device_host: rhel6-test
    top_dir: /var/tmp
  tasks:
    - name: Push fedora file to rhel host
      synchronize:
        src: "/etc/redhat-release"
        dest: "/var/tmp/fedora-rel"
        mode: push
    - name: Pull redhat file to fedora host
      synchronize:
        src: "/etc/redhat-release"
        dest: "/var/tmp/redhat-rel"
        mode: pull
```

In current devel, both of those plays are copying the remote /etc/redhat-release file to a file on the remote machine.  So the remote machine ends up with /var/tmp/fedora-rel and /var/tmp/redhat-rel and both of those are the /etc/redhat-release from the rhel6 box, _not_ the one from the fedora box that ansible-playbook is being run from.

Yes, the first issue and the second are actually related, since it tries to sync two folders on remote, and I do not have any ssh info on the remote, when src tries to go to ubuntu@ip:dest, it fails, and gives me permission denied error.

I also tried to setup my ssh info on the remote server, and then it succeeded. Therefore, that is the issue.

Small update on my debugging of this -- in 1.9.x the tasks run the ansible module (not just the action plugin) on the controller rather than the remote host.  In 2.0 it's running the ansible module on the remote host.  Will need to see what is happening with delegate_to to determine if there's a reason that the ansible module exists and try to find the specialcase code in 1.9 that handles this.

This works if delegate_to: localhost is explicitly stated.  jimi-c's theory is that in v2 we've already built the connection by the time the synchronize action plugin's run() method is called.  So all of the fancy stuff it tries to do to modify how the connection is setup isn't having an effect.  Will try to have the synchronize action plugin create its own connection object and use that instead.

delegating to $other host instead of localhost is also working.  That would seem to validate jimi-c's theory.


Resolving patch:
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
@@ -18,9 +18,11 @@
 from __future__ import (absolute_import, division, print_function)
 __metaclass__ = type
 
+import sys
 import os.path
 
 from ansible.plugins.action import ActionBase
+from ansible.plugins import connection_loader
 from ansible.utils.boolean import boolean
 from ansible import constants
 
@@ -81,47 +83,59 @@ def run(self, tmp=None, task_vars=dict()):
                 transport_overridden = True
                 self._play_context.become = False
 
-        src  = self._task.args.get('src', None)
-        dest = self._task.args.get('dest', None)
         use_ssh_args = self._task.args.pop('use_ssh_args', None)
 
-        # FIXME: this doesn't appear to be used anywhere?
-        local_rsync_path = task_vars.get('ansible_rsync_path')
+        # Parameter name needed by the ansible module
+        self._task.args['_local_rsync_path'] = task_vars.get('ansible_rsync_path') or 'rsync'
 
         # from the perspective of the rsync call the delegate is the localhost
-        src_host  = '127.0.0.1'
+        src_host = '127.0.0.1'
         dest_host = task_vars.get('ansible_ssh_host') or task_vars.get('inventory_hostname')
 
-        # allow ansible_ssh_host to be templated
+        ### FIXME: do we still need to explicitly template ansible_ssh_host here in v2?
+
         dest_is_local = dest_host in ['127.0.0.1', 'localhost']
 
+
         # CHECK FOR NON-DEFAULT SSH PORT
         dest_port = task_vars.get('ansible_ssh_port') or self._task.args.get('dest_port') or 22
 
-        # edge case: explicit delegate and dest_host are the same
+        # CHECK DELEGATE HOST INFO
+        use_delegate = False
+
         if dest_host == task_vars.get('delegate_to'):
+            # edge case: explicit delegate and dest_host are the same
             dest_host = '127.0.0.1'
+            use_delegate = True
+        else:
+            if 'hostvars' in task_vars:
+                if task_vars.get('delegate_to') in task_vars['hostvars'] and original_transport != 'local':
+                    # use a delegate host instead of localhost
+                    use_delegate = True
 
-        # SWITCH SRC AND DEST PER MODE
-        if self._task.args.get('mode', 'push') == 'pull':
-            (dest_host, src_host) = (src_host, dest_host)
-
-        # CHECK DELEGATE HOST INFO
-        use_delegate = False
-        # FIXME: not sure if this is in connection info yet or not...
-        #if conn.delegate != conn.host:
-        #    if 'hostvars' in task_vars:
-        #        if conn.delegate in task_vars['hostvars'] and original_transport != 'local':
-        #            # use a delegate host instead of localhost
-        #            use_delegate = True
-
-        # COMPARE DELEGATE, HOST AND TRANSPORT                             
+        # COMPARE DELEGATE, HOST AND TRANSPORT
         process_args = False
         if not dest_host is src_host and original_transport != 'local':
             # interpret and task_vars remote host info into src or dest
             process_args = True
 
+        # SWITCH SRC AND DEST PER MODE
+        if self._task.args.get('mode', 'push') == 'pull':
+            (dest_host, src_host) = (src_host, dest_host)
+
+        # Delegate to localhost as the source of the rsync unless we've been
+        # told (via delegate_to) that a different host is the source of the
+        # rsync
+        if not use_delegate:
+            # Create a connection to localhost to run rsync on
+            ### FIXME: Do we have to dupe stdin or is this sufficient?
+            new_stdin = self._connection._new_stdin
+            new_connection = connection_loader.get('local', self._play_context, new_stdin)
+            self._connection = new_connection
+
         # MUNGE SRC AND DEST PER REMOTE_HOST INFO
+        src  = self._task.args.get('src', None)
+        dest = self._task.args.get('dest', None)
         if process_args or use_delegate:
 
             user = None
@@ -131,7 +145,7 @@ def run(self, tmp=None, task_vars=dict()):
 
                 if not use_delegate or not user:
                     user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user
-                
+
             if use_delegate:
                 # FIXME
                 private_key = task_vars.get('ansible_ssh_private_key_file') or self._play_context.private_key_file
@@ -152,8 +166,12 @@ def run(self, tmp=None, task_vars=dict()):
                 src  = self._process_origin(src_host, src, user)
                 dest = self._process_remote(dest_host, dest, user)
 
-            self._task.args['src'] = src
-            self._task.args['dest'] = dest
+        self._task.args['src'] = src
+        self._task.args['dest'] = dest
+
+        # Remove mode as it is handled purely in this action module
+        if 'mode' in self._task.args:
+            del self._task.args['mode']
 
         # Allow custom rsync path argument.
         rsync_path = self._task.args.get('rsync_path', None)
@@ -169,13 +187,7 @@ def run(self, tmp=None, task_vars=dict()):
         if use_ssh_args:
             self._task.args['ssh_args'] = constants.ANSIBLE_SSH_ARGS
 
-        # Remove mode as it is handled purely in this action module
-        if 'mode' in self._task.args:
-            del self._task.args['mode']
-
-
         # run the module and store the result
         result = self._execute_module('synchronize', task_vars=task_vars)
 
         return result
-


Test patch: