From 6341a9547f301f04e45848e86848b878962b3dff Mon Sep 17 00:00:00 2001 From: jkleint Date: Thu, 26 Apr 2012 15:01:20 -0300 Subject: [PATCH] Actually wait for password prompt in remote sudo execution. When running on lots of hosts with a large login banner on a slow network, it was still possible that the first recv() didn't to pull in the sudo password prompt, and sudo would fail intermittently. This patch tells sudo to use a specific, randomly-generated prompt and then reads until it finds that prompt (or times out). Only then is the password sent. It also catches `socket.timeout` and thunks it to a more useful `AnsbileError` with the output of sudo so if something goes wrong you can see what's up. --- lib/ansible/connection.py | 63 +++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/lib/ansible/connection.py b/lib/ansible/connection.py index ca85cdc1b9e..b54ab956f3c 100644 --- a/lib/ansible/connection.py +++ b/lib/ansible/connection.py @@ -26,6 +26,8 @@ import re import shutil import subprocess import pipes +import socket +import random from ansible import errors # prevent paramiko warning noise @@ -37,6 +39,7 @@ with warnings.catch_warnings(): ################################################ +RANDOM_PROMPT_LEN = 32 # 32 random chars in [a-z] gives > 128 bits of entropy class Connection(object): @@ -142,19 +145,53 @@ class ParamikoConnection(object): quoted_command = '"$SHELL" -c ' + pipes.quote(cmd) chan.exec_command(quoted_command) else: - # Rather than detect if sudo wants a password this time, -k makes - # sudo always ask for a password if one is required. The "--" - # tells sudo that this is the end of sudo options and the command - # follows. Passing a quoted compound command to sudo (or sudo -s) - # directly doesn't work, so we shellquote it with pipes.quote() - # and pass the quoted string to the user's shell. - sudocmd = 'sudo -k -- "$SHELL" -c ' + pipes.quote(cmd) - chan.exec_command(sudocmd) - if self.runner.sudo_pass: - while not chan.recv_ready(): - time.sleep(0.25) - sudo_output = chan.recv(bufsize) # Pull prompt, catch errors, eat sudo output - chan.sendall(self.runner.sudo_pass + '\n') + """ + Sudo strategy: + + First, if sudo doesn't need a password, it's easy: just run the + command. + + If we need a password, we want to read everything up to and + including the prompt before sending the password. This is so sudo + doesn't block sending the prompt, to catch any errors running sudo + itself, and so sudo's output doesn't gunk up the command's output. + Some systems have large login banners and slow networks, so the + prompt isn't guaranteed to be in the first chunk we read. So, we + have to keep reading until we find the password prompt, or timeout + trying. + + In order to detect the password prompt, we set it ourselves with + the sudo -p switch. We use a random prompt so that a) it's + exceedingly unlikely anyone's login material contains it and b) you + can't forge it. This can fail if passprompt_override is set in + /etc/sudoers. + + Some systems are set to remember your sudo credentials for a set + period across terminals and won't prompt for a password. We use + sudo -k so it always asks for the password every time (if one is + required) to avoid dealing with both cases. + + The "--" tells sudo that this is the end of sudo options and the + command follows. + + We shell quote the command for safety, and since we can't run a quoted + command directly with sudo (or sudo -s), we actually run the user's + shell and pass the quoted command string to the shell's -c option. + """ + prompt = '[sudo via ansible, key=%s] password: ' % ''.join(chr(random.randint(ord('a'), ord('z'))) for _ in xrange(RANDOM_PROMPT_LEN)) + sudocmd = 'sudo -k -p "%s" -- "$SHELL" -c %s' % (prompt, pipes.quote(cmd)) + sudo_output = '' + try: + chan.exec_command(sudocmd) + if self.runner.sudo_pass: + while not sudo_output.endswith(prompt): + chunk = chan.recv(bufsize) + if not chunk: + raise errors.AnsibleError('ssh connection closed waiting for sudo password prompt') + sudo_output += chunk + chan.sendall(self.runner.sudo_pass + '\n') + except socket.timeout: + raise errors.AnsibleError('ssh timed out waiting for sudo.\n' + sudo_output) stdin = chan.makefile('wb', bufsize) stdout = chan.makefile('rb', bufsize)