From 8d57ceecf1d28c06b498512bf65155257dfd4e84 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 5 Mar 2012 20:09:03 -0500 Subject: [PATCH] Factoids and push variables via setup are now available to be templated in command args as well as template files. PLUS, variables are now expressed in playbooks without having to know about the setup task, which means playbooks are simpler to read now. --- README.md | 4 ++++ ansible.spec | 1 + examples/playbook.yml | 9 ++++++--- examples/playbook2.yml | 6 ++++-- lib/ansible/playbook.py | 35 +++++++++++++++++++++++++++++++++-- lib/ansible/runner.py | 34 ++++++++++++++++++++++++++++------ library/setup | 11 +++++------ 7 files changed, 81 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a6410300abc..db3bfa29900 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Requirements are extremely minimal. If you are running python 2.6 on the 'overlord' machine, you will need: * paramiko + * python-jinja2 * PyYAML (if using playbooks) If you are running less than Python 2.6, you will also need @@ -161,6 +162,9 @@ An example showing a small playbook: --- - hosts: all + vars: + http_port: 80 + max_clients: 200 user: root tasks: - include: base.yml diff --git a/ansible.spec b/ansible.spec index c9aa6516209..b8d36012c5b 100644 --- a/ansible.spec +++ b/ansible.spec @@ -13,6 +13,7 @@ BuildArch: noarch Url: http://github.com/mpdehaan/ansible/ BuildRequires: asciidoc Requires: python-paramiko +Requires: python-jinja2 %description Ansible is a extra-simple tool/API for doing 'parallel remote things' over SSH diff --git a/examples/playbook.yml b/examples/playbook.yml index 4cda6dde3d6..7890c35b06e 100644 --- a/examples/playbook.yml +++ b/examples/playbook.yml @@ -1,15 +1,18 @@ --- - hosts: all user: root + vars: + http_port: 80 + max_clients: 200 tasks: - include: base.yml - - name: configure template & module variables for future template calls - action: setup http_port=80 max_clients=200 - - name: write the apache config file + - name: write the apache config file using vars set above action: template src=/srv/httpd.j2 dest=/etc/httpd.conf notify: - restart apache - name: ensure apache is running action: service name=httpd state=started + - name: pointless test action + action: command /bin/echo {{ http_port }} handlers: - include: handlers.yml diff --git a/examples/playbook2.yml b/examples/playbook2.yml index 342a9e79cad..494d1089a13 100644 --- a/examples/playbook2.yml +++ b/examples/playbook2.yml @@ -1,8 +1,10 @@ --- - hosts: '*' + vars: + a: 2 + b: 3 + c: 4 tasks: - - name: config step - action: setup a=2 b=3 c=4 - name: copy comand action: copy src=/srv/a dest=/srv/b notify: diff --git a/lib/ansible/playbook.py b/lib/ansible/playbook.py index e89ee67cf16..522078abeee 100755 --- a/lib/ansible/playbook.py +++ b/lib/ansible/playbook.py @@ -24,6 +24,8 @@ import yaml import shlex import os +SETUP_CACHE={ 'foo' : {} } + ############################################# class PlayBook(object): @@ -150,7 +152,8 @@ class PlayBook(object): remote_pass=self.remote_pass, module_path=self.module_path, timeout=self.timeout, - remote_user=remote_user + remote_user=remote_user, + setup_cache=SETUP_CACHE ).run() def _run_task(self, pattern=None, task=None, host_list=None, @@ -274,6 +277,7 @@ class PlayBook(object): # get configuration information about the pattern pattern = pg['hosts'] + vars = pg.get('vars', {}) tasks = pg['tasks'] handlers = pg['handlers'] user = pg.get('user', C.DEFAULT_REMOTE_USER) @@ -283,8 +287,35 @@ class PlayBook(object): if self.verbose: print "PLAY [%s] ****************************\n" % pattern - # run all the top level tasks, these get run on every node + # 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.items(): + push_var_str += "%s=%s " % (k,v) + if user != 'root': + push_var_str = "%s metadata=~/.ansible_setup" % (push_var_str) + + # push any variables down to the system + setup_results = ansible.runner.Runner( + pattern=pattern, + module_name='setup', + module_args=push_var_str, + host_list=self.host_list, + forks=self.forks, + module_path=self.module_path, + timeout=self.timeout, + remote_user=user, + setup_cache=SETUP_CACHE + ).run() + # now for each result, load into the setup cache so we can + # let runner template out future commands + setup_ok = setup_results.get('contacted', {}) + for (host, result) in setup_ok.items(): + SETUP_CACHE[host] = result + + # run all the top level tasks, these get run on every node for task in tasks: self._run_task( pattern=pattern, diff --git a/lib/ansible/runner.py b/lib/ansible/runner.py index 59b0491b34e..a0370e8a393 100755 --- a/lib/ansible/runner.py +++ b/lib/ansible/runner.py @@ -29,9 +29,10 @@ import signal import os import ansible.constants as C import Queue -import paramiko import random - +import paramiko +import jinja2 + ################################################ def noop(*args, **kwargs): @@ -62,6 +63,7 @@ class Runner(object): remote_user=C.DEFAULT_REMOTE_USER, remote_pass=C.DEFAULT_REMOTE_PASS, background=0, + setup_cache={}, verbose=False): ''' @@ -75,9 +77,10 @@ class Runner(object): remote_user -- who to login as (default root) remote_pass -- provide only if you don't want to use keys or ssh-agent background --- if non 0, run async, failing after X seconds, -1 == infinite + setup_cache -- used only by playbook (complex explanation pending) ''' - # save input values + self.setup_cache = setup_cache self.host_list, self.groups = self.parse_hosts(host_list) self.module_path = module_path @@ -91,6 +94,7 @@ class Runner(object): self.remote_pass = remote_pass self.background = background + # hosts in each group name in the inventory file self._tmp_paths = {} @@ -201,10 +205,17 @@ class Runner(object): def _execute_module(self, conn, tmp, remote_module_path, module_args): ''' - runs a module that has already been transferred + runs a module that has already been transferred, but first + modifies the command using setup_cache variables (see playbook) ''' - args = [ str(x) for x in module_args ] - args = " ".join(args) + args = module_args + if type(args) == list: + args = [ str(x) for x in module_args ] + args = " ".join(args) + inject_vars = self.setup_cache.get(conn._host,{}) + template = jinja2.Template(args) + args = template.render(inject_vars) + cmd = "%s %s" % (remote_module_path, args) result = self._exec_command(conn, cmd) self._delete_remote_files(conn, [ tmp ]) @@ -217,6 +228,16 @@ class Runner(object): ''' module = self._transfer_module(conn, tmp, self.module_name) result = self._execute_module(conn, tmp, module, self.module_args) + # when running the setup module, which pushes vars to the host and ALSO + # returns them (+factoids), store the variables that were returned such that commands + # run AFTER setup use these variables for templating when executed + # from playbooks + if self.module_name == 'setup': + host = conn._host + try: + var_result = json.loads(result) + except: + var_result = {} self._delete_remote_files(conn, tmp) return self._return_from_module(conn, host, result) @@ -303,6 +324,7 @@ class Runner(object): # module, call the appropriate executor function ok, conn = self._connect(host) + conn._host = host if not ok: return [ host, False, conn ] diff --git a/library/setup b/library/setup index c5de2e9f221..30e16032381 100755 --- a/library/setup +++ b/library/setup @@ -96,10 +96,9 @@ md5sum2 = os.popen("md5sum %s" % ansible_file).read().split()[0] if md5sum != md5sum2: changed = True -result = { - "written" : ansible_file, - "changed" : changed, - "md5sum" : md5sum2 -} +new_options['written'] = ansible_file +new_options['changed'] = changed +new_options['md5sum'] = md5sum2 + +print json.dumps(new_options) -print json.dumps(result)