2012-06-17 11:35:59 +00:00
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# 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/>.
#
import os
import subprocess
import shlex
import pipes
import random
import select
import fcntl
2013-08-02 16:08:02 +00:00
import hmac
2013-03-27 20:41:54 +00:00
import pwd
2013-08-08 15:02:52 +00:00
import gettext
2013-09-16 21:19:39 +00:00
import pty
2013-08-02 16:08:02 +00:00
from hashlib import sha1
2012-08-15 00:13:02 +00:00
import ansible . constants as C
2012-08-09 01:09:14 +00:00
from ansible . callbacks import vvv
2012-06-17 11:35:59 +00:00
from ansible import errors
2013-01-10 05:50:56 +00:00
from ansible import utils
2012-06-17 11:35:59 +00:00
2012-08-18 14:52:24 +00:00
class Connection ( object ) :
2012-06-17 11:35:59 +00:00
''' ssh based connections '''
2013-04-05 18:42:18 +00:00
def __init__ ( self , runner , host , port , user , password , private_key_file , * args , * * kwargs ) :
2012-06-17 11:35:59 +00:00
self . runner = runner
self . host = host
2013-08-18 21:08:35 +00:00
self . ipv6 = ' : ' in self . host
2012-06-17 11:35:59 +00:00
self . port = port
2013-02-10 22:22:18 +00:00
self . user = user
self . password = password
2013-03-19 16:28:43 +00:00
self . private_key_file = private_key_file
2013-08-02 16:08:02 +00:00
self . HASHED_KEY_MAGIC = " |1| "
2014-01-15 21:44:28 +00:00
self . has_pipelining = True
2012-06-17 11:35:59 +00:00
2013-09-03 15:59:50 +00:00
fcntl . lockf ( self . runner . process_lockfile , fcntl . LOCK_EX )
self . cp_dir = utils . prepare_writeable_dir ( ' $HOME/.ansible/cp ' , mode = 0700 )
fcntl . lockf ( self . runner . process_lockfile , fcntl . LOCK_UN )
2012-06-17 11:35:59 +00:00
def connect ( self ) :
''' connect to the remote host '''
2012-07-15 16:29:53 +00:00
2013-02-10 22:22:18 +00:00
vvv ( " ESTABLISH CONNECTION FOR USER: %s " % self . user , host = self . host )
2012-08-11 21:33:34 +00:00
2012-06-26 18:59:42 +00:00
self . common_args = [ ]
2012-08-15 00:13:02 +00:00
extra_args = C . ANSIBLE_SSH_ARGS
2012-06-17 11:35:59 +00:00
if extra_args is not None :
self . common_args + = shlex . split ( extra_args )
2012-06-19 19:58:24 +00:00
else :
self . common_args + = [ " -o " , " ControlMaster=auto " ,
" -o " , " ControlPersist=60s " ,
2013-09-18 19:03:40 +00:00
" -o " , " ControlPath= %s " % ( C . ANSIBLE_SSH_CONTROL_PATH % dict ( directory = self . cp_dir ) ) ]
2013-08-20 17:03:50 +00:00
cp_in_use = False
cp_path_set = False
for arg in self . common_args :
if arg . find ( " ControlPersist " ) != - 1 :
cp_in_use = True
if arg . find ( " ControlPath " ) != - 1 :
cp_path_set = True
if cp_in_use and not cp_path_set :
2013-09-18 19:03:40 +00:00
self . common_args + = [ " -o " , " ControlPath= %s " % ( C . ANSIBLE_SSH_CONTROL_PATH % dict ( directory = self . cp_dir ) ) ]
2013-07-03 20:47:20 +00:00
if not C . HOST_KEY_CHECKING :
self . common_args + = [ " -o " , " StrictHostKeyChecking=no " ]
2012-06-26 18:59:42 +00:00
if self . port is not None :
self . common_args + = [ " -o " , " Port= %d " % ( self . port ) ]
2013-03-19 16:28:43 +00:00
if self . private_key_file is not None :
self . common_args + = [ " -o " , " IdentityFile= " + os . path . expanduser ( self . private_key_file ) ]
elif self . runner . private_key_file is not None :
2013-01-20 21:16:01 +00:00
self . common_args + = [ " -o " , " IdentityFile= " + os . path . expanduser ( self . runner . private_key_file ) ]
2013-02-10 22:22:18 +00:00
if self . password :
2012-11-27 09:51:35 +00:00
self . common_args + = [ " -o " , " GSSAPIAuthentication=no " ,
" -o " , " PubkeyAuthentication=no " ]
else :
self . common_args + = [ " -o " , " KbdInteractiveAuthentication=no " ,
2013-09-06 08:29:43 +00:00
" -o " , " PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey " ,
2012-11-27 09:51:35 +00:00
" -o " , " PasswordAuthentication=no " ]
2013-03-27 20:41:54 +00:00
if self . user != pwd . getpwuid ( os . geteuid ( ) ) [ 0 ] :
self . common_args + = [ " -o " , " User= " + self . user ]
2013-02-10 22:22:18 +00:00
self . common_args + = [ " -o " , " ConnectTimeout= %d " % self . runner . timeout ]
2012-06-17 11:35:59 +00:00
return self
2012-11-16 22:42:19 +00:00
def _password_cmd ( self ) :
2013-02-10 22:22:18 +00:00
if self . password :
2012-11-16 22:42:19 +00:00
try :
p = subprocess . Popen ( [ " sshpass " ] , stdin = subprocess . PIPE ,
stdout = subprocess . PIPE , stderr = subprocess . PIPE )
p . communicate ( )
except OSError :
2013-07-19 13:09:04 +00:00
raise errors . AnsibleError ( " to use the ' ssh ' connection type with passwords, you must install the sshpass program " )
2012-11-16 22:42:19 +00:00
( self . rfd , self . wfd ) = os . pipe ( )
return [ " sshpass " , " -d %d " % self . rfd ]
return [ ]
def _send_password ( self ) :
2013-02-10 22:22:18 +00:00
if self . password :
2012-11-16 22:42:19 +00:00
os . close ( self . rfd )
2013-02-10 22:22:18 +00:00
os . write ( self . wfd , " %s \n " % self . password )
2012-11-16 22:42:19 +00:00
os . close ( self . wfd )
2013-07-04 20:03:17 +00:00
def not_in_host_file ( self , host ) :
2014-01-28 20:10:13 +00:00
if ' USER ' in os . environ :
host_file = os . path . expandvars ( " ~$ {USER} /.ssh/known_hosts " )
else :
host_file = " ~/.ssh/known_hosts "
host_file = os . path . expanduser ( host_file )
2013-07-04 20:03:17 +00:00
if not os . path . exists ( host_file ) :
print " previous known host file not found "
return True
host_fh = open ( host_file )
data = host_fh . read ( )
host_fh . close ( )
for line in data . split ( " \n " ) :
if line is None or line . find ( " " ) == - 1 :
continue
tokens = line . split ( )
2013-08-02 16:08:02 +00:00
if tokens [ 0 ] . find ( self . HASHED_KEY_MAGIC ) == 0 :
# this is a hashed known host entry
try :
( kn_salt , kn_host ) = tokens [ 0 ] [ len ( self . HASHED_KEY_MAGIC ) : ] . split ( " | " , 2 )
hash = hmac . new ( kn_salt . decode ( ' base64 ' ) , digestmod = sha1 )
hash . update ( host )
if hash . digest ( ) == kn_host . decode ( ' base64 ' ) :
return False
except :
# invalid hashed host key, skip it
continue
else :
# standard host file entry
if host in tokens [ 0 ] :
return False
2013-07-04 20:03:17 +00:00
return True
2014-01-21 01:19:03 +00:00
def exec_command ( self , cmd , tmp_path , sudo_user = None , sudoable = False , executable = ' /bin/sh ' , in_data = None , su_user = None , su = False ) :
2012-06-17 11:35:59 +00:00
''' run a command on the remote host '''
2012-11-16 22:42:19 +00:00
ssh_cmd = self . _password_cmd ( )
2014-01-15 21:44:28 +00:00
ssh_cmd + = [ " ssh " , " -C " ]
if not in_data :
# we can only use tty when we are not pipelining the modules. piping data into /usr/bin/python
# inside a tty automatically invokes the python interactive-mode but the modules are not
# compatible with the interactive-mode ("unexpected indent" mainly because of empty lines)
ssh_cmd + = [ " -tt " ]
2013-09-19 10:58:54 +00:00
if utils . VERBOSITY > 3 :
ssh_cmd + = [ " -vvv " ]
else :
ssh_cmd + = [ " -q " ]
ssh_cmd + = self . common_args
2013-08-18 21:08:35 +00:00
if self . ipv6 :
ssh_cmd + = [ ' -6 ' ]
ssh_cmd + = [ self . host ]
2012-11-16 22:42:19 +00:00
2014-01-21 01:19:03 +00:00
if su and su_user :
sudocmd , prompt , success_key = utils . make_su_cmd ( su_user , executable , cmd )
ssh_cmd . append ( sudocmd )
elif not self . runner . sudo or not sudoable :
2013-01-08 16:45:37 +00:00
if executable :
ssh_cmd . append ( executable + ' -c ' + pipes . quote ( cmd ) )
else :
ssh_cmd . append ( cmd )
2012-12-23 18:17:07 +00:00
else :
2013-10-30 18:18:35 +00:00
sudocmd , prompt , success_key = utils . make_sudo_cmd ( sudo_user , executable , cmd )
2013-01-10 05:50:56 +00:00
ssh_cmd . append ( sudocmd )
2012-11-16 22:42:19 +00:00
vvv ( " EXEC %s " % ssh_cmd , host = self . host )
2013-07-04 20:03:17 +00:00
not_in_host_file = self . not_in_host_file ( self . host )
if C . HOST_KEY_CHECKING and not_in_host_file :
# lock around the initial SSH connectivity so the user prompt about whether to add
# the host to known hosts is not intermingled with multiprocess output.
2013-07-04 22:17:45 +00:00
fcntl . lockf ( self . runner . process_lockfile , fcntl . LOCK_EX )
fcntl . lockf ( self . runner . output_lockfile , fcntl . LOCK_EX )
2014-01-21 01:19:03 +00:00
2014-01-15 21:44:28 +00:00
# create process
if in_data :
# do not use pseudo-pty
2012-11-23 16:52:55 +00:00
p = subprocess . Popen ( ssh_cmd , stdin = subprocess . PIPE ,
2014-01-15 21:44:28 +00:00
stdout = subprocess . PIPE , stderr = subprocess . PIPE )
2012-11-23 16:52:55 +00:00
stdin = p . stdin
2014-01-15 21:44:28 +00:00
else :
# try to use upseudo-pty
try :
# Make sure stdin is a proper (pseudo) pty to avoid: tcgetattr errors
master , slave = pty . openpty ( )
p = subprocess . Popen ( ssh_cmd , stdin = slave ,
stdout = subprocess . PIPE , stderr = subprocess . PIPE )
stdin = os . fdopen ( master , ' w ' , 0 )
os . close ( slave )
except :
p = subprocess . Popen ( ssh_cmd , stdin = subprocess . PIPE ,
stdout = subprocess . PIPE , stderr = subprocess . PIPE )
stdin = p . stdin
2012-11-16 22:42:19 +00:00
self . _send_password ( )
2014-01-21 01:19:03 +00:00
if ( self . runner . sudo and sudoable and self . runner . sudo_pass ) or \
( self . runner . su and su and self . runner . su_pass ) :
2014-01-15 21:44:28 +00:00
# several cases are handled for sudo privileges with password
# * NOPASSWD (tty & no-tty): detect success_key on stdout
# * without NOPASSWD:
# * detect prompt on stdout (tty)
# * detect prompt on stderr (no-tty)
2012-11-16 22:42:19 +00:00
fcntl . fcntl ( p . stdout , fcntl . F_SETFL ,
fcntl . fcntl ( p . stdout , fcntl . F_GETFL ) | os . O_NONBLOCK )
2014-01-15 21:44:28 +00:00
fcntl . fcntl ( p . stderr , fcntl . F_SETFL ,
fcntl . fcntl ( p . stderr , fcntl . F_GETFL ) | os . O_NONBLOCK )
2012-11-22 19:06:30 +00:00
sudo_output = ' '
2014-01-15 21:44:28 +00:00
sudo_errput = ' '
2013-10-30 18:18:35 +00:00
while not sudo_output . endswith ( prompt ) and success_key not in sudo_output :
2014-01-15 21:44:28 +00:00
rfd , wfd , efd = select . select ( [ p . stdout , p . stderr ] , [ ] ,
2012-11-16 22:42:19 +00:00
[ p . stdout ] , self . runner . timeout )
2014-01-15 21:44:28 +00:00
if p . stderr in rfd :
chunk = p . stderr . read ( )
if not chunk :
2014-01-21 01:19:03 +00:00
raise errors . AnsibleError ( ' ssh connection closed waiting for sudo or su password prompt ' )
2014-01-15 21:44:28 +00:00
sudo_errput + = chunk
incorrect_password = gettext . dgettext (
" sudo " , " Sorry, try again. " )
if sudo_errput . strip ( ) . endswith ( " %s %s " % ( prompt , incorrect_password ) ) :
raise errors . AnsibleError ( ' Incorrect sudo password ' )
elif sudo_errput . endswith ( prompt ) :
stdin . write ( self . runner . sudo_pass + ' \n ' )
2012-11-16 22:42:19 +00:00
if p . stdout in rfd :
chunk = p . stdout . read ( )
if not chunk :
2014-01-21 01:19:03 +00:00
raise errors . AnsibleError ( ' ssh connection closed waiting for sudo or su password prompt ' )
2012-11-16 22:42:19 +00:00
sudo_output + = chunk
2014-01-15 21:44:28 +00:00
if not rfd :
# timeout. wrap up process communication
2012-11-16 22:42:19 +00:00
stdout = p . communicate ( )
2014-01-21 01:19:03 +00:00
raise errors . AnsibleError ( ' ssh connection error waiting for sudo or su password prompt ' )
2014-01-15 21:44:28 +00:00
2013-10-30 18:18:35 +00:00
if success_key not in sudo_output :
2014-01-21 01:19:03 +00:00
if sudoable :
stdin . write ( self . runner . sudo_pass + ' \n ' )
elif su :
stdin . write ( self . runner . su_pass + ' \n ' )
2012-11-16 22:42:19 +00:00
fcntl . fcntl ( p . stdout , fcntl . F_SETFL , fcntl . fcntl ( p . stdout , fcntl . F_GETFL ) & ~ os . O_NONBLOCK )
2014-01-15 21:44:28 +00:00
fcntl . fcntl ( p . stderr , fcntl . F_SETFL , fcntl . fcntl ( p . stderr , fcntl . F_GETFL ) & ~ os . O_NONBLOCK )
2012-06-20 18:22:48 +00:00
# We can't use p.communicate here because the ControlMaster may have stdout open as well
stdout = ' '
2012-12-23 00:44:00 +00:00
stderr = ' '
2013-11-15 19:19:10 +00:00
rpipes = [ p . stdout , p . stderr ]
2014-01-15 21:44:28 +00:00
if in_data :
try :
stdin . write ( in_data )
stdin . close ( )
except :
raise errors . AnsibleError ( ' SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh ' )
2012-09-27 19:44:34 +00:00
while True :
2013-11-15 19:19:10 +00:00
rfd , wfd , efd = select . select ( rpipes , [ ] , rpipes , 1 )
2013-07-17 15:17:13 +00:00
2014-01-21 01:19:03 +00:00
# fail early if the sudo/su password is wrong
2013-08-08 15:02:52 +00:00
if self . runner . sudo and sudoable and self . runner . sudo_pass :
incorrect_password = gettext . dgettext (
" sudo " , " Sorry, try again. " )
if stdout . endswith ( " %s \r \n %s " % ( incorrect_password , prompt ) ) :
2014-01-21 01:19:03 +00:00
raise errors . AnsibleError ( ' Incorrect sudo password ' )
if self . runner . su and su and self . runner . sudo_pass :
incorrect_password = gettext . dgettext (
" su " , " Sorry " )
if stdout . endswith ( " %s \r \n %s " % ( incorrect_password , prompt ) ) :
raise errors . AnsibleError ( ' Incorrect su password ' )
2013-07-17 15:17:13 +00:00
2012-06-20 18:22:48 +00:00
if p . stdout in rfd :
2014-01-30 21:02:05 +00:00
dat = os . read ( p . stdout . fileno ( ) , 9000 )
stdout + = dat
if dat == ' ' :
rpipes . remove ( p . stdout )
2013-11-15 19:19:10 +00:00
if p . stderr in rfd :
2014-01-30 21:02:05 +00:00
dat = os . read ( p . stderr . fileno ( ) , 9000 )
stderr + = dat
if dat == ' ' :
rpipes . remove ( p . stderr )
# only break out if we've emptied the pipes, or there is nothing to
# read from and the process has finished.
if ( not rpipes or not rfd ) and p . poll ( ) is not None :
2012-09-27 19:44:34 +00:00
break
2014-01-30 21:02:05 +00:00
# Calling wait while there are still pipes to read can cause a lock
elif not rpipes and p . poll ( ) == None :
p . wait ( )
2012-11-23 16:52:55 +00:00
stdin . close ( ) # close stdin after we read from stdout (see also issue #848)
2013-07-04 20:03:17 +00:00
if C . HOST_KEY_CHECKING and not_in_host_file :
# lock around the initial SSH connectivity so the user prompt about whether to add
# the host to known hosts is not intermingled with multiprocess output.
2013-07-04 22:17:45 +00:00
fcntl . lockf ( self . runner . output_lockfile , fcntl . LOCK_UN )
fcntl . lockf ( self . runner . process_lockfile , fcntl . LOCK_UN )
2014-01-15 21:44:28 +00:00
controlpersisterror = stderr . find ( ' Bad configuration option: ControlPersist ' ) != - 1 or stderr . find ( ' unknown configuration option: ControlPersist ' ) != - 1
2014-01-08 21:50:40 +00:00
if C . HOST_KEY_CHECKING :
if ssh_cmd [ 0 ] == " sshpass " and p . returncode == 6 :
2014-01-08 22:25:50 +00:00
raise errors . AnsibleError ( ' Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this. Please add this host \' s fingerprint to your known_hosts file to manage this host. ' )
2014-01-08 21:50:40 +00:00
2013-11-22 17:13:24 +00:00
if p . returncode != 0 and controlpersisterror :
2012-10-21 11:11:39 +00:00
raise errors . AnsibleError ( ' using -c ssh on certain older ssh versions may not support ControlPersist, set ANSIBLE_SSH_ARGS= " " (or ansible_ssh_args in the config file) before running again ' )
2014-01-15 21:44:28 +00:00
if p . returncode == 255 and in_data :
raise errors . AnsibleError ( ' SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh ' )
2012-08-07 00:07:02 +00:00
2012-12-23 00:44:00 +00:00
return ( p . returncode , ' ' , stdout , stderr )
2012-06-17 11:35:59 +00:00
def put_file ( self , in_path , out_path ) :
''' transfer a file from local to remote '''
2012-08-09 01:09:14 +00:00
vvv ( " PUT %s TO %s " % ( in_path , out_path ) , host = self . host )
2012-06-17 11:35:59 +00:00
if not os . path . exists ( in_path ) :
raise errors . AnsibleFileNotFound ( " file or module does not exist: %s " % in_path )
2012-11-16 22:42:19 +00:00
cmd = self . _password_cmd ( )
2013-08-18 21:08:35 +00:00
host = self . host
if self . ipv6 :
host = ' [ %s ] ' % host
2012-11-05 22:25:40 +00:00
if C . DEFAULT_SCP_IF_SSH :
2012-11-16 22:42:19 +00:00
cmd + = [ " scp " ] + self . common_args
2013-08-18 21:08:35 +00:00
cmd + = [ in_path , host + " : " + pipes . quote ( out_path ) ]
2012-11-16 22:42:19 +00:00
indata = None
2012-11-05 22:25:40 +00:00
else :
2013-08-18 21:08:35 +00:00
cmd + = [ " sftp " ] + self . common_args + [ host ]
2013-07-20 22:28:11 +00:00
indata = " put %s %s \n " % ( pipes . quote ( in_path ) , pipes . quote ( out_path ) )
2012-11-16 22:42:19 +00:00
p = subprocess . Popen ( cmd , stdin = subprocess . PIPE ,
stdout = subprocess . PIPE , stderr = subprocess . PIPE )
self . _send_password ( )
stdout , stderr = p . communicate ( indata )
2012-06-17 11:35:59 +00:00
if p . returncode != 0 :
raise errors . AnsibleError ( " failed to transfer file to %s : \n %s \n %s " % ( out_path , stdout , stderr ) )
def fetch_file ( self , in_path , out_path ) :
''' fetch a file from remote to local '''
2012-08-09 01:09:14 +00:00
vvv ( " FETCH %s TO %s " % ( in_path , out_path ) , host = self . host )
2012-11-16 22:42:19 +00:00
cmd = self . _password_cmd ( )
2013-08-18 21:08:35 +00:00
host = self . host
if self . ipv6 :
host = ' [ %s ] ' % host
2012-11-05 22:25:40 +00:00
if C . DEFAULT_SCP_IF_SSH :
2012-11-16 22:42:19 +00:00
cmd + = [ " scp " ] + self . common_args
2013-08-18 21:08:35 +00:00
cmd + = [ host + " : " + in_path , out_path ]
2012-11-16 22:42:19 +00:00
indata = None
2012-11-05 22:25:40 +00:00
else :
2013-08-18 21:08:35 +00:00
cmd + = [ " sftp " ] + self . common_args + [ host ]
2012-11-16 22:42:19 +00:00
indata = " get %s %s \n " % ( in_path , out_path )
p = subprocess . Popen ( cmd , stdin = subprocess . PIPE ,
stdout = subprocess . PIPE , stderr = subprocess . PIPE )
self . _send_password ( )
stdout , stderr = p . communicate ( indata )
2012-06-17 11:35:59 +00:00
if p . returncode != 0 :
raise errors . AnsibleError ( " failed to transfer file from %s : \n %s \n %s " % ( in_path , stdout , stderr ) )
def close ( self ) :
2012-07-15 16:29:53 +00:00
''' not applicable since we ' re executing openssh binaries '''
2012-06-17 11:35:59 +00:00
pass
2012-07-15 16:29:53 +00:00