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:
Brian Coca 2015-06-14 22:35:53 -04:00
parent a267f93c83
commit a248678518
3 changed files with 186 additions and 110 deletions

View file

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> # (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
# #
# This file is part of Ansible # This file is part of Ansible
@ -21,6 +23,8 @@ __metaclass__ = type
import pipes import pipes
import random import random
import re
import gettext
from ansible import constants as C from ansible import constants as C
from ansible.template import Templar from ansible.template import Templar
@ -29,6 +33,40 @@ from ansible.errors import AnsibleError
__all__ = ['ConnectionInformation'] __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 # the magic variable mapping dictionary below is used to translate
# host/inventory variables to fields in the ConnectionInformation # host/inventory variables to fields in the ConnectionInformation
# object. The dictionary values are tuples, to account for aliases # object. The dictionary values are tuples, to account for aliases
@ -44,6 +82,40 @@ MAGIC_VARIABLE_MAPPING = dict(
shell = ('ansible_shell_type',), 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: class ConnectionInformation:
''' '''
@ -72,6 +144,14 @@ class ConnectionInformation:
self.become_method = None self.become_method = None
self.become_user = None self.become_user = None
self.become_pass = passwords.get('become_pass','') 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?) # general flags (should we move out?)
self.verbosity = 0 self.verbosity = 0
@ -202,25 +282,20 @@ class ConnectionInformation:
return new_info 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 prompt = None
becomecmd = None success_key = None
executable = executable or '$SHELL'
success_cmd = pipes.quote('echo %s; %s' % (success_key, cmd))
if self.become: 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': if self.become_method == 'sudo':
# Rather than detect if sudo wants a password this time, -k makes sudo always ask for # 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) # 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 # string to the user's shell. We loop reading output until we see the randomly-generated
# sudo prompt set with the -p option. # sudo prompt set with the -p option.
prompt = '[sudo via ansible, key=%s] password: ' % randbits prompt = '[sudo via ansible, key=%s] password: ' % randbits
exe = become_settings.get('sudo_exe', C.DEFAULT_SUDO_EXE) exe = self.become_exe or self.sudo_exe or 'sudo'
flags = become_settings.get('sudo_flags', C.DEFAULT_SUDO_FLAGS) flags = self.become_flags or self.sudo_flags or ''
becomecmd = '%s -k && %s %s -S -p "%s" -u %s %s -c %s' % \ 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) (exe, exe, flags or C.DEFAULT_SUDO_FLAGS, prompt, self.become_user, executable, success_cmd)
elif self.become_method == 'su': 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) becomecmd = '%s %s %s -c "%s -c %s"' % (exe, flags, self.become_user, executable, success_cmd)
elif self.become_method == 'pbrun': 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) becomecmd = '%s -b -l %s -u %s %s' % (exe, flags, self.become_user, success_cmd)
elif self.become_method == 'pfexec': 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 # No user as it uses it's own exec_attr to figure it out
becomecmd = '%s %s "%s"' % (exe, flags, success_cmd) 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 (('%s -c ' % executable) + pipes.quote(becomecmd), prompt, success_key)
return (cmd, "", "") return (cmd, prompt, success_key)
def check_become_success(self, output, become_settings): def check_become_success(self, output, success_key):
#TODO: implement return success_key in output
pass
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): def _get_fields(self):
return [i for i in self.__dict__.keys() if i[:1] != '_'] return [i for i in self.__dict__.keys() if i[:1] != '_']

View file

@ -94,7 +94,7 @@ class ConnectionBase(with_metaclass(ABCMeta, object)):
@ensure_connect @ensure_connect
@abstractmethod @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""" """Run a command on the remote host"""
pass pass

View file

@ -110,9 +110,7 @@ class Connection(ConnectionBase):
"-o", "PasswordAuthentication=no") "-o", "PasswordAuthentication=no")
if self._connection_info.remote_user is not None and self._connection_info.remote_user != pwd.getpwuid(os.geteuid())[0]: 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)) 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._connection_info.timeout))
#self._common_args += ("-o", "ConnectTimeout={0}".format(self.runner.timeout))
self._common_args += ("-o", "ConnectTimeout=15")
self._connected = True self._connected = True
@ -171,24 +169,14 @@ class Connection(ConnectionBase):
while True: while True:
rfd, wfd, efd = select.select(rpipes, [], rpipes, 1) rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
# FIXME: su/sudo stuff # fail early if the become password is wrong
# fail early if the sudo/su password is wrong if self._connection_info.become and sudoable:
#if self.runner.sudo and sudoable: if self._connection_info.become_pass:
# if self.runner.sudo_pass: if self._connection_info.check_incorrect_password(stdout, prompt):
# incorrect_password = gettext.dgettext( raise AnsibleError('Incorrect %s password', self._connection_info.become_method)
# "sudo", "Sorry, try again.")
# if stdout.endswith("%s\r\n%s" % (incorrect_password, elif self._connection_info.check_password_prompt(stdout, prompt):
# prompt)): raise AnsibleError('Missing %s password', self._connection_info.become_method)
# 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')
if p.stdout in rfd: if p.stdout in rfd:
dat = os.read(p.stdout.fileno(), 9000) 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)) self._display.vvv("EXEC previous known host file not found for {0}".format(host))
return True 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 ''' ''' 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 host = self._connection_info.remote_addr
@ -294,6 +282,11 @@ class Connection(ConnectionBase):
ssh_cmd += ['-6'] ssh_cmd += ['-6']
ssh_cmd.append(host) 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) ssh_cmd.append(cmd)
self._display.vvv("EXEC {0}".format(' '.join(ssh_cmd)), host=host) 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.process_lockfile, fcntl.LOCK_EX)
# fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX) # fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
# create process # create process
(p, stdin) = self._run(ssh_cmd, in_data) (p, stdin) = self._run(ssh_cmd, in_data)
self._send_password() if prompt:
self._send_password()
no_prompt_out = '' no_prompt_out = ''
no_prompt_err = '' no_prompt_err = ''
# FIXME: su/sudo stuff q(self._connection_info.password)
#if (self.runner.sudo and sudoable and self.runner.sudo_pass) or \ if self._connection_info.become and sudoable and self._connection_info.password:
# (self.runner.su and su and self.runner.su_pass): # several cases are handled for sudo privileges with password
# # several cases are handled for sudo privileges with password # * NOPASSWD (tty & no-tty): detect success_key on stdout
# # * NOPASSWD (tty & no-tty): detect success_key on stdout # * without NOPASSWD:
# # * without NOPASSWD: # * detect prompt on stdout (tty)
# # * detect prompt on stdout (tty) # * detect prompt on stderr (no-tty)
# # * detect prompt on stderr (no-tty) fcntl.fcntl(p.stdout, fcntl.F_SETFL,
# fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
# fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK) fcntl.fcntl(p.stderr, fcntl.F_SETFL,
# fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
# fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK) become_output = ''
# sudo_output = '' become_errput = ''
# 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
#(returncode, stdout, stderr) = self._communicate(p, stdin, in_data, su=su, sudoable=sudoable, prompt=prompt) while True:
# FIXME: the prompt won't be here anymore if self._connection_info.check_become_success(become_output, success_key) or \
prompt="" self._connection_info.check_password_prompt(become_output, prompt ):
(returncode, stdout, stderr) = self._communicate(p, stdin, in_data, prompt=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: #if C.HOST_KEY_CHECKING and not_in_host_file:
# # lock around the initial SSH connectivity so the user prompt about whether to add # # lock around the initial SSH connectivity so the user prompt about whether to add