Repository: ansible/ansible

Issue:
New "quit if playbook doesn't match" hosts breaks provisioning
I use ansible to provision new EC2 instances on boot, and a2f76c1c696288f4e6741829da4be6dba21b1658 breaks provisioning because it doesn't skip playbooks that don't apply, it just quits.



Discussion:
Help me understand here.

If you had a play that completely failed for all hosts, or didn't select
any hosts, that seems like a good case to stop the playbook run to me.

(If you have steps that you know will fail and you are ok with this, this
is what ignore_errors has been for)

On Mon, Oct 1, 2012 at 4:25 PM, Jonathan Rudenberg <notifications@github.com

> wrote:
> 
> I use ansible to provision new EC2 instances on boot, and a2f76c1https://github.com/ansible/ansible/commit/a2f76c1c696288f4e6741829da4be6dba21b1658breaks provisioning because it doesn't skip playbooks that don't apply, it
> just quits.
> 
> —
> Reply to this email directly or view it on GitHubhttps://github.com/ansible/ansible/issues/1187.

Maybe I'm doing this wrong, but I have a playbook that includes all the setup playbooks for each node type, and then I just use EC2 tags as the host groups to pick which playbooks apply. So the provisioning script runs ansible-playbook with the "setup" playbook, and only the playbooks that apply to that node type end up getting run, the others are skipped.

Understood.  and you are right.

I'll check and make sure this doesn't happen unless there are are actually
nodes messaged as part of the play tonight.

If it doesn't select any nodes, it should abort only the play, not the
whole playbook.

Thanks for the report!

On Mon, Oct 1, 2012 at 4:30 PM, Jonathan Rudenberg <notifications@github.com

> wrote:
> 
> Maybe I'm doing this wrong, but I have a playbook that includes all the
> setup playbooks for each node type, and then I just use EC2 tags as the
> host groups to pick which playbooks apply. So the provisioning script runs
> ansible-playbook with the "setup" playbook, and only the playbooks that
> apply to that node type end up getting run, the others are skipped.
> 
> —
> Reply to this email directly or view it on GitHubhttps://github.com/ansible/ansible/issues/1187#issuecomment-9048575.

Ok, so I have tested this with a couple of host groups and it will only do the "Hard Stop" if the failure is after the setup step, which should fix this for you.

Please update to get this commit -- 5683277 -- and let me know if you're fixed up.

I am pretty sure you should be.   Thanks!

Thanks, I'll test this out tomorrow.

Thinking, it may not work correctly if gather_facts is disabled, the way I am counting tasks, I'll make a quick change if so! 

nm, think I'm wrong.

Thanks!

For me this is still and issue. If I have a set of plays, and one of the plays has 'hosts: webservers', while I am running the playbook on systems not in webservers, the task aborts the whole playbook in all cases. So it means I can no longer have plays in a playbook that excludes systems I run the playbook for.

I think we need to take into account the number of skipped hosts, if that's not zero we should not abort the playbook.

Looking at the code, I fail to understand how the information about whether there are hosts skipped can be used there where we need this information.

I fail to understand how this is designed, this decision is made inside _run_task, which is not where such a decision ought to be IMO. It should be on a higher level where before running the task itself, it evaluates whether there is a need to do it based on the amount of "non-failed" systems.

That would also eliminate the fact that you print the task when aborting. Because when you print the task and then abort you make the user believe that the cause is this task, but it is not.

On Tue, Oct 2, 2012 at 6:56 AM, Dag Wieers notifications@github.com wrote:

> Looking at the code, I fail to understand how the information about
> whether there are hosts skipped can be used there where we need this
> installation.
> 
> I fail to understand how this is designed, this decision is made inside
> _run_task, which is not where such a decision ought to be IMO. It should be
> on a higher level where before running the task itself, it evaluates
> whether there is a need to do it based on the amount of "non-failed"
> systems.

Let's concentrate on how to reproduce the problem and leave how it's
designed for me to figure out :)

Then here's how to reproduce:

``` yaml
---
- hosts: emptygroup
  tasks:
  - action: command date

- hosts: all
  tasks:
  - action: command true
```

This fails for all systems not in _emptygroup_. While it should only skip the first play, and run the second for each system.

On Tue, Oct 2, 2012 at 8:04 AM, Dag Wieers notifications@github.com wrote:

> Then here's how to reproduce:
> 
> ---- hosts: emptygroup
>   tasks:
> - action: command date
>   - hosts: all
>     tasks:
> - action: command true
> 
> This fails for all systems not in _emptygroup_. While it should only skip
> the first play, and run the second for each system.

That worked fine for me last night, actually.

Have you pulled latest?

I did so two hours ago. Output of pull request was:

```
remote: Counting objects: 140, done.
remote: Compressing objects: 100% (43/43), done.
remote: Total 105 (delta 73), reused 88 (delta 56)
Receiving objects: 100% (105/105), 16.35 KiB, done.
Resolving deltas: 100% (73/73), completed with 33 local objects.
From github.com:ansible/ansible
 * branch            devel      -> FETCH_HEAD
Updating 880328c..f897f19
```

Ok, I'll recheck tonight, though pretty sure this is what I did.   Perhaps testing a non-existant pattern versus an empty group made a difference

@titanous still curious on your feedback

Just did a new pull:

```
[dag@moria ansible]$ git pull upstream devel
From github.com:ansible/ansible
 * branch            devel      -> FETCH_HEAD
Current branch devel is up to date.
```

RPM package that I am using is: **ansible-0.8-0.git201210020441.el6.noarch** which matches with the log:

```
 commit f897f19fc5ac783e764244e066adf58099acf520
 Author: Michael DeHaan <michael.dehaan@gmail.com>
 Date:   Mon Oct 1 22:41:00 2012 -0400

     Teach fireball mode to disable the fireball by paying attention to 'minutes=N' (default 30) and do not let fireball module crash
     on input.
```

(Give or take a couple of timezones)

I have it working for me now, I will send a pull-request for consideration once I have copied my modifications to an Internet-bound system ;-)

Meanwhile I am making sure I have no unpushed code :)

yeah it's current -- interested to see what your fix was and how to cause it.


Resolving patch:
diff --git a/lib/ansible/callback_plugins/noop.py b/lib/ansible/callback_plugins/noop.py
--- a/lib/ansible/callback_plugins/noop.py
+++ b/lib/ansible/callback_plugins/noop.py
@@ -63,6 +63,12 @@ def playbook_on_start(self):
     def playbook_on_notify(self, host, handler):
         pass
 
+    def on_no_hosts_matched(self):
+        pass
+
+    def on_no_hosts_remaining(self):
+        pass
+
     def playbook_on_task_start(self, name, is_conditional):
         pass
 
diff --git a/lib/ansible/callbacks.py b/lib/ansible/callbacks.py
--- a/lib/ansible/callbacks.py
+++ b/lib/ansible/callbacks.py
@@ -414,6 +414,14 @@ def on_start(self):
     def on_notify(self, host, handler):
         call_callback_module('playbook_on_notify', host, handler)
 
+    def on_no_hosts_matched(self):
+        print stringc("no hosts matched", 'red')
+        call_callback_module('playbook_on_no_hosts_matched')
+
+    def on_no_hosts_remaining(self):
+        print stringc("\nFATAL: all hosts have already failed -- aborting", 'red')
+        call_callback_module('playbook_on_no_hosts_remaining')
+
     def on_task_start(self, name, is_conditional):
         msg = "TASK: [%s]" % name
         if is_conditional:
diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py
--- a/lib/ansible/playbook/__init__.py
+++ b/lib/ansible/playbook/__init__.py
@@ -115,9 +115,6 @@ def __init__(self,
         (self.playbook, self.play_basedirs) = self._load_playbook_from_file(playbook)
         self.module_path = self.module_path + os.pathsep + os.path.join(self.basedir, "library")
 
-        # which task we are currently executing
-        self.task_counter     = 0
-
     # *****************************************************
 
     def _load_playbook_from_file(self, path):
@@ -239,8 +236,6 @@ def _run_task_internal(self, task):
                 # if not polling, playbook requested fire and forget, so don't poll
                 results = self._async_poll(poller, task.async_seconds, task.async_poll_interval)
 
-        self.task_counter = self.task_counter + 1
-
         contacted = results.get('contacted',{})
         dark      = results.get('dark', {})
 
@@ -346,6 +341,11 @@ def _run_play(self, play):
 
         self.callbacks.on_play_start(play.name)
 
+        # if no hosts matches this play, drop out
+        if not self.inventory.list_hosts(play.hosts):
+            self.callbacks.on_no_hosts_matched()
+            return True
+
         # get facts from system
         self._do_setup_step(play)
 
@@ -384,7 +384,15 @@ def _run_play(self, play):
                         # whether no hosts matched is fatal or not depends if it was on the initial step.
                         # if we got exactly no hosts on the first step (setup!) then the host group
                         # just didn't match anything and that's ok
-                        return (self.task_counter <= 1)
+                        return False
+
+                host_list = [ h for h in self.inventory.list_hosts(play.hosts)
+                    if not (h in self.stats.failures or h in self.stats.dark) ]
+
+                # if no hosts remain, drop out
+                if not host_list:
+                    self.callbacks.on_no_hosts_remaining()
+                    return False
 
             # run notify actions
             for handler in play.handlers():
@@ -394,5 +402,6 @@ def _run_play(self, play):
                     self.inventory.lift_restriction()
 
             self.inventory.lift_also_restriction()
+
         return True
 


Test patch: