mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
More robust remote sudo.
The basic idea is sudo /bin/sh -c 'quoted_command'. We use Paramiko's low-level API to set a timeout, get a pseudo tty, execute sudo and the (shell quoted) command atomically, wait just until sudo is ready to accept the password before sending it down the pipe, and then return the command's stdout and stderr. This should be faster, as there are no unneeded sleeps. There are no permissions issues reading the output. It will raise socket.timeout if the command takes too long. However, this is a per-read timeout, not a total execution timeout, so as long as the command is writing output and you are reading it, it will not time out. Local and non-sudo commands remain unchanged, but should probably adopt a similar approach. Since this is a significant change, it needs a lot of testing. Also, someone smarter than I should double-check the quoting and execution, since it is a security issue.
This commit is contained in:
parent
ec56b30248
commit
e69e078569
1 changed files with 35 additions and 56 deletions
|
@ -19,20 +19,21 @@
|
||||||
################################################
|
################################################
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
import traceback
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import pipes
|
||||||
|
|
||||||
|
from ansible import errors
|
||||||
# prevent paramiko warning noise
|
# prevent paramiko warning noise
|
||||||
# see http://stackoverflow.com/questions/3920502/
|
# see http://stackoverflow.com/questions/3920502/
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter("ignore")
|
warnings.simplefilter("ignore")
|
||||||
import paramiko
|
import paramiko
|
||||||
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from ansible import errors
|
|
||||||
|
|
||||||
################################################
|
################################################
|
||||||
|
|
||||||
|
@ -94,65 +95,44 @@ class ParamikoConnection(object):
|
||||||
|
|
||||||
return ssh
|
return ssh
|
||||||
|
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
''' connect to the remote host '''
|
''' connect to the remote host '''
|
||||||
|
|
||||||
self.ssh = self._get_conn()
|
self.ssh = self._get_conn()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, sudoable=False):
|
def exec_command(self, cmd, tmp_path, sudoable=False): # pylint: disable-msg=W0613
|
||||||
''' run a command on the remote host '''
|
''' run a command on the remote host '''
|
||||||
if not self.runner.sudo or not sudoable:
|
if not self.runner.sudo or not sudoable:
|
||||||
stdin, stdout, stderr = self.ssh.exec_command(cmd)
|
stdin, stdout, stderr = self.ssh.exec_command(cmd)
|
||||||
return (stdin, stdout, stderr)
|
return (stdin, stdout, stderr)
|
||||||
else:
|
else:
|
||||||
# percalculated tmp_path is ONLY required for sudo usage
|
# Rather than detect if sudo wants a password this time, -k makes
|
||||||
if tmp_path is None:
|
# sudo always ask for a password if one is required. The "--"
|
||||||
raise Exception("expecting tmp_path")
|
# tells sudo that this is the end of sudo options and the command
|
||||||
r = random.randint(0,99999)
|
# follows. Passing a quoted compound command to sudo (or sudo -s)
|
||||||
|
# directly doesn't work, so we shellquote it with pipes.quote()
|
||||||
# invoke command using a new connection over sudo
|
# and pass the quoted string to the user's shell.
|
||||||
result_file = os.path.join(tmp_path, "sudo_result.%s" % r)
|
sudocmd = 'sudo -k -- "$SHELL" -c ' + pipes.quote(cmd)
|
||||||
self.ssh.close()
|
bufsize = 4096 # Could make this a Runner param if needed
|
||||||
ssh_sudo = self._get_conn()
|
timeout_secs = self.runner.timeout # Reusing runner's TCP connect timeout as command progress timeout
|
||||||
sudo_chan = ssh_sudo.invoke_shell()
|
chan = self.ssh.get_transport().open_session()
|
||||||
sudo_chan.send("sudo -s\n")
|
chan.settimeout(timeout_secs)
|
||||||
|
chan.get_pty() # Many sudo setups require a terminal
|
||||||
# FIXME: using sudo with a password adds more delay, someone may wish
|
#print "exec_command: " + sudocmd
|
||||||
# to optimize to see when the channel is actually ready
|
chan.exec_command(sudocmd)
|
||||||
if self.runner.sudo_pass:
|
if self.runner.sudo_pass:
|
||||||
time.sleep(0.1) # this is conservative
|
while not chan.recv_ready():
|
||||||
sudo_chan.send("%s\n" % self.runner.sudo_pass)
|
time.sleep(0.25)
|
||||||
time.sleep(0.1)
|
sudo_output = chan.recv(bufsize) # Pull prompt, catch errors, eat sudo output
|
||||||
|
#print "exec_command: " + sudo_output
|
||||||
# to avoid ssh expect logic, redirect output to file and move the
|
#print "exec_command: sending password"
|
||||||
# file when we are done with it...
|
chan.sendall(self.runner.sudo_pass + '\n')
|
||||||
sudo_chan.send("(%s >%s_pre 2>/dev/null ; mv %s_pre %s) &\n" % (cmd, result_file, result_file, result_file))
|
|
||||||
# FIXME: someone may wish to optimize to not background the launch, and tell when the command
|
stdin = chan.makefile('wb', bufsize)
|
||||||
# returns, removing the time.sleep(1) here
|
stdout = chan.makefile('rb', bufsize)
|
||||||
time.sleep(1)
|
stderr = chan.makefile_stderr('rb', bufsize)
|
||||||
sudo_chan.close()
|
return stdin, stdout, stderr
|
||||||
self.ssh = self._get_conn()
|
|
||||||
|
|
||||||
# now load the results of the JSON execution...
|
|
||||||
# FIXME: really need some timeout logic here
|
|
||||||
# though it doesn't make since to use the SSH timeout or impose any particular
|
|
||||||
# limit. Upgrades welcome.
|
|
||||||
sftp = self.ssh.open_sftp()
|
|
||||||
while True:
|
|
||||||
# print "waiting on %s" % result_file
|
|
||||||
time.sleep(1)
|
|
||||||
try:
|
|
||||||
sftp.stat(result_file)
|
|
||||||
break
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
sftp.close()
|
|
||||||
# TODO: see if there's a SFTP way to just get the file contents w/o saving
|
|
||||||
# to disk vs this hack...
|
|
||||||
stdin, stdout, stderr = self.ssh.exec_command("cat %s" % result_file)
|
|
||||||
return (stdin, stdout, stderr)
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
def put_file(self, in_path, out_path):
|
||||||
''' transfer a file from local to remote '''
|
''' transfer a file from local to remote '''
|
||||||
|
@ -231,4 +211,3 @@ class LocalConnection(object):
|
||||||
''' terminate the connection; nothing to do here '''
|
''' terminate the connection; nothing to do here '''
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue