From 1d75a29ec92715a059fcf62bf2d8963d16775185 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Tue, 10 Apr 2012 20:58:40 -0400 Subject: [PATCH] Allow variables coming in from the playbook and the API to be expressed as dictionaries throughout their full life cycle such that nested data can be made available in templates and playbooks. --- lib/ansible/playbook.py | 11 ++-------- lib/ansible/runner.py | 48 +++++++++++++++++++++++++++++++---------- library/setup | 33 ++++++++++++++++------------ test/playbook1.events | 19 ++++++++++++++-- 4 files changed, 75 insertions(+), 36 deletions(-) diff --git a/lib/ansible/playbook.py b/lib/ansible/playbook.py index 0eb0e4d685..c75d27213f 100755 --- a/lib/ansible/playbook.py +++ b/lib/ansible/playbook.py @@ -436,24 +436,17 @@ class PlayBook(object): else: self.callbacks.on_setup_primary() - # first run the setup task on every node, which gets the variables - # written to the JSON file and will also bubble facts back up via - # magic in Runner() - push_var_str='' - for (k,v) in vars.iteritems(): - push_var_str += "%s=\"%s\" " % (k,v) - host_list = [ h for h in self.host_list if not (h in self.stats.failures or h in self.stats.dark) ] # push any variables down to the system setup_results = ansible.runner.Runner( pattern=pattern, groups=self.groups, module_name='setup', - module_args=push_var_str, host_list=host_list, + module_args=vars, host_list=host_list, forks=self.forks, module_path=self.module_path, timeout=self.timeout, remote_user=user, remote_pass=self.remote_pass, remote_port=self.remote_port, setup_cache=SETUP_CACHE, - callbacks=self.runner_callbacks, sudo=sudo, + callbacks=self.runner_callbacks, sudo=sudo, debug=self.debug, ).run() self.stats.compute(setup_results, setup=True) diff --git a/lib/ansible/runner.py b/lib/ansible/runner.py index b2cf3518b9..ad7574452b 100755 --- a/lib/ansible/runner.py +++ b/lib/ansible/runner.py @@ -113,8 +113,8 @@ class Runner(object): self.basedir = basedir self.sudo = sudo - if type(self.module_args) != str: - raise Exception("module_args must be a string: %s" % self.module_args) + if type(self.module_args) != str and type(self.module_args) != dict: + raise Exception("module_args must be a string or dict: %s" % self.module_args) self._tmp_paths = {} random.seed() @@ -271,6 +271,9 @@ class Runner(object): def _transfer_str(self, conn, tmp, name, args_str): ''' transfer arguments as a single file to be fed to the module. ''' + if type(args_str) == dict: + args_str = utils.smjson(args_str) + args_fd, args_file = tempfile.mkstemp() args_fo = os.fdopen(args_fd, 'w') args_fo.write(args_str) @@ -316,23 +319,43 @@ class Runner(object): def _add_setup_vars(self, inject, args): ''' setup module variables need special handling ''' + is_dict = False + if type(args) == dict: + is_dict = True + + # TODO: keep this as a dict through the whole path to simplify this code for (k,v) in inject.iteritems(): if not k.startswith('facter_') and not k.startswith('ohai_'): - if str(v).find(" ") != -1: - v = "\"%s\"" % v - args += " %s=%s" % (k, str(v).replace(" ","~~~")) + if not is_dict: + if str(v).find(" ") != -1: + v = "\"%s\"" % v + args += " %s=%s" % (k, str(v).replace(" ","~~~")) + else: + args[k]=v return args # ***************************************************** def _add_setup_metadata(self, args): ''' automatically determine where to store variables for the setup module ''' + + is_dict = False + if type(args) == dict: + is_dict = True - if args.find("metadata=") == -1: - if self.remote_user == 'root': - args = "%s metadata=/etc/ansible/setup" % args - else: - args = "%s metadata=/home/%s/.ansible/setup" % (args, self.remote_user) + # TODO: keep this as a dict through the whole path to simplify this code + if not is_dict: + if args.find("metadata=") == -1: + if self.remote_user == 'root': + args = "%s metadata=/etc/ansible/setup" % args + else: + args = "%s metadata=/home/%s/.ansible/setup" % (args, self.remote_user) + else: + if not 'metadata' in args: + if self.remote_user == 'root': + args['metadata'] = '/etc/ansible/setup' + else: + args['metadata'] = "/home/%s/.ansible/setup" % (self.remote_user) return args # ***************************************************** @@ -352,9 +375,11 @@ class Runner(object): args = self._add_setup_vars(inject, args) args = self._add_setup_metadata(args) + if type(args) == dict: + args = utils.bigjson(args) args = utils.template(args, inject) + module_name_tail = remote_module_path.split("/")[-1] - client_executed_str = "%s %s" % (module_name_tail, args.strip()) argsfile = self._transfer_str(conn, tmp, 'arguments', args) if async_jid is None: @@ -368,6 +393,7 @@ class Runner(object): res, err = self._exec_command(conn, cmd, tmp, sudoable=True) + client_executed_str = "%s %s" % (module_name_tail, args.strip()) return ( res, err, client_executed_str ) # ***************************************************** diff --git a/library/setup b/library/setup index a2b90b7400..6efb5e973c 100755 --- a/library/setup +++ b/library/setup @@ -23,6 +23,7 @@ import sys import os import shlex import subprocess +import traceback try: import json @@ -34,18 +35,22 @@ except ImportError: if len(sys.argv) == 1: sys.exit(1) + argfile = sys.argv[1] if not os.path.exists(argfile): sys.exit(1) -input_data = shlex.split(open(argfile, 'r').read()) +setup_options = open(argfile).read().strip() +try: + setup_options = json.loads(setup_options) +except: + list_options = shlex.split(setup_options) + setup_options = {} + for opt in list_options: + (k,v) = opt.split("=") + setup_options[k]=v -# turn urlencoded k=v string (space delimited) to regular k=v directionary -splitted = [x.split('=',1) for x in input_data ] -splitted = [ (x[0], x[1].replace("~~~"," ")) for x in splitted ] -new_options = dict(splitted) - -ansible_file = new_options.get('metadata', DEFAULT_ANSIBLE_SETUP) +ansible_file = setup_options.get('metadata', DEFAULT_ANSIBLE_SETUP) ansible_dir = os.path.dirname(ansible_file) # create the config dir if it doesn't exist @@ -74,7 +79,7 @@ if os.path.exists("/usr/bin/facter"): facter = False if facter: for (k,v) in facter_ds.items(): - new_options["facter_%s" % k] = v + setup_options["facter_%s" % k] = v # ditto for ohai, but just top level string keys # because it contains a lot of nested stuff we can't use for @@ -93,13 +98,13 @@ if os.path.exists("/usr/bin/ohai"): for (k,v) in ohai_ds.items(): if type(v) == str or type(v) == unicode: k2 = "ohai_%s" % k - new_options[k2] = v + setup_options[k2] = v # write the template/settings file using # instructions from server f = open(ansible_file, "w+") -reformat = json.dumps(new_options, sort_keys=True, indent=4) +reformat = json.dumps(setup_options, sort_keys=True, indent=4) f.write(reformat) f.close() @@ -108,9 +113,9 @@ md5sum2 = os.popen("md5sum %s" % ansible_file).read().split()[0] if md5sum != md5sum2: changed = True -new_options['written'] = ansible_file -new_options['changed'] = changed -new_options['md5sum'] = md5sum2 +setup_options['written'] = ansible_file +setup_options['changed'] = changed +setup_options['md5sum'] = md5sum2 -print json.dumps(new_options) +print json.dumps(setup_options) diff --git a/test/playbook1.events b/test/playbook1.events index b8ab78e634..e5e9cd489b 100644 --- a/test/playbook1.events +++ b/test/playbook1.events @@ -15,7 +15,7 @@ "answer": "Wuh, I think so, Brain, but if we didn't have ears, we'd look like weasels.", "changed": true, "metadata": "/etc/ansible/setup", - "port": "5150", + "port": 5150, "written": "/etc/ansible/setup" } ] @@ -44,7 +44,7 @@ "cow": "moo", "duck": "quack", "metadata": "/etc/ansible/setup", - "port": "5150", + "port": 5150, "testing": "default", "written": "/etc/ansible/setup" } @@ -228,6 +228,21 @@ "127.0.0.2" ] ], + [ + "ok", + [ + "127.0.0.2", + { + "started": 1 + } + ] + ], + [ + "async poll", + [ + "127.0.0.2" + ] + ], [ "ok", [