mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
initial become support to ssh plugin
- password prompt detection and incorrect passwrod detection to connection info - sudoable flag to avoid become on none pe'able commands
This commit is contained in:
parent
a267f93c83
commit
a248678518
3 changed files with 186 additions and 110 deletions
|
@ -1,3 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
|
@ -21,6 +23,8 @@ __metaclass__ = type
|
|||
|
||||
import pipes
|
||||
import random
|
||||
import re
|
||||
import gettext
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.template import Templar
|
||||
|
@ -29,6 +33,40 @@ from ansible.errors import AnsibleError
|
|||
|
||||
__all__ = ['ConnectionInformation']
|
||||
|
||||
SU_PROMPT_LOCALIZATIONS = [
|
||||
'Password',
|
||||
'암호',
|
||||
'パスワード',
|
||||
'Adgangskode',
|
||||
'Contraseña',
|
||||
'Contrasenya',
|
||||
'Hasło',
|
||||
'Heslo',
|
||||
'Jelszó',
|
||||
'Lösenord',
|
||||
'Mật khẩu',
|
||||
'Mot de passe',
|
||||
'Parola',
|
||||
'Parool',
|
||||
'Pasahitza',
|
||||
'Passord',
|
||||
'Passwort',
|
||||
'Salasana',
|
||||
'Sandi',
|
||||
'Senha',
|
||||
'Wachtwoord',
|
||||
'ססמה',
|
||||
'Лозинка',
|
||||
'Парола',
|
||||
'Пароль',
|
||||
'गुप्तशब्द',
|
||||
'शब्दकूट',
|
||||
'సంకేతపదము',
|
||||
'හස්පදය',
|
||||
'密码',
|
||||
'密碼',
|
||||
]
|
||||
|
||||
# the magic variable mapping dictionary below is used to translate
|
||||
# host/inventory variables to fields in the ConnectionInformation
|
||||
# object. The dictionary values are tuples, to account for aliases
|
||||
|
@ -44,6 +82,40 @@ MAGIC_VARIABLE_MAPPING = dict(
|
|||
shell = ('ansible_shell_type',),
|
||||
)
|
||||
|
||||
SU_PROMPT_LOCALIZATIONS = [
|
||||
'Password',
|
||||
'암호',
|
||||
'パスワード',
|
||||
'Adgangskode',
|
||||
'Contraseña',
|
||||
'Contrasenya',
|
||||
'Hasło',
|
||||
'Heslo',
|
||||
'Jelszó',
|
||||
'Lösenord',
|
||||
'Mật khẩu',
|
||||
'Mot de passe',
|
||||
'Parola',
|
||||
'Parool',
|
||||
'Pasahitza',
|
||||
'Passord',
|
||||
'Passwort',
|
||||
'Salasana',
|
||||
'Sandi',
|
||||
'Senha',
|
||||
'Wachtwoord',
|
||||
'ססמה',
|
||||
'Лозинка',
|
||||
'Парола',
|
||||
'Пароль',
|
||||
'गुप्तशब्द',
|
||||
'शब्दकूट',
|
||||
'సంకేతపదము',
|
||||
'හස්පදය',
|
||||
'密码',
|
||||
'密碼',
|
||||
]
|
||||
|
||||
class ConnectionInformation:
|
||||
|
||||
'''
|
||||
|
@ -72,6 +144,14 @@ class ConnectionInformation:
|
|||
self.become_method = None
|
||||
self.become_user = None
|
||||
self.become_pass = passwords.get('become_pass','')
|
||||
self.become_exe = None
|
||||
self.become_flags = None
|
||||
|
||||
# backwards compat
|
||||
self.sudo_exe = None
|
||||
self.sudo_flags = None
|
||||
self.su_exe = None
|
||||
self.su_flags = None
|
||||
|
||||
# general flags (should we move out?)
|
||||
self.verbosity = 0
|
||||
|
@ -202,25 +282,20 @@ class ConnectionInformation:
|
|||
|
||||
return new_info
|
||||
|
||||
def make_become_cmd(self, cmd, executable, become_settings=None):
|
||||
def make_become_cmd(self, cmd, executable ):
|
||||
""" helper function to create privilege escalation commands """
|
||||
|
||||
"""
|
||||
helper function to create privilege escalation commands
|
||||
"""
|
||||
|
||||
# FIXME: become settings should probably be stored in the connection info itself
|
||||
if become_settings is None:
|
||||
become_settings = {}
|
||||
|
||||
randbits = ''.join(chr(random.randint(ord('a'), ord('z'))) for x in xrange(32))
|
||||
success_key = 'BECOME-SUCCESS-%s' % randbits
|
||||
prompt = None
|
||||
becomecmd = None
|
||||
success_key = None
|
||||
|
||||
executable = executable or '$SHELL'
|
||||
|
||||
success_cmd = pipes.quote('echo %s; %s' % (success_key, cmd))
|
||||
if self.become:
|
||||
|
||||
becomecmd = None
|
||||
randbits = ''.join(chr(random.randint(ord('a'), ord('z'))) for x in xrange(32))
|
||||
success_key = 'BECOME-SUCCESS-%s' % randbits
|
||||
executable = executable or '$SHELL'
|
||||
success_cmd = pipes.quote('echo %s; %s' % (success_key, cmd))
|
||||
|
||||
if self.become_method == 'sudo':
|
||||
# Rather than detect if sudo wants a password this time, -k makes sudo always ask for
|
||||
# a password if one is required. Passing a quoted compound command to sudo (or sudo -s)
|
||||
|
@ -228,24 +303,33 @@ class ConnectionInformation:
|
|||
# string to the user's shell. We loop reading output until we see the randomly-generated
|
||||
# sudo prompt set with the -p option.
|
||||
prompt = '[sudo via ansible, key=%s] password: ' % randbits
|
||||
exe = become_settings.get('sudo_exe', C.DEFAULT_SUDO_EXE)
|
||||
flags = become_settings.get('sudo_flags', C.DEFAULT_SUDO_FLAGS)
|
||||
exe = self.become_exe or self.sudo_exe or 'sudo'
|
||||
flags = self.become_flags or self.sudo_flags or ''
|
||||
becomecmd = '%s -k && %s %s -S -p "%s" -u %s %s -c %s' % \
|
||||
(exe, exe, flags or C.DEFAULT_SUDO_FLAGS, prompt, self.become_user, executable, success_cmd)
|
||||
|
||||
elif self.become_method == 'su':
|
||||
exe = become_settings.get('su_exe', C.DEFAULT_SU_EXE)
|
||||
flags = become_settings.get('su_flags', C.DEFAULT_SU_FLAGS)
|
||||
|
||||
def detect_su_prompt(data):
|
||||
SU_PROMPT_LOCALIZATIONS_RE = re.compile("|".join(['(\w+\'s )?' + x + ' ?: ?' for x in SU_PROMPT_LOCALIZATIONS]), flags=re.IGNORECASE)
|
||||
return bool(SU_PROMPT_LOCALIZATIONS_RE.match(data))
|
||||
|
||||
prompt = su_prompt()
|
||||
exe = self.become_exe or self.su_exe or 'su'
|
||||
flags = self.become_flags or self.su_flags or ''
|
||||
becomecmd = '%s %s %s -c "%s -c %s"' % (exe, flags, self.become_user, executable, success_cmd)
|
||||
|
||||
elif self.become_method == 'pbrun':
|
||||
exe = become_settings.get('pbrun_exe', 'pbrun')
|
||||
flags = become_settings.get('pbrun_flags', '')
|
||||
|
||||
prompt='assword:'
|
||||
exe = self.become_exe or 'pbrun'
|
||||
flags = self.become_flags or ''
|
||||
becomecmd = '%s -b -l %s -u %s %s' % (exe, flags, self.become_user, success_cmd)
|
||||
|
||||
elif self.become_method == 'pfexec':
|
||||
exe = become_settings.get('pfexec_exe', 'pbrun')
|
||||
flags = become_settings.get('pfexec_flags', '')
|
||||
|
||||
exe = self.become_exe or 'pfexec'
|
||||
flags = self.become_flags or ''
|
||||
# No user as it uses it's own exec_attr to figure it out
|
||||
becomecmd = '%s %s "%s"' % (exe, flags, success_cmd)
|
||||
|
||||
|
@ -254,11 +338,20 @@ class ConnectionInformation:
|
|||
|
||||
return (('%s -c ' % executable) + pipes.quote(becomecmd), prompt, success_key)
|
||||
|
||||
return (cmd, "", "")
|
||||
return (cmd, prompt, success_key)
|
||||
|
||||
def check_become_success(self, output, become_settings):
|
||||
#TODO: implement
|
||||
pass
|
||||
def check_become_success(self, output, success_key):
|
||||
return success_key in output
|
||||
|
||||
def check_password_prompt(self, output, prompt):
|
||||
if isinstance(prompt, basestring):
|
||||
return output.endswith(prompt)
|
||||
else:
|
||||
return prompt(output)
|
||||
|
||||
def check_incorrect_password(self, output, prompt):
|
||||
incorrect_password = gettext.dgettext(self.become_method, "Sorry, try again.")
|
||||
return output.endswith(incorrect_password)
|
||||
|
||||
def _get_fields(self):
|
||||
return [i for i in self.__dict__.keys() if i[:1] != '_']
|
||||
|
|
|
@ -94,7 +94,7 @@ class ConnectionBase(with_metaclass(ABCMeta, object)):
|
|||
|
||||
@ensure_connect
|
||||
@abstractmethod
|
||||
def exec_command(self, cmd, tmp_path, executable=None, in_data=None):
|
||||
def exec_command(self, cmd, tmp_path, executable=None, in_data=None, sudoable=True):
|
||||
"""Run a command on the remote host"""
|
||||
pass
|
||||
|
||||
|
|
|
@ -110,9 +110,7 @@ class Connection(ConnectionBase):
|
|||
"-o", "PasswordAuthentication=no")
|
||||
if self._connection_info.remote_user is not None and self._connection_info.remote_user != pwd.getpwuid(os.geteuid())[0]:
|
||||
self._common_args += ("-o", "User={0}".format(self._connection_info.remote_user))
|
||||
# FIXME: figure out where this goes
|
||||
#self._common_args += ("-o", "ConnectTimeout={0}".format(self.runner.timeout))
|
||||
self._common_args += ("-o", "ConnectTimeout=15")
|
||||
self._common_args += ("-o", "ConnectTimeout={0}".format(self._connection_info.timeout))
|
||||
|
||||
self._connected = True
|
||||
|
||||
|
@ -171,24 +169,14 @@ class Connection(ConnectionBase):
|
|||
while True:
|
||||
rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
|
||||
|
||||
# FIXME: su/sudo stuff
|
||||
# fail early if the sudo/su password is wrong
|
||||
#if self.runner.sudo and sudoable:
|
||||
# if self.runner.sudo_pass:
|
||||
# incorrect_password = gettext.dgettext(
|
||||
# "sudo", "Sorry, try again.")
|
||||
# if stdout.endswith("%s\r\n%s" % (incorrect_password,
|
||||
# prompt)):
|
||||
# raise AnsibleError('Incorrect sudo password')
|
||||
#
|
||||
# if stdout.endswith(prompt):
|
||||
# raise AnsibleError('Missing sudo password')
|
||||
#
|
||||
#if self.runner.su and su and self.runner.su_pass:
|
||||
# incorrect_password = gettext.dgettext(
|
||||
# "su", "Sorry")
|
||||
# if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
|
||||
# raise AnsibleError('Incorrect su password')
|
||||
# fail early if the become password is wrong
|
||||
if self._connection_info.become and sudoable:
|
||||
if self._connection_info.become_pass:
|
||||
if self._connection_info.check_incorrect_password(stdout, prompt):
|
||||
raise AnsibleError('Incorrect %s password', self._connection_info.become_method)
|
||||
|
||||
elif self._connection_info.check_password_prompt(stdout, prompt):
|
||||
raise AnsibleError('Missing %s password', self._connection_info.become_method)
|
||||
|
||||
if p.stdout in rfd:
|
||||
dat = os.read(p.stdout.fileno(), 9000)
|
||||
|
@ -270,10 +258,10 @@ class Connection(ConnectionBase):
|
|||
self._display.vvv("EXEC previous known host file not found for {0}".format(host))
|
||||
return True
|
||||
|
||||
def exec_command(self, cmd, tmp_path, executable='/bin/sh', in_data=None):
|
||||
def exec_command(self, cmd, tmp_path, executable='/bin/sh', in_data=None, sudoable=True):
|
||||
''' run a command on the remote host '''
|
||||
|
||||
super(Connection, self).exec_command(cmd, tmp_path, executable=executable, in_data=in_data)
|
||||
super(Connection, self).exec_command(cmd, tmp_path, executable=executable, in_data=in_data, sudoable=False)
|
||||
|
||||
host = self._connection_info.remote_addr
|
||||
|
||||
|
@ -294,6 +282,11 @@ class Connection(ConnectionBase):
|
|||
ssh_cmd += ['-6']
|
||||
ssh_cmd.append(host)
|
||||
|
||||
prompt = None
|
||||
success_key = ''
|
||||
if sudoable:
|
||||
cmd, prompt, success_key = self._connection_info.make_become_cmd(cmd, executable)
|
||||
|
||||
ssh_cmd.append(cmd)
|
||||
self._display.vvv("EXEC {0}".format(' '.join(ssh_cmd)), host=host)
|
||||
|
||||
|
@ -306,72 +299,62 @@ class Connection(ConnectionBase):
|
|||
# fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
|
||||
# fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
|
||||
|
||||
|
||||
# create process
|
||||
(p, stdin) = self._run(ssh_cmd, in_data)
|
||||
|
||||
self._send_password()
|
||||
if prompt:
|
||||
self._send_password()
|
||||
|
||||
no_prompt_out = ''
|
||||
no_prompt_err = ''
|
||||
# FIXME: su/sudo stuff
|
||||
#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 True:
|
||||
# if success_key in sudo_output or \
|
||||
# (self.runner.sudo_pass and sudo_output.endswith(prompt)) or \
|
||||
# (self.runner.su_pass and utils.su_prompts.check_su_prompt(sudo_output)):
|
||||
# break
|
||||
#
|
||||
# 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 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 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 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 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')
|
||||
# else:
|
||||
# no_prompt_out += sudo_output
|
||||
# no_prompt_err += sudo_errput
|
||||
q(self._connection_info.password)
|
||||
if self._connection_info.become and sudoable and self._connection_info.password:
|
||||
# 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)
|
||||
become_output = ''
|
||||
become_errput = ''
|
||||
|
||||
#(returncode, stdout, stderr) = self._communicate(p, stdin, in_data, su=su, sudoable=sudoable, prompt=prompt)
|
||||
# FIXME: the prompt won't be here anymore
|
||||
prompt=""
|
||||
(returncode, stdout, stderr) = self._communicate(p, stdin, in_data, prompt=prompt)
|
||||
while True:
|
||||
if self._connection_info.check_become_success(become_output, success_key) or \
|
||||
self._connection_info.check_password_prompt(become_output, prompt ):
|
||||
break
|
||||
rfd, wfd, efd = select.select([p.stdout, p.stderr], [], [p.stdout], self._connection_info.timeout)
|
||||
if p.stderr in rfd:
|
||||
chunk = p.stderr.read()
|
||||
if not chunk:
|
||||
raise AnsibleError('ssh connection closed waiting for privilege escalation password prompt')
|
||||
become_errput += chunk
|
||||
|
||||
if self._connection_info.check_incorrect_password(become_errput, prompt):
|
||||
raise AnsibleError('Incorrect %s password', self._connection_info.become_method)
|
||||
|
||||
if p.stdout in rfd:
|
||||
chunk = p.stdout.read()
|
||||
if not chunk:
|
||||
raise AnsibleError('ssh connection closed waiting for sudo or su password prompt')
|
||||
become_output += chunk
|
||||
|
||||
if not rfd:
|
||||
# timeout. wrap up process communication
|
||||
stdout = p.communicate()
|
||||
raise AnsibleError('ssh connection error waiting for sudo or su password prompt')
|
||||
|
||||
if not self._connection_info.check_become_success(become_output, success_key):
|
||||
if sudoable:
|
||||
stdin.write(self._connection_info.password + '\n')
|
||||
else:
|
||||
no_prompt_out += become_output
|
||||
no_prompt_err += become_errput
|
||||
|
||||
(returncode, stdout, stderr) = self._communicate(p, stdin, in_data, sudoable=sudoable, prompt=prompt)
|
||||
|
||||
#if C.HOST_KEY_CHECKING and not_in_host_file:
|
||||
# # lock around the initial SSH connectivity so the user prompt about whether to add
|
||||
|
|
Loading…
Reference in a new issue