From 14037443dee3538b37aabf68c0486fc0fef5b43f Mon Sep 17 00:00:00 2001 From: Yauhen Kirylau Date: Mon, 5 Nov 2018 21:00:34 +0100 Subject: [PATCH] fix(tasks: synchronize): wrap in sshpass if ssh password was provided (#30743) * fix(tasks: synchronize): wrap in sshpass if ssh password was provided Closes #16616 * fix(tasks: synchronize): pass rsync password to sshpass via fd * fix(tasks: synchronize): use fail_json instead of AnsibleError * fixup! fix(tasks: synchronize): use fail_json instead of AnsibleError fix python2 handling * feat(module_utils: basic: run_command): add optional arguments `pass_fds` and `before_communicate_callback` * fix(tasks: synchronize): use module.run_command instead of subprocess.Popen * fixup! fix(tasks: synchronize): use module.run_command instead of subprocess.Popen remove unused import * fixup! fixup! fix(tasks: synchronize): use module.run_command instead of subprocess.Popen pass_fds only if they passed to run_command() --- lib/ansible/module_utils/basic.py | 13 ++++++++- lib/ansible/modules/files/synchronize.py | 34 +++++++++++++++++++++-- lib/ansible/plugins/action/synchronize.py | 1 + 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py index 63a20ea9b8..4a79b5424a 100644 --- a/lib/ansible/module_utils/basic.py +++ b/lib/ansible/module_utils/basic.py @@ -2695,7 +2695,7 @@ class AnsibleModule(object): 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. @@ -2738,6 +2738,13 @@ class AnsibleModule(object): 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 @@ -2839,6 +2846,8 @@ class AnsibleModule(object): stderr=subprocess.PIPE, preexec_fn=self._restore_signal_handlers, ) + if PY3 and pass_fds: + kwargs["pass_fds"] = pass_fds # store the pwd prev_dir = os.getcwd() @@ -2861,6 +2870,8 @@ class AnsibleModule(object): 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 index 198d515d67..49adc4d0b3 100644 --- a/lib/ansible/modules/files/synchronize.py +++ b/lib/ansible/modules/files/synchronize.py @@ -319,8 +319,9 @@ EXAMPLES = ''' 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 @@ -365,6 +366,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), @@ -404,6 +406,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'] @@ -428,6 +431,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: @@ -534,7 +547,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 index 009a611f24..59d9376712 100644 --- a/lib/ansible/plugins/action/synchronize.py +++ b/lib/ansible/plugins/action/synchronize.py @@ -205,6 +205,7 @@ class ActionModule(ActionBase): # 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