mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
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.
This commit is contained in:
parent
30ce430363
commit
6341a9547f
1 changed files with 50 additions and 13 deletions
|
@ -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)
|
||||
"""
|
||||
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 chan.recv_ready():
|
||||
time.sleep(0.25)
|
||||
sudo_output = chan.recv(bufsize) # Pull prompt, catch errors, eat sudo output
|
||||
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)
|
||||
|
|
Loading…
Reference in a new issue