diff --git a/lib/ansible/playbook.py b/lib/ansible/playbook.py index df3f8aa987..2eb8297e5c 100755 --- a/lib/ansible/playbook.py +++ b/lib/ansible/playbook.py @@ -77,6 +77,7 @@ class PlayBook(object): self.changed = {} self.invocations = {} self.failures = {} + self.skipped = {} # playbook file can be passed in as a path or # as file contents (to support API usage) @@ -158,8 +159,13 @@ class PlayBook(object): 'resources' : self.invocations.get(host, 0), 'changed' : self.changed.get(host, 0), 'dark' : self.dark.get(host, 0), - 'failed' : self.failures.get(host, 0) + 'failed' : self.failures.get(host, 0), + 'skipped' : self.skipped.get(host, 0) } + + # FIXME: TODO: use callback to reinstate per-host summary + # and add corresponding code in /bin/ansible-playbook + # print results return results def _prune_failed_hosts(self, host_list): @@ -175,12 +181,12 @@ class PlayBook(object): for (host, res) in results['contacted'].iteritems(): # FIXME: make polling pattern in /bin/ansible match # move to common function in utils - if not 'finished' in res and 'started' in res: + if not 'finished' in res and not 'skipped' in res and 'started' in res: hosts.append(host) return hosts - def _async_poll(self, runner, async_seconds, async_poll_interval): + def _async_poll(self, runner, async_seconds, async_poll_interval, only_if): ''' launch an async job, if poll_interval is set, wait for completion ''' # TODO: refactor this function @@ -197,6 +203,8 @@ class PlayBook(object): if 'failed' in host_result: self.callbacks.on_failed(host, host_result) self.failures[host] = 1 + if 'skipped' in host_result: + self.skipped[host] = self.skipped.get(host, 0) + 1 if async_poll_interval <= 0: # if not polling, playbook requested fire and forget @@ -248,6 +256,11 @@ class PlayBook(object): if 'failed' in host_result: self.callbacks.on_failed(host, host_result) self.failures[host] = 1 + if 'skipped' in host_result: + # NOTE: callbacks on skipped? should not really + # happen at this point in the loop + self.skipped[host] = self.skipped.get(host, 0) + 1 + for (host, host_result) in poll_results['contacted'].iteritems(): results['contacted'][host] = host_result @@ -267,7 +280,7 @@ class PlayBook(object): return results def _run_module(self, pattern, module, args, hosts, remote_user, - async_seconds, async_poll_interval): + async_seconds, async_poll_interval, only_if): ''' run a particular module step in a playbook ''' runner = ansible.runner.Runner( @@ -281,13 +294,14 @@ class PlayBook(object): timeout=self.timeout, remote_user=remote_user, setup_cache=SETUP_CACHE, - basedir=self.basedir + basedir=self.basedir, + conditionally_execute_if=only_if ) if async_seconds == 0: rc = runner.run() else: - rc = self._async_poll(runner, async_seconds, async_poll_interval) + rc = self._async_poll(runner, async_seconds, async_poll_interval, only_if) dark_hosts = rc.get('dark',{}) for (host, error) in dark_hosts.iteritems(): @@ -314,13 +328,12 @@ class PlayBook(object): host_list = self._prune_failed_hosts(host_list) # load the module name and parameters from the task entry - name = task['name'] - action = task['action'] + name = task['name'] # FIXME: error if not set + action = task['action'] # FIXME: error if not set + only_if = task.get('only_if', 'True') async_seconds = int(task.get('async', 0)) # not async by default async_poll_interval = int(task.get('poll', 10)) # default poll = 10 seconds - # comment = task.get('comment', '') - tokens = shlex.split(action) module_name = tokens[0] module_args = tokens[1:] @@ -336,7 +349,7 @@ class PlayBook(object): # run the task in parallel results = self._run_module(pattern, module_name, module_args, host_list, remote_user, - async_seconds, async_poll_interval) + async_seconds, async_poll_interval, only_if) # if no hosts are matched, carry on, unlike /bin/ansible # which would warn you about this @@ -357,7 +370,8 @@ class PlayBook(object): self.dark[host] = 1 else: self.dark[host] = self.dark[host] + 1 - + + # FIXME: refactor for host, results in contacted.iteritems(): self.processed[host] = 1 @@ -378,6 +392,12 @@ class PlayBook(object): self.changed[host] = 1 else: self.changed[host] = self.changed[host] + 1 + # TODO: verify/test that async steps are skippable + if results.get('skipped', False): + if not host in self.changed: + self.skipped[host] = 1 + else: + self.skipped[host] = self.skipped[host] + 1 # flag which notify handlers need to be run # this will be on a SUBSET of the actual host list. For instance diff --git a/lib/ansible/runner.py b/lib/ansible/runner.py index 50250816db..45bab33a78 100755 --- a/lib/ansible/runner.py +++ b/lib/ansible/runner.py @@ -76,6 +76,7 @@ class Runner(object): basedir=None, setup_cache=None, transport='paramiko', + conditionally_execute_if='True', verbose=False): ''' @@ -95,6 +96,7 @@ class Runner(object): if setup_cache is None: setup_cache = {} self.setup_cache = setup_cache + self.conditionally_execute_if = conditionally_execute_if self.host_list, self.groups = self.parse_hosts(host_list) self.module_path = module_path @@ -286,12 +288,18 @@ class Runner(object): modifies the command using setup_cache variables (see playbook) ''' + args = module_args if type(args) == list: args = " ".join([ str(x) for x in module_args ]) # by default the args to substitute in the action line are those from the setup cache inject_vars = self.setup_cache.get(conn.host,{}) + + # see if we really need to run this or not... + conditional = utils.template(self.conditionally_execute_if, inject_vars) + if not eval(conditional): + return utils.smjson(dict(skipped=True)) # if the host file was an external script, execute it with the hostname # as a first parameter to get the variables to use for the host diff --git a/test/playbook1.events b/test/playbook1.events index fc2f73cde9..b5c9c11cce 100644 --- a/test/playbook1.events +++ b/test/playbook1.events @@ -235,7 +235,8 @@ "changed": 2, "dark": 0, "failed": 0, - "resources": 9 + "resources": 9, + "skipped": 0 } } }