Repository: ansible/ansible

Issue:
2.1.1 no longer works against localhost
##### ISSUE TYPE
- Bug Report
##### ANSIBLE VERSION

ansible 2.1.0.0
  config file =
  configured module search path = Default w/o overrides
ansible 2.1.1.0
  config file =
  configured module search path = Default w/o overrrides
##### CONFIGURATION

<!---
n/a
-->
##### OS / ENVIRONMENT

<!---
FreeBSD 10.1-RELEASE-p36 #0 r301873 amd64
-->
##### SUMMARY

<!--- Explain the problem briefly -->

2.1.0 works (see here)
2.1.1 does not (see here)
##### STEPS TO REPRODUCE

upgrade from 2.1.0 to 2.1.1

<!--- Paste example playbooks or commands between quotes below -->

```
- name: Installation, setup ansible user for remote ssh
  hosts: 127.0.0.1
  connection: local
  tasks:
  - name: Add user
    user: name=ansible comment="Ansible" state=present shell=/bin/sh

  - name: Configure SSH daemon
    lineinfile: dest=/etc/ssh/sshd_config state=present line={{ item }}
    with_items:
      - AuthorizedKeysFile=/etc/ssh/AuthorizedKeys/%u
      - ChallengeResponseAuthentication=no
      - LoginGraceTime=20
      - UseDNS=no

  - name: Install sudo
    pkgng: state=present name=security/sudo

  - name: Configure sudoers
    lineinfile: >
      dest=/usr/local/etc/sudoers.d/ansible  state=present 
      line={{ item }} create=yes group=wheel owner=root mode=u=rw,g=r
    with_items:
      - "ansible ALL=(root) NOPASSWD: ALL"

```

<!--- You can also paste gist.github.com links for larger files -->
##### EXPECTED RESULTS

<!--- What did you expect to happen when running the steps above? -->

For it to work as before (regression)
##### ACTUAL RESULTS

<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

see screen shots for exact message

Instead of 
TASK [setup]**\* 
ok: [127.0.0.1]

it now says
PLAY [Installation, setup ansible user for remote ssh] **\* 
skipping: no hosts matched

<!--- Paste verbatim command output between quotes below -->

```
![image](https://cloud.githubusercontent.com/assets/5847183/17273171/c3ec4a7a-5660-11e6-9289-3c449e8e3ffe.png)
![image](https://cloud.githubusercontent.com/assets/5847183/17273174/d7ff5b60-5660-11e6-9135-06331ad9216c.png)

```



