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 shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import pipes
|
import pipes
|
||||||
|
import socket
|
||||||
|
import random
|
||||||
|
|
||||||
from ansible import errors
|
from ansible import errors
|
||||||
# prevent paramiko warning noise
|
# 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):
|
class Connection(object):
|
||||||
|
@ -142,19 +145,53 @@ class ParamikoConnection(object):
|
||||||
quoted_command = '"$SHELL" -c ' + pipes.quote(cmd)
|
quoted_command = '"$SHELL" -c ' + pipes.quote(cmd)
|
||||||
chan.exec_command(quoted_command)
|
chan.exec_command(quoted_command)
|
||||||
else:
|
else:
|
||||||
# Rather than detect if sudo wants a password this time, -k makes
|
"""
|
||||||
# sudo always ask for a password if one is required. The "--"
|
Sudo strategy:
|
||||||
# tells sudo that this is the end of sudo options and the command
|
|
||||||
# follows. Passing a quoted compound command to sudo (or sudo -s)
|
First, if sudo doesn't need a password, it's easy: just run the
|
||||||
# directly doesn't work, so we shellquote it with pipes.quote()
|
command.
|
||||||
# and pass the quoted string to the user's shell.
|
|
||||||
sudocmd = 'sudo -k -- "$SHELL" -c ' + pipes.quote(cmd)
|
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)
|
chan.exec_command(sudocmd)
|
||||||
if self.runner.sudo_pass:
|
if self.runner.sudo_pass:
|
||||||
while not chan.recv_ready():
|
while not sudo_output.endswith(prompt):
|
||||||
time.sleep(0.25)
|
chunk = chan.recv(bufsize)
|
||||||
sudo_output = chan.recv(bufsize) # Pull prompt, catch errors, eat sudo output
|
if not chunk:
|
||||||
|
raise errors.AnsibleError('ssh connection closed waiting for sudo password prompt')
|
||||||
|
sudo_output += chunk
|
||||||
chan.sendall(self.runner.sudo_pass + '\n')
|
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)
|
stdin = chan.makefile('wb', bufsize)
|
||||||
stdout = chan.makefile('rb', bufsize)
|
stdout = chan.makefile('rb', bufsize)
|
||||||
|
|
Loading…
Reference in a new issue