2015-01-02 13:51:15 +00:00
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012-2013, Timothy Appnel <tim@appnel.com>
#
# 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 <http://www.gnu.org/licenses/>.
2015-04-13 20:28:01 +00:00
from __future__ import ( absolute_import , division , print_function )
__metaclass__ = type
2015-01-02 13:51:15 +00:00
import os . path
2016-01-29 00:02:57 +00:00
from ansible . playbook . play_context import MAGIC_VARIABLE_MAPPING
2015-01-02 13:51:15 +00:00
from ansible . plugins . action import ActionBase
2015-07-29 16:10:24 +00:00
from ansible . plugins import connection_loader
2015-01-02 13:51:15 +00:00
from ansible . utils . boolean import boolean
2015-08-19 00:02:03 +00:00
from ansible import constants as C
2015-05-06 22:06:43 +00:00
2015-01-02 13:51:15 +00:00
class ActionModule ( ActionBase ) :
2015-04-02 16:54:45 +00:00
def _get_absolute_path ( self , path ) :
if self . _task . _role is not None :
original_path = path
2015-08-16 06:36:49 +00:00
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 )
2015-04-02 16:54:45 +00:00
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 + = ' / '
2015-01-02 13:51:15 +00:00
return path
2015-10-09 22:01:22 +00:00
def _host_is_ipv6_address ( self , host ) :
return ' : ' in host
2015-09-18 09:46:33 +00:00
def _format_rsync_rsh_target ( self , host , path , user ) :
''' formats rsync rsh target, escaping ipv6 addresses if needed '''
user_prefix = ' '
if user :
user_prefix = ' %s @ ' % ( user , )
2015-10-09 22:01:22 +00:00
if self . _host_is_ipv6_address ( host ) :
2015-09-18 09:46:33 +00:00
return ' [ %s %s ]: %s ' % ( user_prefix , host , path )
else :
return ' %s %s : %s ' % ( user_prefix , host , path )
2015-04-02 16:54:45 +00:00
def _process_origin ( self , host , path , user ) :
2015-01-02 13:51:15 +00:00
2015-08-19 00:02:03 +00:00
if host not in C . LOCALHOST :
2015-09-18 09:46:33 +00:00
return self . _format_rsync_rsh_target ( host , path , user )
2015-08-03 22:04:41 +00:00
if ' : ' not in path and not path . startswith ( ' / ' ) :
path = self . _get_absolute_path ( path = path )
return path
2015-01-02 13:51:15 +00:00
2016-01-26 21:53:42 +00:00
def _process_remote ( self , 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 . )
"""
2015-07-21 16:12:22 +00:00
transport = self . _play_context . connection
2015-08-19 00:02:03 +00:00
if host not in C . LOCALHOST or transport != " local " :
2016-01-26 21:53:42 +00:00
if port_matches_localhost_port and host in C . LOCALHOST :
2016-01-18 21:26:54 +00:00
self . _task . args [ ' _substitute_controller ' ] = True
2015-09-18 09:46:33 +00:00
return self . _format_rsync_rsh_target ( host , path , user )
2015-01-02 13:51:15 +00:00
2015-08-03 22:04:41 +00:00
if ' : ' not in path and not path . startswith ( ' / ' ) :
path = self . _get_absolute_path ( path = path )
return path
2015-01-02 13:51:15 +00:00
2015-08-03 20:29:54 +00:00
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 ' ]
2015-08-26 09:01:00 +00:00
for key in task_vars . keys ( ) :
2015-08-03 20:29:54 +00:00
if key . startswith ( " ansible_ " ) and key . endswith ( " _interpreter " ) :
del task_vars [ key ]
2015-08-05 20:06:39 +00:00
# Add the definitions from localhost
2015-08-26 20:36:50 +00:00
for host in C . LOCALHOST :
if host in task_vars [ ' hostvars ' ] :
localhost = task_vars [ ' hostvars ' ] [ host ]
break
2015-08-03 20:29:54 +00:00
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 ]
2015-10-22 23:07:26 +00:00
def run ( self , tmp = None , task_vars = None ) :
2015-01-02 13:51:15 +00:00
''' generates params and passes them on to the rsync module '''
2016-01-18 21:26:54 +00:00
# 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.
2015-10-22 23:07:26 +00:00
if task_vars is None :
task_vars = dict ( )
result = super ( ActionModule , self ) . run ( tmp , task_vars )
2015-01-02 13:51:15 +00:00
2016-01-18 21:26:54 +00:00
# self._play_context.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.
2015-08-05 20:06:39 +00:00
remote_transport = False
2016-01-18 21:26:54 +00:00
if self . _play_context . connection != ' local ' :
2015-08-05 20:06:39 +00:00
remote_transport = True
2015-07-31 21:51:26 +00:00
try :
2015-09-22 20:06:52 +00:00
delegate_to = self . _task . delegate_to
2015-07-31 21:51:26 +00:00
except ( AttributeError , KeyError ) :
delegate_to = None
2015-01-02 13:51:15 +00:00
2016-01-27 20:48:15 +00:00
# ssh paramiko and local are fully supported transports. Anything
# else only works with delegate_to
if delegate_to is None and self . _play_context . connection not in ( ' ssh ' , ' paramiko ' , ' smart ' , ' local ' ) :
result [ ' failed ' ] = True
result [ ' msg ' ] = " synchronize uses rsync to function. rsync needs to connect to the remote host via ssh or a direct filesystem copy. This remote host is being accessed via %s instead so it cannot work. " % self . _play_context . connection
return result
2015-05-06 22:06:43 +00:00
use_ssh_args = self . _task . args . pop ( ' use_ssh_args ' , None )
2015-01-02 13:51:15 +00:00
2015-07-29 16:10:24 +00:00
# Parameter name needed by the ansible module
self . _task . args [ ' _local_rsync_path ' ] = task_vars . get ( ' ansible_rsync_path ' ) or ' rsync '
2015-01-02 13:51:15 +00:00
2015-08-05 20:06:39 +00:00
# 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)
2015-10-22 23:07:26 +00:00
src_host = ' 127.0.0.1 '
2015-10-15 01:55:46 +00:00
inventory_hostname = task_vars . get ( ' inventory_hostname ' )
dest_host_inventory_vars = task_vars [ ' hostvars ' ] . get ( inventory_hostname )
2015-11-08 12:08:44 +00:00
try :
dest_host = dest_host_inventory_vars [ ' ansible_host ' ]
except KeyError :
2015-11-19 17:55:06 +00:00
dest_host = dest_host_inventory_vars . get ( ' ansible_ssh_host ' , inventory_hostname )
2015-01-02 13:51:15 +00:00
2016-01-26 21:53:42 +00:00
localhost_ports = set ( )
for host in C . LOCALHOST :
localhost_vars = task_vars [ ' hostvars ' ] . get ( host , { } )
2016-01-29 00:02:57 +00:00
for port_var in MAGIC_VARIABLE_MAPPING [ ' port ' ] :
2016-01-26 21:53:42 +00:00
port = localhost_vars . get ( port_var , None )
if port :
break
else :
port = C . DEFAULT_REMOTE_PORT
localhost_ports . add ( port )
2016-01-18 21:26:54 +00:00
# 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 not delegate_to and remote_transport is False :
dest_is_local = True
elif delegate_to and delegate_to == dest_host :
dest_is_local = True
2015-07-29 16:10:24 +00:00
2015-01-02 13:51:15 +00:00
# CHECK FOR NON-DEFAULT SSH PORT
2016-03-24 13:57:52 +00:00
inv_port = task_vars . get ( ' ansible_ssh_port ' , None ) or C . DEFAULT_REMOTE_PORT
2015-08-05 20:06:39 +00:00
if self . _task . args . get ( ' dest_port ' , None ) is None :
if inv_port is not None :
self . _task . args [ ' dest_port ' ] = inv_port
2015-01-02 13:51:15 +00:00
2015-08-05 20:06:39 +00:00
# Set use_delegate if we are going to run rsync on a delegated host
# instead of localhost
2015-07-29 16:10:24 +00:00
use_delegate = False
2015-07-31 21:51:26 +00:00
if dest_host == delegate_to :
2015-07-29 16:10:24 +00:00
# edge case: explicit delegate and dest_host are the same
2015-10-26 21:13:49 +00:00
# so we run rsync on the remote machine targeting its localhost
2015-07-31 21:51:26 +00:00
# (itself)
2015-01-02 13:51:15 +00:00
dest_host = ' 127.0.0.1 '
2015-07-29 16:10:24 +00:00
use_delegate = True
2015-08-05 20:06:39 +00:00
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
2015-07-29 16:10:24 +00:00
# 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
2015-08-05 20:06:39 +00:00
if not use_delegate and remote_transport :
2015-07-29 16:10:24 +00:00
# Create a connection to localhost to run rsync on
new_stdin = self . _connection . _new_stdin
2016-01-29 00:02:57 +00:00
# Unike 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 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
2015-07-29 16:10:24 +00:00
new_connection = connection_loader . get ( ' local ' , self . _play_context , new_stdin )
self . _connection = new_connection
2015-08-03 20:29:54 +00:00
self . _override_module_replaced_vars ( task_vars )
2015-08-01 01:26:30 +00:00
2015-08-05 20:06:39 +00:00
# SWITCH SRC AND DEST HOST PER MODE
if self . _task . args . get ( ' mode ' , ' push ' ) == ' pull ' :
( dest_host , src_host ) = ( src_host , dest_host )
2015-07-29 16:10:24 +00:00
2015-01-02 13:51:15 +00:00
# MUNGE SRC AND DEST PER REMOTE_HOST INFO
2015-08-05 20:06:39 +00:00
src = self . _task . args . get ( ' src ' , None )
2015-07-29 16:10:24 +00:00
dest = self . _task . args . get ( ' dest ' , None )
2016-01-18 21:26:54 +00:00
if not dest_is_local :
2015-08-05 20:06:39 +00:00
# Private key handling
2015-01-02 13:51:15 +00:00
if use_delegate :
2015-07-21 16:12:22 +00:00
private_key = task_vars . get ( ' ansible_ssh_private_key_file ' ) or self . _play_context . private_key_file
2015-01-02 13:51:15 +00:00
else :
2015-07-21 16:12:22 +00:00
private_key = task_vars . get ( ' ansible_ssh_private_key_file ' ) or self . _play_context . private_key_file
2015-01-02 13:51:15 +00:00
2015-01-08 16:51:54 +00:00
if private_key is not None :
2015-01-02 13:51:15 +00:00
private_key = os . path . expanduser ( private_key )
2015-05-07 17:20:11 +00:00
self . _task . args [ ' private_key ' ] = private_key
2015-08-05 20:06:39 +00:00
# Src and dest rsync "path" handling
# Determine if we need a user@
user = None
if boolean ( self . _task . args . get ( ' set_remote_user ' , ' yes ' ) ) :
if use_delegate :
2015-09-22 20:06:52 +00:00
user = task_vars . get ( ' ansible_delegated_vars ' , dict ( ) ) . get ( ' ansible_ssh_user ' , None )
if not user :
user = C . DEFAULT_REMOTE_USER
2015-08-05 20:06:39 +00:00
2015-09-22 20:06:52 +00:00
else :
2015-08-05 20:06:39 +00:00
user = task_vars . get ( ' ansible_ssh_user ' ) or self . _play_context . remote_user
2015-01-02 13:51:15 +00:00
# use the mode to define src and dest's url
if self . _task . args . get ( ' mode ' , ' push ' ) == ' pull ' :
# src is a remote path: <user>@<host>, dest is a local path
2016-01-26 21:53:42 +00:00
src = self . _process_remote ( src_host , src , user , inv_port in localhost_ports )
2015-04-02 16:54:45 +00:00
dest = self . _process_origin ( dest_host , dest , user )
2015-01-02 13:51:15 +00:00
else :
# src is a local path, dest is a remote path: <user>@<host>
2015-08-05 20:06:39 +00:00
src = self . _process_origin ( src_host , src , user )
2016-01-26 21:53:42 +00:00
dest = self . _process_remote ( dest_host , dest , user , inv_port in localhost_ports )
2015-08-05 20:32:12 +00:00
else :
# Still need to munge paths (to account for roles) even if we aren't
# copying files between hosts
if not src . startswith ( ' / ' ) :
src = self . _get_absolute_path ( path = src )
if not dest . startswith ( ' / ' ) :
dest = self . _get_absolute_path ( path = dest )
2015-01-02 13:51:15 +00:00
2015-07-29 16:10:24 +00:00
self . _task . args [ ' src ' ] = src
self . _task . args [ ' dest ' ] = dest
2015-08-05 20:06:39 +00:00
# Allow custom rsync path argument
2015-01-02 13:51:15 +00:00
rsync_path = self . _task . args . get ( ' rsync_path ' , None )
2016-01-18 21:26:54 +00:00
if not dest_is_local :
if self . _play_context . become and not rsync_path :
# 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 ' :
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
2015-01-02 13:51:15 +00:00
# make sure rsync path is quoted.
if rsync_path :
2015-04-02 16:54:45 +00:00
self . _task . args [ ' rsync_path ' ] = ' " %s " ' % rsync_path
2015-01-02 13:51:15 +00:00
2015-05-06 22:06:43 +00:00
if use_ssh_args :
2015-08-19 00:02:03 +00:00
self . _task . args [ ' ssh_args ' ] = C . ANSIBLE_SSH_ARGS
2015-05-06 22:06:43 +00:00
2015-01-02 13:51:15 +00:00
# run the module and store the result
2015-10-22 23:07:26 +00:00
result . update ( self . _execute_module ( ' synchronize ' , task_vars = task_vars ) )
2015-01-02 13:51:15 +00:00
2016-01-18 21:26:54 +00:00
if ' SyntaxError ' in result . get ( ' exception ' , result . get ( ' msg ' , ' ' ) ) :
2015-08-03 21:43:27 +00:00
# Emit a warning about using python3 because synchronize is
# somewhat unique in running on localhost
2016-03-16 03:31:40 +00:00
result [ ' exception ' ] = result [ ' msg ' ]
2015-08-03 21:43:27 +00:00
result [ ' msg ' ] = ' SyntaxError parsing module. Perhaps invoking " python " on your local (or delegate_to) machine invokes python3. You can set ansible_python_interpreter for localhost (or the delegate_to machine) to the location of python2 to fix this '
2015-01-02 13:51:15 +00:00
return result