Repository: ansible/ansible
Pull request: fix(tasks: synchronize): wrap in sshpass if ssh password was provided
##### 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.
> sshpass somewhere in the the exec arguments and sending the password when the prompt occurs.
> 

Fixes #16616

##### ISSUE TYPE
 - Bugfix Pull Request

##### COMPONENT NAME
synchronize

##### ANSIBLE VERSION
(could be easily backported to older versions if needed)
```
$ ansible --version
ansible 2.5.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/user/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/sbin/ansible
  python version = 2.7.13 (default, Jul 21 2017, 03:24:34) [GCC 7.1.1 20170630]
```



Problem statement:
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:
```

Hints from issue 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.
> Yes. Just submit a pull request.

@tima your suggestion seems to be not any effective

Commit message: fix(tasks: synchronize): pass rsync password to sshpass via fd

Patch to review:
diff --git a/lib/ansible/modules/files/synchronize.py b/lib/ansible/modules/files/synchronize.py
index 616cfcbdee2164..af0df272a8b5bc 100644
--- a/lib/ansible/modules/files/synchronize.py
+++ b/lib/ansible/modules/files/synchronize.py
@@ -305,9 +305,12 @@
 
 
 import os
+import subprocess
+import errno
 
-from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.basic import AnsibleModule, to_bytes, PY3
 from ansible.module_utils.six.moves import shlex_quote
+from ansible.errors import AnsibleError
 
 
 client_addr = None
@@ -351,6 +354,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 +394,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 +419,19 @@ def main():
         rsync = module.get_bin_path(rsync, required=True)
 
     cmd = [rsync, '--delay-updates', '-F']
+    _sshpass_pipe = None
+    if rsync_password:
+        try:
+            proc = subprocess.Popen(
+                ["sshpass"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+            )
+            proc.communicate()
+        except OSError:
+            raise AnsibleError(
+                "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 +538,27 @@ def main():
     cmd.append(source)
     cmd.append(dest)
     cmdstr = ' '.join(cmd)
-    (rc, out, err) = module.run_command(cmd)
+
+    if not (PY3 and rsync_password):
+        proc = subprocess.Popen(
+            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+        )
+    # If we are using password authentication, write the password into the pipe
+    else:
+        # pylint: disable=unexpected-keyword-arg
+        proc = subprocess.Popen(
+            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, pass_fds=_sshpass_pipe
+        )
+        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
+    out, err = proc.communicate()
+    rc = proc.returncode
+
     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
index 2bb5b7c78632bc..1dfc4267c87c25 100644
--- 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


Code review comments:
diff_hunk: @@ -414,6 +419,19 @@ def main():
         rsync = module.get_bin_path(rsync, required=True)
 
     cmd = [rsync, '--delay-updates', '-F']
+    _sshpass_pipe = None
+    if rsync_password:
+        try:
+            proc = subprocess.Popen(
+                ["sshpass"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+            )
+            proc.communicate()
+        except OSError:
+            raise AnsibleError(
+                "to use rsync connection with passwords, you must install the sshpass program"
path: lib/ansible/modules/files/synchronize.py
text: @user1:
In modules you'll need to call ```module.fail_json(msg='error message here')``` instead of raising an exception.

@author:
thanks, fixed!

diff_hunk: @@ -520,7 +538,27 @@ def main():
     cmd.append(source)
     cmd.append(dest)
     cmdstr = ' '.join(cmd)
-    (rc, out, err) = module.run_command(cmd)
+
+    if not (PY3 and rsync_password):
path: lib/ansible/modules/files/synchronize.py
text: @user1:
The logic here seems like it leaves of PY2 + rsync_password?

@author:
i've copied it from here: https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/connection/ssh.py#L711

@author:
i see, that's because `pass_fds` not implemented in python2

@author:
i am wondering if some warning or error need to be produced to react on such case?

and if so, should this handling be also added to ssh connection plugin?

@user1:
@user2 could you review this code?  I don't know the history but I suspect that PY3 needs the pass_fds parameter and PY2 doesn't but both PY3 and PY2 need to write the portion which then handles writing to the ssh_pass pipe.

@user2:
@user1 it seems you were the one who added that PY3 branch for `pass_fds`: https://github.com/ansible/ansible/pull/17863

@author:
now i've understood what the point, on python2 it just works without passing fds explicitly

pushed a fixup