From a2486785188f44878cd58445970c27b067fa2534 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 14 Jun 2015 22:35:53 -0400 Subject: [PATCH] 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 --- lib/ansible/executor/connection_info.py | 147 +++++++++++++++---- lib/ansible/plugins/connections/__init__.py | 2 +- lib/ansible/plugins/connections/ssh.py | 149 +++++++++----------- 3 files changed, 187 insertions(+), 111 deletions(-) diff --git a/lib/ansible/executor/connection_info.py b/lib/ansible/executor/connection_info.py index d8881f54ab7..d52ae72c396 100644 --- a/lib/ansible/executor/connection_info.py +++ b/lib/ansible/executor/connection_info.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + # (c) 2012-2014, Michael DeHaan # # 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, 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_become_success(self, output, become_settings): - #TODO: implement - pass + 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] != '_'] diff --git a/lib/ansible/plugins/connections/__init__.py b/lib/ansible/plugins/connections/__init__.py index 1d3a2bdeede..449d1379ef6 100644 --- a/lib/ansible/plugins/connections/__init__.py +++ b/lib/ansible/plugins/connections/__init__.py @@ -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 diff --git a/lib/ansible/plugins/connections/ssh.py b/lib/ansible/plugins/connections/ssh.py index 44efbf901ef..353f2400658 100644 --- a/lib/ansible/plugins/connections/ssh.py +++ b/lib/ansible/plugins/connections/ssh.py @@ -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 - - #(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) + 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 = '' + + 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