From 2372a3b7347a4cd8e0c3ee0f68dba8522d2423c1 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Thu, 29 Mar 2012 22:58:10 -0400 Subject: [PATCH] Sudo support operational in both playbooks and main program. Implementation could use some cleanup. --- examples/playbooks/intro_example.yml | 4 +++ lib/ansible/connection.py | 48 +++++++++++++++++----------- lib/ansible/playbook.py | 20 +++++++----- lib/ansible/runner.py | 16 +++++----- 4 files changed, 54 insertions(+), 34 deletions(-) diff --git a/examples/playbooks/intro_example.yml b/examples/playbooks/intro_example.yml index 66acae50f75..1fb31d2688b 100644 --- a/examples/playbooks/intro_example.yml +++ b/examples/playbooks/intro_example.yml @@ -10,6 +10,10 @@ - hosts: all user: root +# could have also have done: +# user: mdehaan +# sudo: True + # make these variables available inside of templates # for when we use the 'template' action/module later on... diff --git a/lib/ansible/connection.py b/lib/ansible/connection.py index 11838a18a7d..f8e01c5e0b3 100755 --- a/lib/ansible/connection.py +++ b/lib/ansible/connection.py @@ -22,6 +22,7 @@ import paramiko import traceback import os import time +import random from ansible import errors ################################################ @@ -80,36 +81,47 @@ class ParamikoConnection(object): self.ssh = self._get_conn() return self - def exec_command(self, cmd, sudoable=True): - + def exec_command(self, cmd, tmp_path, sudoable=False): ''' run a command on the remote host ''' if not self.runner.sudo or not sudoable: stdin, stdout, stderr = self.ssh.exec_command(cmd) return (stdin, stdout, stderr) else: + # percalculated tmp_path is ONLY required for sudo usage + if tmp_path is None: + raise Exception("expecting tmp_path") + r = random.randint(0,99999) + + # invoke command using a new connection over sudo + result_file = os.path.join(tmp_path, "sudo_result.%s" % r) self.ssh.close() ssh_sudo = self._get_conn() sudo_chan = ssh_sudo.invoke_shell() sudo_chan.send("sudo -s\n") - sudo_chan.send("echo 'START==>';%s;echo '<==STOP'\n" % cmd) - timeout = 60 # make configurable? + + # to avoid ssh expect logic, redirect output to file and move the + # file when we are done with it... + sudo_chan.send("(%s >%s_pre 2>/dev/null ; mv %s_pre %s) &\n" % (cmd, result_file, result_file, result_file)) time.sleep(1) - while not sudo_chan.recv_ready(): - time.sleep(1) - timeout -= 1 - if timeout < 0: - return (None, json.dumps(dict(failed=True, msg="sudo timeout")), '') - out = sudo_chan.recv(2058) sudo_chan.close() self.ssh = self._get_conn() - out = self._expect_like(out) - return (None, out, '') - - def _expect_like(self, msg): - ''' hack to make invoke_shell more or less work for sudo usage ''' - left = msg.rindex("START==>") - right = msg.rindex("<==STOP") - return msg[left+8:right].lstrip().rstrip() + + # now load the results of the JSON execution... + # FIXME: really need some timeout logic here + sftp = self.ssh.open_sftp() + while True: + # print "waiting on %s" % result_file + time.sleep(1) + try: + stat = 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): ''' transfer a file from local to remote ''' diff --git a/lib/ansible/playbook.py b/lib/ansible/playbook.py index ca2cf09982a..4bd23528392 100755 --- a/lib/ansible/playbook.py +++ b/lib/ansible/playbook.py @@ -255,7 +255,7 @@ class PlayBook(object): # ***************************************************** def _run_module(self, pattern, host_list, module, args, remote_user, - async_seconds, async_poll_interval, only_if): + async_seconds, async_poll_interval, only_if, sudo): ''' run a particular module step in a playbook ''' hosts = [ h for h in host_list if (h not in self.stats.failures) and (h not in self.stats.dark)] @@ -268,6 +268,7 @@ class PlayBook(object): remote_port=self.remote_port, setup_cache=SETUP_CACHE, basedir=self.basedir, conditional=only_if, callbacks=self.runner_callbacks, + sudo=sudo ) if async_seconds == 0: @@ -278,7 +279,7 @@ class PlayBook(object): # ***************************************************** def _run_task(self, pattern=None, host_list=None, task=None, - remote_user=None, handlers=None, conditional=False): + remote_user=None, handlers=None, conditional=False, sudo=False): ''' run a single task in the playbook and recursively run any subtasks. ''' # load the module name and parameters from the task entry @@ -307,7 +308,7 @@ class PlayBook(object): # run the task in parallel results = self._run_module(pattern, host_list, module_name, module_args, remote_user, async_seconds, - async_poll_interval, only_if) + async_poll_interval, only_if, sudo) self.stats.compute(results) @@ -402,7 +403,7 @@ class PlayBook(object): # ***************************************************** - def _do_setup_step(self, pattern, vars, user, port, vars_files=None): + def _do_setup_step(self, pattern, vars, user, port, sudo, vars_files=None): ''' push variables down to the systems and get variables+facts back up ''' # this enables conditional includes like $facter_os.yml and is only done @@ -432,7 +433,7 @@ class PlayBook(object): timeout=self.timeout, remote_user=user, remote_pass=self.remote_pass, remote_port=self.remote_port, setup_cache=SETUP_CACHE, - callbacks=self.runner_callbacks, + callbacks=self.runner_callbacks, sudo=sudo, ).run() self.stats.compute(setup_results, setup=True) @@ -464,15 +465,16 @@ class PlayBook(object): handlers = pg.get('handlers', []) user = pg.get('user', C.DEFAULT_REMOTE_USER) port = pg.get('port', C.DEFAULT_REMOTE_PORT) + sudo = pg.get('sudo', False) self.callbacks.on_play_start(pattern) # push any variables down to the system # and get facts/ohai/other data back up - self._do_setup_step(pattern, vars, user, port, None) + self._do_setup_step(pattern, vars, user, port, sudo, None) # now with that data, handle contentional variable file imports! if len(vars_files) > 0: - self._do_setup_step(pattern, vars, user, port, vars_files) + self._do_setup_step(pattern, vars, user, port, sudo, vars_files) # run all the top level tasks, these get run on every node for task in tasks: @@ -482,6 +484,7 @@ class PlayBook(object): task=task, handlers=handlers, remote_user=user, + sudo=sudo ) # handlers only run on certain nodes, they are flagged by _flag_handlers @@ -499,7 +502,8 @@ class PlayBook(object): handlers=[], host_list=triggered_by, conditional=True, - remote_user=user + remote_user=user, + sudo=sudo ) # end of execution for this particular pattern. Multiple patterns diff --git a/lib/ansible/runner.py b/lib/ansible/runner.py index 492838882ec..569de4595a3 100755 --- a/lib/ansible/runner.py +++ b/lib/ansible/runner.py @@ -241,7 +241,7 @@ class Runner(object): for filename in files: if not filename.startswith('/tmp/'): raise Exception("not going to happen") - self._exec_command(conn, "rm -rf %s" % filename) + self._exec_command(conn, "rm -rf %s" % filename, None) # ***************************************************** @@ -249,7 +249,7 @@ class Runner(object): ''' transfers a module file to the remote side to execute it, but does not execute it yet ''' outpath = self._copy_module(conn, tmp, module) - self._exec_command(conn, "chmod +x %s" % outpath) + self._exec_command(conn, "chmod +x %s" % outpath, tmp) return outpath # ***************************************************** @@ -313,7 +313,7 @@ class Runner(object): if self.remote_user == 'root': args = "%s metadata=/etc/ansible/setup" % args else: - args = "%s metadata=~/.ansible/setup" % args + args = "%s metadata=/home/%s/.ansible/setup" % (args, self.remote_user) return args # ***************************************************** @@ -356,7 +356,7 @@ class Runner(object): cmd = "%s %s" % (remote_module_path, argsfile) else: cmd = " ".join([str(x) for x in [remote_module_path, async_jid, async_limit, async_module, argsfile]]) - return [ self._exec_command(conn, cmd, sudoable=True), client_executed_str ] + return [ self._exec_command(conn, cmd, tmp, sudoable=True), client_executed_str ] # ***************************************************** @@ -555,14 +555,14 @@ class Runner(object): # ***************************************************** - def _exec_command(self, conn, cmd, sudoable=False): + def _exec_command(self, conn, cmd, tmp, sudoable=False): ''' execute a command string over SSH, return the output ''' msg = '%s: %s' % (self.module_name, cmd) # log remote command execution - conn.exec_command('/usr/bin/logger -t ansible -p auth.info "%s"' % msg) + conn.exec_command('/usr/bin/logger -t ansible -p auth.info "%s"' % msg, None) # now run actual command - stdin, stdout, stderr = conn.exec_command(cmd, sudoable=sudoable) + stdin, stdout, stderr = conn.exec_command(cmd, tmp, sudoable=sudoable) if type(stdout) != str: return "\n".join(stdout.readlines()) else: @@ -573,7 +573,7 @@ class Runner(object): def _get_tmp_path(self, conn): ''' gets a temporary path on a remote box ''' - result = self._exec_command(conn, "mktemp -d /tmp/ansible.XXXXXX", sudoable=False) + result = self._exec_command(conn, "mktemp -d /tmp/ansible.XXXXXX", None, sudoable=False) cleaned = result.split("\n")[0].strip() + '/' return cleaned