Repository: ansible/ansible

Issue:
Implicit localhost is ignored in all groups, instead of just the 'all' group
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
Inventory, playbook

##### ANSIBLE VERSION
```
ansible 2.3.0.0
```
and
```
ansible 2.4.0 (devel 594d7d5d9d) last updated 2017/04/30 13:11:01 (GMT -600)
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
N/A

##### SUMMARY
When using the implicit `localhost`, adding it to groups, and referencing those groups in a later play, the play is skipped due to it not matching any hosts.

##### STEPS TO REPRODUCE

Playbook:

```yaml
---
- hosts: localhost
  gather_facts: no
  tasks:
    - group_by:
        key: my_group

- hosts: my_group
  gather_facts: no
  tasks:
    - debug: msg="here I am"
```

With implicit localhost:

```
$ ansible-playbook foo.yml 

PLAY [localhost] **************************************************************************************************************************************************************

TASK [group_by] ***************************************************************************************************************************************************************
ok: [localhost]

PLAY [my_group] ***************************************************************************************************************************************************************
skipping: no hosts matched

PLAY RECAP ********************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0   
```

With explicit localhost:

```
$ ansible-playbook -i localhost, -c local foo.yml 

PLAY [localhost] **************************************************************************************************************************************************************

TASK [group_by] ***************************************************************************************************************************************************************
ok: [localhost]

PLAY [my_group] ***************************************************************************************************************************************************************

TASK [debug] ******************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "here I am"
}

PLAY RECAP ********************************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0  
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
Result for implicit localhost should look like explicit localhost

##### ACTUAL RESULTS
The implicit localhost is excluded/ignored from *all* groups instead of just the `all` group.


Discussion:
I was able to work around this fairly easily by using `add_host` instead of `group_by`.

```
---
- hosts: localhost
  gather_facts: no
  tasks:
    # Add a dummy host pointing to localhost that's in the groups expected by
    # the other playbooks
    - add_host:
        name: dummy
        groups: my_group
        ansible_connection: local
```
This is probably closely related to #24157 
The implicit localhost only comes to existence 'on use', but once it is used it will prevent other localhost entries from being created (assumed they were implicit localhost again).

Resolving patch:
diff --git a/lib/ansible/inventory/manager.py b/lib/ansible/inventory/manager.py
--- a/lib/ansible/inventory/manager.py
+++ b/lib/ansible/inventory/manager.py
@@ -290,27 +290,20 @@ def refresh_inventory(self):
         self._inventory = InventoryData()
         self.parse_sources(cache=False)
 
-    def _match(self, string, pattern_str):
-        try:
-            if pattern_str.startswith('~'):
-                return re.search(pattern_str[1:], string)
-            else:
-                return fnmatch.fnmatch(string, pattern_str)
-        except Exception as e:
-            raise AnsibleError('invalid host pattern (%s): %s' % (pattern_str, str(e)))
-
-    def _match_list(self, items, item_attr, pattern_str):
-        results = []
+    def _match_list(self, items, pattern_str):
+        # compile patterns
         try:
             if not pattern_str.startswith('~'):
                 pattern = re.compile(fnmatch.translate(pattern_str))
             else:
                 pattern = re.compile(pattern_str[1:])
         except Exception:
-            raise AnsibleError('invalid host list pattern: %s' % pattern_str)
+            raise AnsibleError('Invalid host list pattern: %s' % pattern_str)
 
+        # apply patterns
+        results = []
         for item in items:
-            if pattern.match(getattr(item, item_attr)):
+            if pattern.match(item):
                 results.append(item)
         return results
 
@@ -383,12 +376,11 @@ def _evaluate_patterns(self, patterns):
             else:
                 that = self._match_one_pattern(p)
                 if p.startswith("!"):
-                    hosts = [h for h in hosts if h not in that]
+                    hosts = [h for h in hosts if h not in frozenset(that)]
                 elif p.startswith("&"):
-                    hosts = [h for h in hosts if h in that]
+                    hosts = [h for h in hosts if h in frozenset(that)]
                 else:
-                    to_append = [h for h in that if h.name not in [y.name for y in hosts]]
-                    hosts.extend(to_append)
+                    hosts.extend([h for h in that if h.name not in frozenset([y.name for y in hosts])])
         return hosts
 
     def _match_one_pattern(self, pattern):
@@ -513,33 +505,25 @@ def _enumerate_matches(self, pattern):
         """
 
         results = []
-
-        def __append_host_to_results(host):
-            if host.name not in results:
-                if not host.implicit:
-                    results.append(host)
-
-        matched = False
-        for group in self._inventory.groups.values():
-            if self._match(to_text(group.name), pattern):
-                matched = True
-                for host in group.get_hosts():
-                    __append_host_to_results(host)
-            else:
-                matching_hosts = self._match_list(group.get_hosts(), 'name', pattern)
-                if matching_hosts:
-                    matched = True
-                    for host in matching_hosts:
-                        __append_host_to_results(host)
+        # check if pattern matches group
+        matching_groups = self._match_list(self._inventory.groups, pattern)
+        if matching_groups:
+            for groupname in matching_groups:
+                results.extend(self._inventory.groups[groupname].get_hosts())
+        else:
+            # pattern might match host
+            matching_hosts = self._match_list(self._inventory.hosts, pattern)
+            if matching_hosts:
+                for hostname in matching_hosts:
+                    results.append(self._inventory.hosts[hostname])
 
         if not results and pattern in C.LOCALHOST:
             # get_host autocreates implicit when needed
             implicit = self._inventory.get_host(pattern)
             if implicit:
                 results.append(implicit)
-                matched = True
 
-        if not matched:
+        if not results:
             display.warning("Could not match supplied host pattern, ignoring: %s" % pattern)
         return results
 


Test patch: