Repository: ansible/ansible

Issue:
sychronize module could wrap rsync with sshpass in certain cases
##### ISSUE TYPE
Feature Idea

##### COMPONENT NAME
core

##### ANSIBLE VERSION
```
2.2
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
N/A

##### SUMMARY
The action plugin detects the inventory data and connection type then juggles the connection to local and sets the remote host args for rsync. If the remote host required a password for auth and the user gave a password via the available methods, we would wrap ssh calls with sshpass. However, we completely ignore that in the action plugin for synchronize where we would probably know that a password is needed based on contexts. We should look into detecting that situation and wrapping rsync with sshpass.

##### STEPS TO REPRODUCE
1. Add an inventory host that would require an ssh password.
2. use synchronize to copy some files to that host
3. get password prompt

##### EXPECTED RESULTS
sshpass somewhere in the the exec arguments and sending the password when the prompt occurs.

##### ACTUAL RESULTS
```
TASK [synchronize] *************************************************************
task path: /home/jtanner/workspace/issues/AP-gmuniz.synchronize/site.yml:4
<10.0.0.106> ESTABLISH LOCAL CONNECTION FOR USER: jtanner
<10.0.0.106> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1467812727.41-11476211114503 `" && echo ansible-tmp-1467812727.41-11476211114503="` echo $HOME/.ansible/tmp/ansible-tmp-1467812727.41-11476211114503 `" ) && sleep 0'
<10.0.0.106> PUT /tmp/tmpvRshr4 TO /home/jtanner/.ansible/tmp/ansible-tmp-1467812727.41-11476211114503/synchronize
<10.0.0.106> EXEC /bin/sh -c 'LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /home/jtanner/.ansible/tmp/ansible-tmp-1467812727.41-11476211114503/synchronize; rm -rf "/home/jtanner/.ansible/tmp/ansible-tmp-1467812727.41-11476211114503/" > /dev/null 2>&1 && sleep 0'
passuser@10.0.0.106's password:
```


sychronize module could wrap rsync with sshpass in certain cases
##### ISSUE TYPE
Feature Idea

##### COMPONENT NAME
core

##### ANSIBLE VERSION
```
2.2
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
N/A

##### SUMMARY
The action plugin detects the inventory data and connection type then juggles the connection to local and sets the remote host args for rsync. If the remote host required a password for auth and the user gave a password via the available methods, we would wrap ssh calls with sshpass. However, we completely ignore that in the action plugin for synchronize where we would probably know that a password is needed based on contexts. We should look into detecting that situation and wrapping rsync with sshpass.

##### STEPS TO REPRODUCE
1. Add an inventory host that would require an ssh password.
2. use synchronize to copy some files to that host
3. get password prompt

##### EXPECTED RESULTS
sshpass somewhere in the the exec arguments and sending the password when the prompt occurs.

##### ACTUAL RESULTS
```
TASK [synchronize] *************************************************************
task path: /home/jtanner/workspace/issues/AP-gmuniz.synchronize/site.yml:4
<10.0.0.106> ESTABLISH LOCAL CONNECTION FOR USER: jtanner
<10.0.0.106> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1467812727.41-11476211114503 `" && echo ansible-tmp-1467812727.41-11476211114503="` echo $HOME/.ansible/tmp/ansible-tmp-1467812727.41-11476211114503 `" ) && sleep 0'
<10.0.0.106> PUT /tmp/tmpvRshr4 TO /home/jtanner/.ansible/tmp/ansible-tmp-1467812727.41-11476211114503/synchronize
<10.0.0.106> EXEC /bin/sh -c 'LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /home/jtanner/.ansible/tmp/ansible-tmp-1467812727.41-11476211114503/synchronize; rm -rf "/home/jtanner/.ansible/tmp/ansible-tmp-1467812727.41-11476211114503/" > /dev/null 2>&1 && sleep 0'
passuser@10.0.0.106's password:
```




Discussion:
This approach sounds good to me FWIW.

Linking for future review on this topic: https://github.com/ritzk/ansible/commit/f7b50a2a2889b67fe09fbb7b48b448ddf9eac7ef

Any ideas if this could get developed anytime soon? 
Yes. Just submit a pull request.
This approach sounds good to me FWIW.

Linking for future review on this topic: https://github.com/ritzk/ansible/commit/f7b50a2a2889b67fe09fbb7b48b448ddf9eac7ef

Any ideas if this could get developed anytime soon? 
Yes. Just submit a pull request.

Resolving patch:
diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py
--- a/lib/ansible/module_utils/basic.py
+++ b/lib/ansible/module_utils/basic.py
@@ -2681,7 +2681,7 @@ def _clean_args(self, args):
 
     def run_command(self, args, check_rc=False, close_fds=True, executable=None, data=None, binary_data=False, path_prefix=None, cwd=None,
                     use_unsafe_shell=False, prompt_regex=None, environ_update=None, umask=None, encoding='utf-8', errors='surrogate_or_strict',
-                    expand_user_and_vars=True):
+                    expand_user_and_vars=True, pass_fds=None, before_communicate_callback=None):
         '''
         Execute a command, returns rc, stdout, and stderr.
 
@@ -2724,6 +2724,13 @@ def run_command(self, args, check_rc=False, close_fds=True, executable=None, dat
             are expanded before running the command. When ``True`` a string such as
             ``$SHELL`` will be expanded regardless of escaping. When ``False`` and
             ``use_unsafe_shell=False`` no path or variable expansion will be done.
+        :kw pass_fds: When running on python3 this argument
+            dictates which file descriptors should be passed
+            to an underlying ``Popen`` constructor.
+        :kw before_communicate_callback: This function will be called
+            after ``Popen`` object will be created
+            but before communicating to the process.
+            (``Popen`` object will be passed to callback as a first argument)
         :returns: A 3-tuple of return code (integer), stdout (native string),
             and stderr (native string).  On python2, stdout and stderr are both
             byte strings.  On python3, stdout and stderr are text strings converted
@@ -2824,6 +2831,8 @@ def run_command(self, args, check_rc=False, close_fds=True, executable=None, dat
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
         )
+        if PY3 and pass_fds:
+            kwargs["pass_fds"] = pass_fds
 
         # store the pwd
         prev_dir = os.getcwd()
@@ -2846,6 +2855,8 @@ def run_command(self, args, check_rc=False, close_fds=True, executable=None, dat
             if self._debug:
                 self.log('Executing: ' + self._clean_args(args))
             cmd = subprocess.Popen(args, **kwargs)
+            if before_communicate_callback:
+                before_communicate_callback(cmd)
 
             # the communication logic here is essentially taken from that
             # of the _communicate() function in ssh.py
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
@@ -305,8 +305,9 @@
 
 
 import os
+import errno
 
-from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.basic import AnsibleModule, to_bytes
 from ansible.module_utils.six.moves import shlex_quote
 
 
@@ -351,6 +352,7 @@ def main():
             private_key=dict(type='path'),
             rsync_path=dict(type='str'),
             _local_rsync_path=dict(type='path', default='rsync'),
+            _local_rsync_password=dict(default=None, no_log=True),
             _substitute_controller=dict(type='bool', default=False),
             archive=dict(type='bool', default=True),
             checksum=dict(type='bool', default=False),
@@ -390,6 +392,7 @@ def main():
     private_key = module.params['private_key']
     rsync_path = module.params['rsync_path']
     rsync = module.params.get('_local_rsync_path', 'rsync')
+    rsync_password = module.params.get('_local_rsync_password')
     rsync_timeout = module.params.get('rsync_timeout', 'rsync_timeout')
     archive = module.params['archive']
     checksum = module.params['checksum']
@@ -414,6 +417,16 @@ def main():
         rsync = module.get_bin_path(rsync, required=True)
 
     cmd = [rsync, '--delay-updates', '-F']
+    _sshpass_pipe = None
+    if rsync_password:
+        try:
+            module.run_command(["sshpass"])
+        except OSError:
+            module.fail_json(
+                msg="to use rsync connection with passwords, you must install the sshpass program"
+            )
+        _sshpass_pipe = os.pipe()
+        cmd = ['sshpass', '-d' + _sshpass_pipe[0]] + cmd
     if compress:
         cmd.append('--compress')
     if rsync_timeout:
@@ -520,7 +533,24 @@ def main():
     cmd.append(source)
     cmd.append(dest)
     cmdstr = ' '.join(cmd)
-    (rc, out, err) = module.run_command(cmd)
+
+    # If we are using password authentication, write the password into the pipe
+    if rsync_password:
+        def _write_password_to_pipe(proc):
+            os.close(_sshpass_pipe[0])
+            try:
+                os.write(_sshpass_pipe[1], to_bytes(rsync_password) + b'\n')
+            except OSError as exc:
+                # Ignore broken pipe errors if the sshpass process has exited.
+                if exc.errno != errno.EPIPE or proc.poll() is None:
+                    raise
+        (rc, out, err) = module.run_command(
+            cmd, pass_fds=_sshpass_pipe,
+            before_communicate_callback=_write_password_to_pipe
+        )
+    else:
+        (rc, out, err) = module.run_command(cmd)
+
     if rc:
         return module.fail_json(msg=err, rc=rc, cmd=cmdstr)
 
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
@@ -205,6 +205,7 @@ def run(self, tmp=None, task_vars=None):
 
         # Parameter name needed by the ansible module
         _tmp_args['_local_rsync_path'] = task_vars.get('ansible_rsync_path') or 'rsync'
+        _tmp_args['_local_rsync_password'] = task_vars.get('ansible_ssh_pass') or task_vars.get('ansible_password')
 
         # rsync thinks that one end of the connection is localhost and the
         # other is the host we're running the task for  (Note: We use


Test patch: