Repository: ansible/ansible

Issue:
hosts: [] is matching 'all' hosts
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and devel branch are affected too.
Always add information AFTER of these html comments.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Bug Report

##### COMPONENT NAME
lib/ansible/plugins/filters/mathstuff.py

Set Theory Filters - difference()



##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.0.0
  config file = None
  configured module search path = [u'/Users/aaron/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /Users/aaron/ha-dev/venv/ans24-pyvmomi/lib/python2.7/site-packages/ansible
  executable location = ha-dev/venv/ans24-pyvmomi/bin/ansible
  python version = 2.7.14 (default, Feb 26 2018, 20:28:39) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)]

```

##### CONFIGURATION
N/A


##### OS / ENVIRONMENT

N/A


##### SUMMARY
<!--- Explain the problem briefly -->
this expression:

```
{{ groups['masters'] | difference([groups['masters'][0]]) }}
```
does not produce an empty set if there is only one item in the masters group.
for >1 items in the group, it works as expected.

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
```yaml

- name: 'test'
  hosts: "{{ groups['masters'] | difference([groups['masters'][0]]) }}"
  gather_facts: no
  tasks:
    - debug: msg="item - {{inventory_hostname}}"

hosts file:
[nodes]
master1.foo.com ansible_host=192.168.0.51 
worker1.foo.com ansible_host=192.168.0.56
[masters]
master1.foo.com ansible_host=192.168.0.51 
[workers]
worker1.foo.com ansible_host=192.168.0.56
[dns]
ns1.foo.com ansible_host=192.168.0.254
[vip]
k8s.foo.com ansible_host=192.168.0.244
[collector]
collector.foo.com ansible_host=192.168.0.50

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
that the play does not iterate over *any* hosts, as I think difference() in this case should prodce an empty set.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
play iterates over ALL of the hosts:
PLAY RECAP *********************************************************************
k8s.foo.com      : ok=1    changed=0    unreachable=0    failed=0   
master1.foo.com  : ok=1    changed=0    unreachable=0    failed=0   
ns1.foo.com      : ok=1    changed=0    unreachable=0    failed=0   
worker1.foo.com  : ok=1    changed=0    unreachable=0    failed=0 

<!--- Paste verbatim command output between quotes below -->
```

```



Discussion:
pinging @bcoca, @abadger 

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
@@ -325,49 +325,52 @@ def get_hosts(self, pattern="all", ignore_limits=False, ignore_restrictions=Fals
         or applied subsets
         """
 
+        hosts = []
+
         # Check if pattern already computed
         if isinstance(pattern, list):
             pattern_hash = u":".join(pattern)
         else:
             pattern_hash = pattern
 
-        if not ignore_limits and self._subset:
-            pattern_hash += ":%s" % to_native(self._subset)
+        if pattern_hash:
+            if not ignore_limits and self._subset:
+                pattern_hash += u":%s" % to_text(self._subset, errors='surrogate_or_strict')
 
-        if not ignore_restrictions and self._restriction:
-            pattern_hash += ":%s" % to_native(self._restriction)
+            if not ignore_restrictions and self._restriction:
+                pattern_hash += u":%s" % to_text(self._restriction, errors='surrogate_or_strict')
 
-        if pattern_hash not in self._hosts_patterns_cache:
+            if pattern_hash not in self._hosts_patterns_cache:
 
-            patterns = split_host_pattern(pattern)
-            hosts = self._evaluate_patterns(patterns)
+                patterns = split_host_pattern(pattern)
+                hosts = self._evaluate_patterns(patterns)
 
-            # mainly useful for hostvars[host] access
-            if not ignore_limits and self._subset:
-                # exclude hosts not in a subset, if defined
-                subset = self._evaluate_patterns(self._subset)
-                hosts = [h for h in hosts if h in subset]
+                # mainly useful for hostvars[host] access
+                if not ignore_limits and self._subset:
+                    # exclude hosts not in a subset, if defined
+                    subset = self._evaluate_patterns(self._subset)
+                    hosts = [h for h in hosts if h in subset]
 
-            if not ignore_restrictions and self._restriction:
-                # exclude hosts mentioned in any restriction (ex: failed hosts)
-                hosts = [h for h in hosts if h.name in self._restriction]
-
-            seen = set()
-            self._hosts_patterns_cache[pattern_hash] = [x for x in hosts if x not in seen and not seen.add(x)]
-
-        # sort hosts list if needed (should only happen when called from strategy)
-        if order in ['sorted', 'reverse_sorted']:
-            from operator import attrgetter
-            hosts = sorted(self._hosts_patterns_cache[pattern_hash][:], key=attrgetter('name'), reverse=(order == 'reverse_sorted'))
-        elif order == 'reverse_inventory':
-            hosts = sorted(self._hosts_patterns_cache[pattern_hash][:], reverse=True)
-        else:
-            hosts = self._hosts_patterns_cache[pattern_hash][:]
-            if order == 'shuffle':
-                from random import shuffle
-                shuffle(hosts)
-            elif order not in [None, 'inventory']:
-                AnsibleOptionsError("Invalid 'order' specified for inventory hosts: %s" % order)
+                if not ignore_restrictions and self._restriction:
+                    # exclude hosts mentioned in any restriction (ex: failed hosts)
+                    hosts = [h for h in hosts if h.name in self._restriction]
+
+                seen = set()
+                self._hosts_patterns_cache[pattern_hash] = [x for x in hosts if x not in seen and not seen.add(x)]
+
+            # sort hosts list if needed (should only happen when called from strategy)
+            if order in ['sorted', 'reverse_sorted']:
+                from operator import attrgetter
+                hosts = sorted(self._hosts_patterns_cache[pattern_hash][:], key=attrgetter('name'), reverse=(order == 'reverse_sorted'))
+            elif order == 'reverse_inventory':
+                hosts = sorted(self._hosts_patterns_cache[pattern_hash][:], reverse=True)
+            else:
+                hosts = self._hosts_patterns_cache[pattern_hash][:]
+                if order == 'shuffle':
+                    from random import shuffle
+                    shuffle(hosts)
+                elif order not in [None, 'inventory']:
+                    AnsibleOptionsError("Invalid 'order' specified for inventory hosts: %s" % order)
 
         return hosts
 


Test patch: