From fc4ba46d1a8a36b5aacaab064a2aad303279d7ba Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sun, 26 Feb 2012 20:29:27 -0500 Subject: [PATCH] Add a ton of comments so folks can understand what runner does. More refactoring is certaintly possible too. --- lib/ansible/runner.py | 86 +++++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/lib/ansible/runner.py b/lib/ansible/runner.py index 526e7433278..8361e4970ec 100755 --- a/lib/ansible/runner.py +++ b/lib/ansible/runner.py @@ -58,6 +58,7 @@ class Runner(object): remote_pass -- provide only if you don't want to use keys or ssh-agent ''' + # save input values self.host_list = self._parse_hosts(host_list) self.module_path = module_path self.module_name = module_name @@ -71,18 +72,23 @@ class Runner(object): def _parse_hosts(self, host_list): ''' parse the host inventory file if not sent as an array ''' + + # if the host list is given as a string load the host list + # from a file, one host per line if type(host_list) != list: host_list = os.path.expanduser(host_list) return file(host_list).read().split("\n") + return host_list def _matches(self, host_name, pattern=None): ''' returns if a hostname is matched by the pattern ''' + # a pattern is in fnmatch format but more than one pattern + # can be strung together with semicolons. ex: + # atlanta-web*.example.com;dc-web*.example.com if host_name == '': return False - if not pattern: - pattern = self.pattern subpatterns = pattern.split(";") for subpattern in subpatterns: if fnmatch.fnmatch(host_name, subpattern): @@ -98,48 +104,66 @@ class Runner(object): ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: + # try paramiko ssh.connect(host, username=self.remote_user, allow_agent=True, look_for_keys=True, password=self.remote_pass) return [ True, ssh ] except: + # it failed somehow, return the failure string return [ False, traceback.format_exc() ] def _return_from_module(self, conn, host, result): + ''' helper function to handle JSON parsing of results ''' + # disconnect from paramiko/SSH conn.close() try: + # try to parse the JSON response return [ host, True, json.loads(result) ] except: + # it failed, say so, but return the string anyway return [ host, False, result ] def _delete_remote_files(self, conn, files): + ''' deletes one or more remote files ''' for filename in files: self._exec_command(conn, "rm -f %s" % filename) def _transfer_file(self, conn, source, dest): + ''' transfers a remote file ''' self.remote_log(conn, 'COPY remote:%s local:%s' % (source, dest)) - ftp = conn.open_sftp() - ftp.put(source, dest) - ftp.close() + sftp = conn.open_sftp() + sftp.put(source, dest) + sftp.close() def _transfer_module(self, conn): + ''' + transfers a module file to the remote side to execute it, + but does not execute it yet + ''' outpath = self._copy_module(conn) self._exec_command(conn, "chmod +x %s" % outpath) return outpath def _execute_module(self, conn, outpath): + ''' + runs a module that has already been transferred + ''' cmd = self._command(outpath) result = self._exec_command(conn, cmd) self._delete_remote_files(conn, [ outpath ]) return result def _execute_normal_module(self, conn, host): - ''' transfer a module, set it executable, and run it ''' - + ''' + transfer & execute a module that is not 'copy' or 'template' + because those require extra work. + ''' module = self._transfer_module(conn) result = self._execute_module(conn, module) return self._return_from_module(conn, host, result) def _parse_kv(self, args): + ''' helper function to convert a string of key/value items to a dict ''' options = {} for x in args: if x.find("=") != -1: @@ -150,10 +174,12 @@ class Runner(object): def _execute_copy(self, conn, host): ''' handler for file transfer operations ''' - # transfer the file to a remote tmp location + # load up options options = self._parse_kv(self.module_args) source = options['src'] dest = options['dest'] + + # transfer the file to a remote tmp location tmp_src = self._get_tmp_path(conn, dest.split("/")[-1]) self._transfer_file(conn, source, tmp_src) @@ -170,6 +196,7 @@ class Runner(object): def _execute_template(self, conn, host): ''' handler for template operations ''' + # load up options options = self._parse_kv(self.module_args) source = options['src'] dest = options['dest'] @@ -199,6 +226,10 @@ class Runner(object): or a traceback string ''' + # depending on whether it's a normal module, + # or a request to use the copy or template + # module, call the appropriate executor function + ok, conn = self._connect(host) if not ok: return [ host, False, conn ] @@ -209,19 +240,23 @@ class Runner(object): elif self.module_name == 'template': return self._execute_template(conn, host) else: + # this would be a coding error in THIS module + # shouldn't occur raise Exception("???") def _command(self, outpath): - ''' form up a command string ''' + ''' form up a command string for running over SSH ''' cmd = "%s %s" % (outpath, " ".join(self.module_args)) return cmd - def remote_log(self, conn, msg): + ''' this is the function we use to log things ''' stdin, stdout, stderr = conn.exec_command('/usr/bin/logger -t ansible -p auth.info %r' % msg) + # TODO: doesn't actually call logger on the remote box, should though + # TODO: maybe make that optional def _exec_command(self, conn, cmd): - ''' execute a command over SSH ''' + ''' execute a command string over SSH, return the output ''' msg = '%s: %s' % (self.module_name, cmd) self.remote_log(conn, msg) stdin, stdout, stderr = conn.exec_command(cmd) @@ -229,11 +264,12 @@ class Runner(object): return results def _get_tmp_path(self, conn, file_name): + ''' gets a temporary path on a remote box ''' output = self._exec_command(conn, "mktemp /tmp/%s.XXXXXX" % file_name) return output.split("\n")[0] def _copy_module(self, conn): - ''' transfer a module over SFTP ''' + ''' transfer a module over SFTP, does not run it ''' in_path = os.path.expanduser( os.path.join(self.module_path, self.module_name) ) @@ -244,17 +280,18 @@ class Runner(object): sftp.close() return out_path - def match_hosts(self, pattern=None): - ''' return all matched hosts ''' + def match_hosts(self, pattern): + ''' return all matched hosts fitting a pattern ''' return [ h for h in self.host_list if self._matches(h, pattern) ] def run(self): ''' xfer & run module on all matched hosts ''' # find hosts that match the pattern - hosts = self.match_hosts() + hosts = self.match_hosts(self.pattern) # attack pool of hosts in N forks + # _executor_hook does all of the work hosts = [ (self,x) for x in hosts ] if self.forks > 1: pool = multiprocessing.Pool(self.forks) @@ -263,7 +300,9 @@ class Runner(object): results = [ _executor_hook(x) for x in hosts ] # sort hosts by ones we successfully contacted - # and ones we did not + # and ones we did not so that we can return a + # dictionary containing results of everything + results2 = { "contacted" : {}, "dark" : {} @@ -278,18 +317,3 @@ class Runner(object): return results2 -if __name__ == '__main__': - - # test code... - - r = Runner( - host_list = DEFAULT_HOST_LIST, - module_name='ping', - module_args='', - pattern='*', - forks=3 - ) - print r.run() - - -