Discussion:
Same here :(

did a rollback to 2.1.0 on my ubuntu 16.04 LTS machine. Now it is working again.

Same here ! 

reproduced, looking at why 127.0.0.1 is not matching as 'implicit localhost'


Resolving patch:
diff --git a/lib/ansible/inventory/__init__.py b/lib/ansible/inventory/__init__.py
--- a/lib/ansible/inventory/__init__.py
+++ b/lib/ansible/inventory/__init__.py
@@ -90,8 +90,6 @@ def __init__(self, loader, variable_manager, host_list=C.DEFAULT_HOST_LIST):
         self.clear_pattern_cache()
 
         self.parse_inventory(host_list)
-        if self.localhost is None:
-            self.localhost = self._create_implicit_localhost()
 
     def serialize(self):
         data = dict()
@@ -128,13 +126,15 @@ def parse_inventory(self, host_list):
                     display.vvv("Unable to parse address from hostname, leaving unchanged: %s" % to_unicode(e))
                     host = h
                     port = None
+
                 new_host = Host(host, port)
+                if h in C.LOCALHOST:
+                    # set default localhost from inventory to avoid creating an implicit one. Last localhost defined 'wins'.
+                    if self.localhost is not None:
+                        display.warning("A duplicate localhost-like entry was found (%s). First found localhost was %s" % (h, self.localhost.name))
+                    display.vvvv("Set default localhost to %s" % h)
+                    self.localhost = new_host
                 all.add_host(new_host)
-                if new_host.name in C.LOCALHOST:
-                    if self.localhost is None:
-                        self.localhost = new_host
-                    else:
-                        display.warning("A duplicate localhost-like entry was found (%s). First found localhost was %s" % (new_host.name, self.localhost.name))
         elif self._loader.path_exists(host_list):
             #TODO: switch this to a plugin loader and a 'condition' per plugin on which it should be tried, restoring 'inventory pllugins'
             if self.is_directory(host_list):
@@ -470,22 +470,30 @@ def __append_host_to_results(host):
                     for host in matching_hosts:
                         __append_host_to_results(host)
 
+        if pattern in C.LOCALHOST and len(results) == 0:
+            new_host = self._create_implicit_localhost(pattern)
+            results.append(new_host)
         return results
 
-    def _create_implicit_localhost(self, pattern='localhost'):
-        new_host = Host(pattern)
-        new_host.address = "127.0.0.1"
-        new_host.implicit = True
-        new_host.vars = self.get_host_vars(new_host)
-        new_host.set_variable("ansible_connection", "local")
-        if "ansible_python_interpreter" not in new_host.vars:
-            py_interp = sys.executable
-            if not py_interp:
-                # sys.executable is not set in some cornercases.  #13585
-                display.warning('Unable to determine python interpreter from sys.executable. Using /usr/bin/python default. You can correct this by setting ansible_python_interpreter for localhost')
-                py_interp = '/usr/bin/python'
-            new_host.set_variable("ansible_python_interpreter", py_interp)
-        self.get_group("ungrouped").add_host(new_host)
+    def _create_implicit_localhost(self, pattern):
+
+        if self.localhost:
+            new_host = self.localhost
+        else:
+            new_host = Host(pattern)
+            new_host.address = "127.0.0.1"
+            new_host.implicit = True
+            new_host.vars = self.get_host_vars(new_host)
+            new_host.set_variable("ansible_connection", "local")
+            if "ansible_python_interpreter" not in new_host.vars:
+                py_interp = sys.executable
+                if not py_interp:
+                    # sys.executable is not set in some cornercases.  #13585
+                    display.warning('Unable to determine python interpreter from sys.executable. Using /usr/bin/python default. You can correct this by setting ansible_python_interpreter for localhost')
+                    py_interp = '/usr/bin/python'
+                new_host.set_variable("ansible_python_interpreter", py_interp)
+            self.get_group("ungrouped").add_host(new_host)
+            self.localhost = new_host
         return new_host
 
     def clear_pattern_cache(self):
@@ -509,14 +517,28 @@ def get_host(self, hostname):
         return self._hosts_cache[hostname]
 
     def _get_host(self, hostname):
-        if hostname in C.LOCALHOST and self.localhost:
-            self.localhost
         matching_host = None
-        for group in self.groups.values():
-            for host in group.get_hosts():
-                if hostname == host.name:
-                    matching_host = host
-                self._hosts_cache[host.name] = host
+        if hostname in C.LOCALHOST:
+            if self.localhost:
+                matching_host= self.localhost
+            else:
+                for host in self.get_group('all').get_hosts():
+                    if host.name in C.LOCALHOST:
+                        matching_host = host
+                        break
+                if not matching_host:
+                    matching_host = self._create_implicit_localhost(hostname)
+                # update caches
+                self._hosts_cache[hostname] = matching_host
+                for host in C.LOCALHOST.difference((hostname,)):
+                    self._hosts_cache[host] = self._hosts_cache[hostname]
+        else:
+            for group in self.groups.values():
+                for host in group.get_hosts():
+                    if host not in self._hosts_cache:
+                        self._hosts_cache[host.name] = host
+                    if hostname == host.name:
+                        matching_host = host
         return matching_host
 
     def get_group(self, groupname):
diff --git a/lib/ansible/vars/hostvars.py b/lib/ansible/vars/hostvars.py
--- a/lib/ansible/vars/hostvars.py
+++ b/lib/ansible/vars/hostvars.py
@@ -62,7 +62,7 @@ def set_inventory(self, inventory):
         self._inventory = inventory
 
     def _find_host(self, host_name):
-        if host_name in C.LOCALHOST:
+        if host_name in C.LOCALHOST and self._inventory.localhost:
             host = self._inventory.localhost
         else:
             host = self._inventory.get_host(host_name)


Test patch: