mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-25 03:41:25 -07:00
We break the read while loop after waiting "the end of the process" and the pipes are empty, otherwise we do another select that waits all the timeout.
384 lines
16 KiB
Python
384 lines
16 KiB
Python
# (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
|
|
import hmac
|
|
import pwd
|
|
import gettext
|
|
import pty
|
|
from hashlib import sha1
|
|
import ansible.constants as C
|
|
from ansible.callbacks import vvv
|
|
from ansible import errors
|
|
from ansible import utils
|
|
|
|
class Connection(object):
|
|
''' ssh based connections '''
|
|
|
|
def __init__(self, runner, host, port, user, password, private_key_file, *args, **kwargs):
|
|
self.runner = runner
|
|
self.host = host
|
|
self.ipv6 = ':' in self.host
|
|
self.port = port
|
|
self.user = user
|
|
self.password = password
|
|
self.private_key_file = private_key_file
|
|
self.HASHED_KEY_MAGIC = "|1|"
|
|
self.has_pipelining = True
|
|
|
|
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)
|
|
|
|
def connect(self):
|
|
''' connect to the remote host '''
|
|
|
|
vvv("ESTABLISH CONNECTION FOR USER: %s" % self.user, host=self.host)
|
|
|
|
self.common_args = []
|
|
extra_args = C.ANSIBLE_SSH_ARGS
|
|
if extra_args is not None:
|
|
self.common_args += shlex.split(extra_args)
|
|
else:
|
|
self.common_args += ["-o", "ControlMaster=auto",
|
|
"-o", "ControlPersist=60s",
|
|
"-o", "ControlPath=%s" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self.cp_dir))]
|
|
|
|
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:
|
|
self.common_args += ["-o", "ControlPath=%s" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self.cp_dir))]
|
|
|
|
if not C.HOST_KEY_CHECKING:
|
|
self.common_args += ["-o", "StrictHostKeyChecking=no"]
|
|
|
|
if self.port is not None:
|
|
self.common_args += ["-o", "Port=%d" % (self.port)]
|
|
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:
|
|
self.common_args += ["-o", "IdentityFile="+os.path.expanduser(self.runner.private_key_file)]
|
|
if self.password:
|
|
self.common_args += ["-o", "GSSAPIAuthentication=no",
|
|
"-o", "PubkeyAuthentication=no"]
|
|
else:
|
|
self.common_args += ["-o", "KbdInteractiveAuthentication=no",
|
|
"-o", "PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey",
|
|
"-o", "PasswordAuthentication=no"]
|
|
if self.user != pwd.getpwuid(os.geteuid())[0]:
|
|
self.common_args += ["-o", "User="+self.user]
|
|
self.common_args += ["-o", "ConnectTimeout=%d" % self.runner.timeout]
|
|
|
|
return self
|
|
|
|
def _password_cmd(self):
|
|
if self.password:
|
|
try:
|
|
p = subprocess.Popen(["sshpass"], stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
p.communicate()
|
|
except OSError:
|
|
raise errors.AnsibleError("to use the 'ssh' connection type with passwords, you must install the sshpass program")
|
|
(self.rfd, self.wfd) = os.pipe()
|
|
return ["sshpass", "-d%d" % self.rfd]
|
|
return []
|
|
|
|
def _send_password(self):
|
|
if self.password:
|
|
os.close(self.rfd)
|
|
os.write(self.wfd, "%s\n" % self.password)
|
|
os.close(self.wfd)
|
|
|
|
def not_in_host_file(self, host):
|
|
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)
|
|
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()
|
|
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
|
|
return True
|
|
|
|
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su_user=None, su=False):
|
|
''' run a command on the remote host '''
|
|
|
|
ssh_cmd = self._password_cmd()
|
|
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"]
|
|
if utils.VERBOSITY > 3:
|
|
ssh_cmd += ["-vvv"]
|
|
else:
|
|
ssh_cmd += ["-q"]
|
|
ssh_cmd += self.common_args
|
|
|
|
if self.ipv6:
|
|
ssh_cmd += ['-6']
|
|
ssh_cmd += [self.host]
|
|
|
|
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:
|
|
if executable:
|
|
ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd))
|
|
else:
|
|
ssh_cmd.append(cmd)
|
|
else:
|
|
sudocmd, prompt, success_key = utils.make_sudo_cmd(sudo_user, executable, cmd)
|
|
ssh_cmd.append(sudocmd)
|
|
|
|
vvv("EXEC %s" % ssh_cmd, host=self.host)
|
|
|
|
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.
|
|
fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
|
|
fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
|
|
|
|
# create process
|
|
if in_data:
|
|
# do not use pseudo-pty
|
|
p = subprocess.Popen(ssh_cmd, stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
stdin = p.stdin
|
|
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
|
|
|
|
self._send_password()
|
|
|
|
if (self.runner.sudo and sudoable and self.runner.sudo_pass) or \
|
|
(self.runner.su and su and self.runner.su_pass):
|
|
# 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)
|
|
fcntl.fcntl(p.stdout, fcntl.F_SETFL,
|
|
fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
|
fcntl.fcntl(p.stderr, fcntl.F_SETFL,
|
|
fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
|
|
sudo_output = ''
|
|
sudo_errput = ''
|
|
|
|
while not sudo_output.endswith(prompt) and success_key not in sudo_output:
|
|
rfd, wfd, efd = select.select([p.stdout, p.stderr], [],
|
|
[p.stdout], self.runner.timeout)
|
|
if p.stderr in rfd:
|
|
chunk = p.stderr.read()
|
|
if not chunk:
|
|
raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
|
|
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')
|
|
|
|
if p.stdout in rfd:
|
|
chunk = p.stdout.read()
|
|
if not chunk:
|
|
raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
|
|
sudo_output += chunk
|
|
|
|
if not rfd:
|
|
# timeout. wrap up process communication
|
|
stdout = p.communicate()
|
|
raise errors.AnsibleError('ssh connection error waiting for sudo or su password prompt')
|
|
|
|
if success_key not in sudo_output:
|
|
if sudoable:
|
|
stdin.write(self.runner.sudo_pass + '\n')
|
|
elif su:
|
|
stdin.write(self.runner.su_pass + '\n')
|
|
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
|
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
|
# We can't use p.communicate here because the ControlMaster may have stdout open as well
|
|
stdout = ''
|
|
stderr = ''
|
|
rpipes = [p.stdout, p.stderr]
|
|
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')
|
|
while True:
|
|
rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
|
|
|
|
# fail early if the sudo/su password is wrong
|
|
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)):
|
|
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')
|
|
|
|
if p.stdout in rfd:
|
|
dat = os.read(p.stdout.fileno(), 9000)
|
|
stdout += dat
|
|
if dat == '':
|
|
rpipes.remove(p.stdout)
|
|
if p.stderr in rfd:
|
|
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:
|
|
break
|
|
# Calling wait while there are still pipes to read can cause a lock
|
|
elif not rpipes and p.poll() == None:
|
|
p.wait()
|
|
# the process has finished and the pipes are empty,
|
|
# if we loop and do the select it waits all the timeout
|
|
break
|
|
stdin.close() # close stdin after we read from stdout (see also issue #848)
|
|
|
|
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.
|
|
fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_UN)
|
|
fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
|
|
controlpersisterror = stderr.find('Bad configuration option: ControlPersist') != -1 or stderr.find('unknown configuration option: ControlPersist') != -1
|
|
|
|
if C.HOST_KEY_CHECKING:
|
|
if ssh_cmd[0] == "sshpass" and p.returncode == 6:
|
|
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.')
|
|
|
|
if p.returncode != 0 and controlpersisterror:
|
|
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')
|
|
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')
|
|
|
|
return (p.returncode, '', stdout, stderr)
|
|
|
|
def put_file(self, in_path, out_path):
|
|
''' transfer a file from local to remote '''
|
|
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
|
if not os.path.exists(in_path):
|
|
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
cmd = self._password_cmd()
|
|
|
|
host = self.host
|
|
if self.ipv6:
|
|
host = '[%s]' % host
|
|
|
|
if C.DEFAULT_SCP_IF_SSH:
|
|
cmd += ["scp"] + self.common_args
|
|
cmd += [in_path,host + ":" + pipes.quote(out_path)]
|
|
indata = None
|
|
else:
|
|
cmd += ["sftp"] + self.common_args + [host]
|
|
indata = "put %s %s\n" % (pipes.quote(in_path), pipes.quote(out_path))
|
|
|
|
p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
self._send_password()
|
|
stdout, stderr = p.communicate(indata)
|
|
|
|
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 '''
|
|
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
|
cmd = self._password_cmd()
|
|
|
|
host = self.host
|
|
if self.ipv6:
|
|
host = '[%s]' % host
|
|
|
|
if C.DEFAULT_SCP_IF_SSH:
|
|
cmd += ["scp"] + self.common_args
|
|
cmd += [host + ":" + in_path, out_path]
|
|
indata = None
|
|
else:
|
|
cmd += ["sftp"] + self.common_args + [host]
|
|
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)
|
|
|
|
if p.returncode != 0:
|
|
raise errors.AnsibleError("failed to transfer file from %s:\n%s\n%s" % (in_path, stdout, stderr))
|
|
|
|
def close(self):
|
|
''' not applicable since we're executing openssh binaries '''
|
|
pass
|
|
|