From 139e103b0f2f211bcf73b0bdff4307ff3ce9d2dc Mon Sep 17 00:00:00 2001 From: Juan Antonio Valino Garcia Date: Fri, 7 Oct 2022 17:20:20 +0200 Subject: [PATCH 1/4] Fixes ##390. Hosts involved must have same password --- plugins/action/synchronize.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/action/synchronize.py b/plugins/action/synchronize.py index a5752b9..7a330ac 100644 --- a/plugins/action/synchronize.py +++ b/plugins/action/synchronize.py @@ -225,7 +225,6 @@ 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 @@ -333,8 +332,9 @@ class ActionModule(ActionBase): if src is None or dest is None: return dict(failed=True, msg="synchronize requires both src and dest parameters are set") - # Determine if we need a user@ + # Determine if we need a user@ and a password user = None + password = task_vars.get('ansible_ssh_pass', None) or task_vars.get('ansible_password', None) if not dest_is_local: # Src and dest rsync "path" handling if boolean(_tmp_args.get('set_remote_user', 'yes'), strict=False): @@ -344,10 +344,11 @@ class ActionModule(ActionBase): user = task_vars.get('ansible_user') or self._play_context.remote_user if not user: user = C.DEFAULT_REMOTE_USER - else: user = task_vars.get('ansible_user') or self._play_context.remote_user + user = self._templar.template(user) + # Private key handling # Use the private_key parameter if passed else use context private_key_file _tmp_args['private_key'] = _tmp_args.get('private_key', self._play_context.private_key_file) @@ -361,12 +362,15 @@ class ActionModule(ActionBase): # src is a local path, dest is a remote path: @ src = self._process_origin(src_host, src, user) dest = self._process_remote(_tmp_args, dest_host, dest, user, inv_port in localhost_ports) + + password = dest_host_inventory_vars.get('ansible_ssh_pass', None) or dest_host_inventory_vars.get('ansible_password', None) else: # Still need to munge paths (to account for roles) even if we aren't # copying files between hosts src = self._get_absolute_path(path=src) dest = self._get_absolute_path(path=dest) + _tmp_args['_local_rsync_password'] = self._templar.template(password) _tmp_args['src'] = src _tmp_args['dest'] = dest From 297a10fec7eb431c95584eb7317f90434e22dbe3 Mon Sep 17 00:00:00 2001 From: Juan Antonio Valino Garcia Date: Fri, 7 Oct 2022 18:04:09 +0200 Subject: [PATCH 2/4] handle missing templar --- synchronize.py | 434 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 synchronize.py diff --git a/synchronize.py b/synchronize.py new file mode 100644 index 0000000..c70db5f --- /dev/null +++ b/synchronize.py @@ -0,0 +1,434 @@ +# -*- coding: utf-8 -*- + +# (c) 2012-2013, Timothy Appnel +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os.path + +from ansible import constants as C +from ansible.module_utils.six import string_types +from ansible.module_utils.six.moves import shlex_quote +from ansible.module_utils._text import to_text +from ansible.module_utils.common._collections_compat import MutableSequence +from ansible.module_utils.parsing.convert_bool import boolean +from ansible.plugins.action import ActionBase +from ansible.plugins.loader import connection_loader + + +DOCKER = ['docker', 'community.general.docker', 'community.docker.docker'] +PODMAN = ['podman', 'ansible.builtin.podman', 'containers.podman.podman'] +BUILDAH = ['buildah', 'containers.podman.buildah'] + + +class ActionModule(ActionBase): + + def _get_absolute_path(self, path): + original_path = path + + # + # Check if we have a local relative path and do not process + # * remote paths (some.server.domain:/some/remote/path/...) + # * URLs (rsync://...) + # * local absolute paths (/some/local/path/...) + # + if ':' in path or path.startswith('/'): + return path + + if self._task._role is not None: + path = self._loader.path_dwim_relative(self._task._role._role_path, 'files', path) + else: + path = self._loader.path_dwim_relative(self._loader.get_basedir(), 'files', path) + + if original_path and original_path[-1] == '/' and path[-1] != '/': + # make sure the dwim'd path ends in a trailing "/" + # if the original path did + path += '/' + + return path + + def _host_is_ipv6_address(self, host): + return ':' in to_text(host, errors='surrogate_or_strict') + + def _format_rsync_rsh_target(self, host, path, user): + ''' formats rsync rsh target, escaping ipv6 addresses if needed ''' + + user_prefix = '' + + if path.startswith('rsync://'): + return path + + # If using docker or buildah, do not add user information + if self._remote_transport not in DOCKER + PODMAN + BUILDAH and user: + user_prefix = '%s@' % (user, ) + + if self._host_is_ipv6_address(host): + return '[%s%s]:%s' % (user_prefix, host, path) + return '%s%s:%s' % (user_prefix, host, path) + + def _process_origin(self, host, path, user): + + if host not in C.LOCALHOST: + return self._format_rsync_rsh_target(host, path, user) + + path = self._get_absolute_path(path=path) + return path + + def _process_remote(self, task_args, host, path, user, port_matches_localhost_port): + """ + :arg host: hostname for the path + :arg path: file path + :arg user: username for the transfer + :arg port_matches_localhost_port: boolean whether the remote port + matches the port used by localhost's sshd. This is used in + conjunction with seeing whether the host is localhost to know + if we need to have the module substitute the pathname or if it + is a different host (for instance, an ssh tunnelled port or an + alternative ssh port to a vagrant host.) + """ + transport = self._connection.transport + # If we're connecting to a remote host or we're delegating to another + # host or we're connecting to a different ssh instance on the + # localhost then we have to format the path as a remote rsync path + if host not in C.LOCALHOST or transport != "local" or \ + (host in C.LOCALHOST and not port_matches_localhost_port): + # If we're delegating to non-localhost and but the + # inventory_hostname host is localhost then we need the module to + # fix up the rsync path to use the controller's public DNS/IP + # instead of "localhost" + if port_matches_localhost_port and host in C.LOCALHOST: + task_args['_substitute_controller'] = True + return self._format_rsync_rsh_target(host, path, user) + + path = self._get_absolute_path(path=path) + return path + + def _override_module_replaced_vars(self, task_vars): + """ Some vars are substituted into the modules. Have to make sure + that those are correct for localhost when synchronize creates its own + connection to localhost.""" + + # Clear the current definition of these variables as they came from the + # connection to the remote host + if 'ansible_syslog_facility' in task_vars: + del task_vars['ansible_syslog_facility'] + for key in list(task_vars.keys()): + if key.startswith("ansible_") and key.endswith("_interpreter"): + del task_vars[key] + + # Add the definitions from localhost + for host in C.LOCALHOST: + if host in task_vars['hostvars']: + localhost = task_vars['hostvars'][host] + break + if 'ansible_syslog_facility' in localhost: + task_vars['ansible_syslog_facility'] = localhost['ansible_syslog_facility'] + for key in localhost: + if key.startswith("ansible_") and key.endswith("_interpreter"): + task_vars[key] = localhost[key] + + def run(self, tmp=None, task_vars=None): + ''' generates params and passes them on to the rsync module ''' + # When modifying this function be aware of the tricky convolutions + # your thoughts have to go through: + # + # In normal ansible, we connect from controller to inventory_hostname + # (playbook's hosts: field) or controller to delegate_to host and run + # a module on one of those hosts. + # + # So things that are directly related to the core of ansible are in + # terms of that sort of connection that always originate on the + # controller. + # + # In synchronize we use ansible to connect to either the controller or + # to the delegate_to host and then run rsync which makes its own + # connection from controller to inventory_hostname or delegate_to to + # inventory_hostname. + # + # That means synchronize needs to have some knowledge of the + # controller to inventory_host/delegate host that ansible typically + # establishes and use those to construct a command line for rsync to + # connect from the inventory_host to the controller/delegate. The + # challenge for coders is remembering which leg of the trip is + # associated with the conditions that you're checking at any one time. + if task_vars is None: + task_vars = dict() + + # We make a copy of the args here because we may fail and be asked to + # retry. If that happens we don't want to pass the munged args through + # to our next invocation. Munged args are single use only. + _tmp_args = self._task.args.copy() + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + # Store remote connection type + self._remote_transport = self._connection.transport + use_ssh_args = _tmp_args.pop('use_ssh_args', None) + + if use_ssh_args and self._connection.transport == 'ssh': + ssh_args = [ + self._connection.get_option('ssh_args'), + self._connection.get_option('ssh_common_args'), + self._connection.get_option('ssh_extra_args'), + ] + _tmp_args['ssh_args'] = ' '.join([a for a in ssh_args if a]) + + # Handle docker connection options + if self._remote_transport in DOCKER: + self._docker_cmd = self._connection.docker_cmd + if self._play_context.docker_extra_args: + self._docker_cmd = "%s %s" % (self._docker_cmd, self._play_context.docker_extra_args) + elif self._remote_transport in PODMAN: + self._docker_cmd = self._connection._options['podman_executable'] + if self._connection._options.get('podman_extra_args'): + self._docker_cmd = "%s %s" % (self._docker_cmd, self._connection._options['podman_extra_args']) + + # self._connection accounts for delegate_to so + # remote_transport is the transport ansible thought it would need + # between the controller and the delegate_to host or the controller + # and the remote_host if delegate_to isn't set. + + remote_transport = False + if self._connection.transport != 'local': + remote_transport = True + + try: + delegate_to = self._task.delegate_to + except (AttributeError, KeyError): + delegate_to = None + + # ssh paramiko docker buildah and local are fully supported transports. Anything + # else only works with delegate_to + if delegate_to is None and self._connection.transport not in [ + 'ssh', 'paramiko', 'local'] + DOCKER + PODMAN + BUILDAH: + result['failed'] = True + result['msg'] = ( + "synchronize uses rsync to function. rsync needs to connect to the remote " + "host via ssh, docker client or a direct filesystem " + "copy. This remote host is being accessed via %s instead " + "so it cannot work." % self._connection.transport) + return result + + # Parameter name needed by the ansible module + _tmp_args['_local_rsync_path'] = task_vars.get('ansible_rsync_path') or 'rsync' + + # 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 + # ansible's delegate_to mechanism to determine which host rsync is + # running on so localhost could be a non-controller machine if + # delegate_to is used) + src_host = '127.0.0.1' + inventory_hostname = task_vars.get('inventory_hostname') + dest_host_inventory_vars = task_vars['hostvars'].get(inventory_hostname) + dest_host = dest_host_inventory_vars.get('ansible_host', inventory_hostname) + + dest_host_ids = [hostid for hostid in (dest_host_inventory_vars.get('inventory_hostname'), + dest_host_inventory_vars.get('ansible_host')) + if hostid is not None] + + localhost_ports = set() + for host in C.LOCALHOST: + localhost_vars = task_vars['hostvars'].get(host, {}) + for port_var in C.MAGIC_VARIABLE_MAPPING['port']: + port = localhost_vars.get(port_var, None) + if port: + break + else: + port = C.DEFAULT_REMOTE_PORT + localhost_ports.add(port) + + # dest_is_local tells us if the host rsync runs on is the same as the + # 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 delegate_to is None and remote_transport is False: + dest_is_local = True + elif delegate_to is not None and delegate_to in dest_host_ids: + dest_is_local = True + + # CHECK FOR NON-DEFAULT SSH PORT + inv_port = task_vars.get('ansible_port', None) or C.DEFAULT_REMOTE_PORT + if _tmp_args.get('dest_port', None) is None: + if inv_port is not None: + _tmp_args['dest_port'] = inv_port + + # Set use_delegate if we are going to run rsync on a delegated host + # instead of localhost + use_delegate = False + 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) + dest_host = '127.0.0.1' + use_delegate = True + elif delegate_to is not None and remote_transport: + # If we're delegating to a remote host then we need to use the + # delegate_to settings + use_delegate = True + + # 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 and remote_transport: + # Create a connection to localhost to run rsync on + new_stdin = self._connection._new_stdin + + # Unlike port, there can be only one shell + localhost_shell = None + for host in C.LOCALHOST: + localhost_vars = task_vars['hostvars'].get(host, {}) + for shell_var in C.MAGIC_VARIABLE_MAPPING['shell']: + localhost_shell = localhost_vars.get(shell_var, None) + if localhost_shell: + break + if localhost_shell: + break + else: + localhost_shell = os.path.basename(C.DEFAULT_EXECUTABLE) + self._play_context.shell = localhost_shell + + # Unlike port, there can be only one executable + localhost_executable = None + for host in C.LOCALHOST: + localhost_vars = task_vars['hostvars'].get(host, {}) + for executable_var in C.MAGIC_VARIABLE_MAPPING['executable']: + localhost_executable = localhost_vars.get(executable_var, None) + if localhost_executable: + break + if localhost_executable: + break + else: + localhost_executable = C.DEFAULT_EXECUTABLE + self._play_context.executable = localhost_executable + + new_connection = connection_loader.get('local', self._play_context, new_stdin) + self._connection = new_connection + # Override _remote_is_local as an instance attribute specifically for the synchronize use case + # ensuring we set local tmpdir correctly + self._connection._remote_is_local = True + self._override_module_replaced_vars(task_vars) + + # SWITCH SRC AND DEST HOST PER MODE + if _tmp_args.get('mode', 'push') == 'pull': + (dest_host, src_host) = (src_host, dest_host) + + # MUNGE SRC AND DEST PER REMOTE_HOST INFO + src = _tmp_args.get('src', None) + dest = _tmp_args.get('dest', None) + if src is None or dest is None: + return dict(failed=True, msg="synchronize requires both src and dest parameters are set") + + # Determine if we need a user@ and a password + user = None + password = task_vars.get('ansible_ssh_pass', None) or task_vars.get('ansible_password', None) + if not dest_is_local: + # Src and dest rsync "path" handling + if boolean(_tmp_args.get('set_remote_user', 'yes'), strict=False): + if use_delegate: + user = task_vars.get('ansible_delegated_vars', dict()).get('ansible_user', None) + if not user: + user = task_vars.get('ansible_user') or self._play_context.remote_user + if not user: + user = C.DEFAULT_REMOTE_USER + else: + user = task_vars.get('ansible_user') or self._play_context.remote_user + + if self._templar is not None: + user = self._templar.template(user) + + # Private key handling + # Use the private_key parameter if passed else use context private_key_file + _tmp_args['private_key'] = _tmp_args.get('private_key', self._play_context.private_key_file) + + # use the mode to define src and dest's url + if _tmp_args.get('mode', 'push') == 'pull': + # src is a remote path: @, dest is a local path + src = self._process_remote(_tmp_args, src_host, src, user, inv_port in localhost_ports) + dest = self._process_origin(dest_host, dest, user) + else: + # src is a local path, dest is a remote path: @ + src = self._process_origin(src_host, src, user) + dest = self._process_remote(_tmp_args, dest_host, dest, user, inv_port in localhost_ports) + + password = dest_host_inventory_vars.get('ansible_ssh_pass', None) or dest_host_inventory_vars.get('ansible_password', None) + if self._templar is not None: + password = self._templar.template(password) + else: + # Still need to munge paths (to account for roles) even if we aren't + # copying files between hosts + src = self._get_absolute_path(path=src) + dest = self._get_absolute_path(path=dest) + + _tmp_args['_local_rsync_password'] = password + _tmp_args['src'] = src + _tmp_args['dest'] = dest + + # Allow custom rsync path argument + rsync_path = _tmp_args.get('rsync_path', None) + + # backup original become as we are probably about to unset it + become = self._play_context.become + + if not dest_is_local: + # don't escalate for docker. doing --rsync-path with docker exec fails + # and we can switch directly to the user via docker arguments + if self._play_context.become and not rsync_path and self._remote_transport not in DOCKER + PODMAN: + # If no rsync_path is set, become was originally set, and dest is + # remote then add privilege escalation here. + if self._play_context.become_method == 'sudo': + if self._play_context.become_user: + rsync_path = 'sudo -u %s rsync' % self._play_context.become_user + else: + rsync_path = 'sudo rsync' + # TODO: have to add in the rest of the become methods here + + # We cannot use privilege escalation on the machine running the + # module. Instead we run it on the machine rsync is connecting + # to. + self._play_context.become = False + + _tmp_args['rsync_path'] = rsync_path + + # If launching synchronize against docker container + # use rsync_opts to support container to override rsh options + if self._remote_transport in DOCKER + BUILDAH + PODMAN and not use_delegate: + # Replicate what we do in the module argumentspec handling for lists + if not isinstance(_tmp_args.get('rsync_opts'), MutableSequence): + tmp_rsync_opts = _tmp_args.get('rsync_opts', []) + if isinstance(tmp_rsync_opts, string_types): + tmp_rsync_opts = tmp_rsync_opts.split(',') + elif isinstance(tmp_rsync_opts, (int, float)): + tmp_rsync_opts = [to_text(tmp_rsync_opts)] + _tmp_args['rsync_opts'] = tmp_rsync_opts + + if '--blocking-io' not in _tmp_args['rsync_opts']: + _tmp_args['rsync_opts'].append('--blocking-io') + + if self._remote_transport in DOCKER + PODMAN: + if become and self._play_context.become_user: + _tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('%s exec -u %s -i' % (self._docker_cmd, self._play_context.become_user))) + elif user is not None: + _tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('%s exec -u %s -i' % (self._docker_cmd, user))) + else: + _tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('%s exec -i' % self._docker_cmd)) + elif self._remote_transport in BUILDAH: + _tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('buildah run --')) + + # run the module and store the result + result.update(self._execute_module('ansible.posix.synchronize', module_args=_tmp_args, task_vars=task_vars)) + + return result From 4512e7b1e9d195bd30a535f09f207b322da53177 Mon Sep 17 00:00:00 2001 From: Juan Antonio Valino Garcia Date: Fri, 7 Oct 2022 18:10:45 +0200 Subject: [PATCH 3/4] add changelog fragment --- changelogs/fragments/390_hosts_involved_same_password.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelogs/fragments/390_hosts_involved_same_password.yml diff --git a/changelogs/fragments/390_hosts_involved_same_password.yml b/changelogs/fragments/390_hosts_involved_same_password.yml new file mode 100644 index 0000000..1169a31 --- /dev/null +++ b/changelogs/fragments/390_hosts_involved_same_password.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - synchronize - Fixed hosts involved in rsync require the same password From 50f87b0d15903cfc3f7cd700d550b1e633310a0b Mon Sep 17 00:00:00 2001 From: Juan Antonio Valino Garcia Date: Fri, 7 Oct 2022 18:30:49 +0200 Subject: [PATCH 4/4] move plugin to correct dir --- plugins/action/synchronize.py | 7 +- synchronize.py | 434 ---------------------------------- 2 files changed, 5 insertions(+), 436 deletions(-) delete mode 100644 synchronize.py diff --git a/plugins/action/synchronize.py b/plugins/action/synchronize.py index 7a330ac..c70db5f 100644 --- a/plugins/action/synchronize.py +++ b/plugins/action/synchronize.py @@ -347,7 +347,8 @@ class ActionModule(ActionBase): else: user = task_vars.get('ansible_user') or self._play_context.remote_user - user = self._templar.template(user) + if self._templar is not None: + user = self._templar.template(user) # Private key handling # Use the private_key parameter if passed else use context private_key_file @@ -364,13 +365,15 @@ class ActionModule(ActionBase): dest = self._process_remote(_tmp_args, dest_host, dest, user, inv_port in localhost_ports) password = dest_host_inventory_vars.get('ansible_ssh_pass', None) or dest_host_inventory_vars.get('ansible_password', None) + if self._templar is not None: + password = self._templar.template(password) else: # Still need to munge paths (to account for roles) even if we aren't # copying files between hosts src = self._get_absolute_path(path=src) dest = self._get_absolute_path(path=dest) - _tmp_args['_local_rsync_password'] = self._templar.template(password) + _tmp_args['_local_rsync_password'] = password _tmp_args['src'] = src _tmp_args['dest'] = dest diff --git a/synchronize.py b/synchronize.py deleted file mode 100644 index c70db5f..0000000 --- a/synchronize.py +++ /dev/null @@ -1,434 +0,0 @@ -# -*- coding: utf-8 -*- - -# (c) 2012-2013, Timothy Appnel -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os.path - -from ansible import constants as C -from ansible.module_utils.six import string_types -from ansible.module_utils.six.moves import shlex_quote -from ansible.module_utils._text import to_text -from ansible.module_utils.common._collections_compat import MutableSequence -from ansible.module_utils.parsing.convert_bool import boolean -from ansible.plugins.action import ActionBase -from ansible.plugins.loader import connection_loader - - -DOCKER = ['docker', 'community.general.docker', 'community.docker.docker'] -PODMAN = ['podman', 'ansible.builtin.podman', 'containers.podman.podman'] -BUILDAH = ['buildah', 'containers.podman.buildah'] - - -class ActionModule(ActionBase): - - def _get_absolute_path(self, path): - original_path = path - - # - # Check if we have a local relative path and do not process - # * remote paths (some.server.domain:/some/remote/path/...) - # * URLs (rsync://...) - # * local absolute paths (/some/local/path/...) - # - if ':' in path or path.startswith('/'): - return path - - if self._task._role is not None: - path = self._loader.path_dwim_relative(self._task._role._role_path, 'files', path) - else: - path = self._loader.path_dwim_relative(self._loader.get_basedir(), 'files', path) - - if original_path and original_path[-1] == '/' and path[-1] != '/': - # make sure the dwim'd path ends in a trailing "/" - # if the original path did - path += '/' - - return path - - def _host_is_ipv6_address(self, host): - return ':' in to_text(host, errors='surrogate_or_strict') - - def _format_rsync_rsh_target(self, host, path, user): - ''' formats rsync rsh target, escaping ipv6 addresses if needed ''' - - user_prefix = '' - - if path.startswith('rsync://'): - return path - - # If using docker or buildah, do not add user information - if self._remote_transport not in DOCKER + PODMAN + BUILDAH and user: - user_prefix = '%s@' % (user, ) - - if self._host_is_ipv6_address(host): - return '[%s%s]:%s' % (user_prefix, host, path) - return '%s%s:%s' % (user_prefix, host, path) - - def _process_origin(self, host, path, user): - - if host not in C.LOCALHOST: - return self._format_rsync_rsh_target(host, path, user) - - path = self._get_absolute_path(path=path) - return path - - def _process_remote(self, task_args, host, path, user, port_matches_localhost_port): - """ - :arg host: hostname for the path - :arg path: file path - :arg user: username for the transfer - :arg port_matches_localhost_port: boolean whether the remote port - matches the port used by localhost's sshd. This is used in - conjunction with seeing whether the host is localhost to know - if we need to have the module substitute the pathname or if it - is a different host (for instance, an ssh tunnelled port or an - alternative ssh port to a vagrant host.) - """ - transport = self._connection.transport - # If we're connecting to a remote host or we're delegating to another - # host or we're connecting to a different ssh instance on the - # localhost then we have to format the path as a remote rsync path - if host not in C.LOCALHOST or transport != "local" or \ - (host in C.LOCALHOST and not port_matches_localhost_port): - # If we're delegating to non-localhost and but the - # inventory_hostname host is localhost then we need the module to - # fix up the rsync path to use the controller's public DNS/IP - # instead of "localhost" - if port_matches_localhost_port and host in C.LOCALHOST: - task_args['_substitute_controller'] = True - return self._format_rsync_rsh_target(host, path, user) - - path = self._get_absolute_path(path=path) - return path - - def _override_module_replaced_vars(self, task_vars): - """ Some vars are substituted into the modules. Have to make sure - that those are correct for localhost when synchronize creates its own - connection to localhost.""" - - # Clear the current definition of these variables as they came from the - # connection to the remote host - if 'ansible_syslog_facility' in task_vars: - del task_vars['ansible_syslog_facility'] - for key in list(task_vars.keys()): - if key.startswith("ansible_") and key.endswith("_interpreter"): - del task_vars[key] - - # Add the definitions from localhost - for host in C.LOCALHOST: - if host in task_vars['hostvars']: - localhost = task_vars['hostvars'][host] - break - if 'ansible_syslog_facility' in localhost: - task_vars['ansible_syslog_facility'] = localhost['ansible_syslog_facility'] - for key in localhost: - if key.startswith("ansible_") and key.endswith("_interpreter"): - task_vars[key] = localhost[key] - - def run(self, tmp=None, task_vars=None): - ''' generates params and passes them on to the rsync module ''' - # When modifying this function be aware of the tricky convolutions - # your thoughts have to go through: - # - # In normal ansible, we connect from controller to inventory_hostname - # (playbook's hosts: field) or controller to delegate_to host and run - # a module on one of those hosts. - # - # So things that are directly related to the core of ansible are in - # terms of that sort of connection that always originate on the - # controller. - # - # In synchronize we use ansible to connect to either the controller or - # to the delegate_to host and then run rsync which makes its own - # connection from controller to inventory_hostname or delegate_to to - # inventory_hostname. - # - # That means synchronize needs to have some knowledge of the - # controller to inventory_host/delegate host that ansible typically - # establishes and use those to construct a command line for rsync to - # connect from the inventory_host to the controller/delegate. The - # challenge for coders is remembering which leg of the trip is - # associated with the conditions that you're checking at any one time. - if task_vars is None: - task_vars = dict() - - # We make a copy of the args here because we may fail and be asked to - # retry. If that happens we don't want to pass the munged args through - # to our next invocation. Munged args are single use only. - _tmp_args = self._task.args.copy() - - result = super(ActionModule, self).run(tmp, task_vars) - del tmp # tmp no longer has any effect - - # Store remote connection type - self._remote_transport = self._connection.transport - use_ssh_args = _tmp_args.pop('use_ssh_args', None) - - if use_ssh_args and self._connection.transport == 'ssh': - ssh_args = [ - self._connection.get_option('ssh_args'), - self._connection.get_option('ssh_common_args'), - self._connection.get_option('ssh_extra_args'), - ] - _tmp_args['ssh_args'] = ' '.join([a for a in ssh_args if a]) - - # Handle docker connection options - if self._remote_transport in DOCKER: - self._docker_cmd = self._connection.docker_cmd - if self._play_context.docker_extra_args: - self._docker_cmd = "%s %s" % (self._docker_cmd, self._play_context.docker_extra_args) - elif self._remote_transport in PODMAN: - self._docker_cmd = self._connection._options['podman_executable'] - if self._connection._options.get('podman_extra_args'): - self._docker_cmd = "%s %s" % (self._docker_cmd, self._connection._options['podman_extra_args']) - - # self._connection accounts for delegate_to so - # remote_transport is the transport ansible thought it would need - # between the controller and the delegate_to host or the controller - # and the remote_host if delegate_to isn't set. - - remote_transport = False - if self._connection.transport != 'local': - remote_transport = True - - try: - delegate_to = self._task.delegate_to - except (AttributeError, KeyError): - delegate_to = None - - # ssh paramiko docker buildah and local are fully supported transports. Anything - # else only works with delegate_to - if delegate_to is None and self._connection.transport not in [ - 'ssh', 'paramiko', 'local'] + DOCKER + PODMAN + BUILDAH: - result['failed'] = True - result['msg'] = ( - "synchronize uses rsync to function. rsync needs to connect to the remote " - "host via ssh, docker client or a direct filesystem " - "copy. This remote host is being accessed via %s instead " - "so it cannot work." % self._connection.transport) - return result - - # Parameter name needed by the ansible module - _tmp_args['_local_rsync_path'] = task_vars.get('ansible_rsync_path') or 'rsync' - - # 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 - # ansible's delegate_to mechanism to determine which host rsync is - # running on so localhost could be a non-controller machine if - # delegate_to is used) - src_host = '127.0.0.1' - inventory_hostname = task_vars.get('inventory_hostname') - dest_host_inventory_vars = task_vars['hostvars'].get(inventory_hostname) - dest_host = dest_host_inventory_vars.get('ansible_host', inventory_hostname) - - dest_host_ids = [hostid for hostid in (dest_host_inventory_vars.get('inventory_hostname'), - dest_host_inventory_vars.get('ansible_host')) - if hostid is not None] - - localhost_ports = set() - for host in C.LOCALHOST: - localhost_vars = task_vars['hostvars'].get(host, {}) - for port_var in C.MAGIC_VARIABLE_MAPPING['port']: - port = localhost_vars.get(port_var, None) - if port: - break - else: - port = C.DEFAULT_REMOTE_PORT - localhost_ports.add(port) - - # dest_is_local tells us if the host rsync runs on is the same as the - # 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 delegate_to is None and remote_transport is False: - dest_is_local = True - elif delegate_to is not None and delegate_to in dest_host_ids: - dest_is_local = True - - # CHECK FOR NON-DEFAULT SSH PORT - inv_port = task_vars.get('ansible_port', None) or C.DEFAULT_REMOTE_PORT - if _tmp_args.get('dest_port', None) is None: - if inv_port is not None: - _tmp_args['dest_port'] = inv_port - - # Set use_delegate if we are going to run rsync on a delegated host - # instead of localhost - use_delegate = False - 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) - dest_host = '127.0.0.1' - use_delegate = True - elif delegate_to is not None and remote_transport: - # If we're delegating to a remote host then we need to use the - # delegate_to settings - use_delegate = True - - # 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 and remote_transport: - # Create a connection to localhost to run rsync on - new_stdin = self._connection._new_stdin - - # Unlike port, there can be only one shell - localhost_shell = None - for host in C.LOCALHOST: - localhost_vars = task_vars['hostvars'].get(host, {}) - for shell_var in C.MAGIC_VARIABLE_MAPPING['shell']: - localhost_shell = localhost_vars.get(shell_var, None) - if localhost_shell: - break - if localhost_shell: - break - else: - localhost_shell = os.path.basename(C.DEFAULT_EXECUTABLE) - self._play_context.shell = localhost_shell - - # Unlike port, there can be only one executable - localhost_executable = None - for host in C.LOCALHOST: - localhost_vars = task_vars['hostvars'].get(host, {}) - for executable_var in C.MAGIC_VARIABLE_MAPPING['executable']: - localhost_executable = localhost_vars.get(executable_var, None) - if localhost_executable: - break - if localhost_executable: - break - else: - localhost_executable = C.DEFAULT_EXECUTABLE - self._play_context.executable = localhost_executable - - new_connection = connection_loader.get('local', self._play_context, new_stdin) - self._connection = new_connection - # Override _remote_is_local as an instance attribute specifically for the synchronize use case - # ensuring we set local tmpdir correctly - self._connection._remote_is_local = True - self._override_module_replaced_vars(task_vars) - - # SWITCH SRC AND DEST HOST PER MODE - if _tmp_args.get('mode', 'push') == 'pull': - (dest_host, src_host) = (src_host, dest_host) - - # MUNGE SRC AND DEST PER REMOTE_HOST INFO - src = _tmp_args.get('src', None) - dest = _tmp_args.get('dest', None) - if src is None or dest is None: - return dict(failed=True, msg="synchronize requires both src and dest parameters are set") - - # Determine if we need a user@ and a password - user = None - password = task_vars.get('ansible_ssh_pass', None) or task_vars.get('ansible_password', None) - if not dest_is_local: - # Src and dest rsync "path" handling - if boolean(_tmp_args.get('set_remote_user', 'yes'), strict=False): - if use_delegate: - user = task_vars.get('ansible_delegated_vars', dict()).get('ansible_user', None) - if not user: - user = task_vars.get('ansible_user') or self._play_context.remote_user - if not user: - user = C.DEFAULT_REMOTE_USER - else: - user = task_vars.get('ansible_user') or self._play_context.remote_user - - if self._templar is not None: - user = self._templar.template(user) - - # Private key handling - # Use the private_key parameter if passed else use context private_key_file - _tmp_args['private_key'] = _tmp_args.get('private_key', self._play_context.private_key_file) - - # use the mode to define src and dest's url - if _tmp_args.get('mode', 'push') == 'pull': - # src is a remote path: @, dest is a local path - src = self._process_remote(_tmp_args, src_host, src, user, inv_port in localhost_ports) - dest = self._process_origin(dest_host, dest, user) - else: - # src is a local path, dest is a remote path: @ - src = self._process_origin(src_host, src, user) - dest = self._process_remote(_tmp_args, dest_host, dest, user, inv_port in localhost_ports) - - password = dest_host_inventory_vars.get('ansible_ssh_pass', None) or dest_host_inventory_vars.get('ansible_password', None) - if self._templar is not None: - password = self._templar.template(password) - else: - # Still need to munge paths (to account for roles) even if we aren't - # copying files between hosts - src = self._get_absolute_path(path=src) - dest = self._get_absolute_path(path=dest) - - _tmp_args['_local_rsync_password'] = password - _tmp_args['src'] = src - _tmp_args['dest'] = dest - - # Allow custom rsync path argument - rsync_path = _tmp_args.get('rsync_path', None) - - # backup original become as we are probably about to unset it - become = self._play_context.become - - if not dest_is_local: - # don't escalate for docker. doing --rsync-path with docker exec fails - # and we can switch directly to the user via docker arguments - if self._play_context.become and not rsync_path and self._remote_transport not in DOCKER + PODMAN: - # If no rsync_path is set, become was originally set, and dest is - # remote then add privilege escalation here. - if self._play_context.become_method == 'sudo': - if self._play_context.become_user: - rsync_path = 'sudo -u %s rsync' % self._play_context.become_user - else: - rsync_path = 'sudo rsync' - # TODO: have to add in the rest of the become methods here - - # We cannot use privilege escalation on the machine running the - # module. Instead we run it on the machine rsync is connecting - # to. - self._play_context.become = False - - _tmp_args['rsync_path'] = rsync_path - - # If launching synchronize against docker container - # use rsync_opts to support container to override rsh options - if self._remote_transport in DOCKER + BUILDAH + PODMAN and not use_delegate: - # Replicate what we do in the module argumentspec handling for lists - if not isinstance(_tmp_args.get('rsync_opts'), MutableSequence): - tmp_rsync_opts = _tmp_args.get('rsync_opts', []) - if isinstance(tmp_rsync_opts, string_types): - tmp_rsync_opts = tmp_rsync_opts.split(',') - elif isinstance(tmp_rsync_opts, (int, float)): - tmp_rsync_opts = [to_text(tmp_rsync_opts)] - _tmp_args['rsync_opts'] = tmp_rsync_opts - - if '--blocking-io' not in _tmp_args['rsync_opts']: - _tmp_args['rsync_opts'].append('--blocking-io') - - if self._remote_transport in DOCKER + PODMAN: - if become and self._play_context.become_user: - _tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('%s exec -u %s -i' % (self._docker_cmd, self._play_context.become_user))) - elif user is not None: - _tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('%s exec -u %s -i' % (self._docker_cmd, user))) - else: - _tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('%s exec -i' % self._docker_cmd)) - elif self._remote_transport in BUILDAH: - _tmp_args['rsync_opts'].append('--rsh=' + shlex_quote('buildah run --')) - - # run the module and store the result - result.update(self._execute_module('ansible.posix.synchronize', module_args=_tmp_args, task_vars=task_vars)) - - return result