Repository: ansible/ansible

Issue:
KeyError: 'localhost' when using `group_by`
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
strategy plugin init

##### ANSIBLE VERSION
```
ansible-playbook 2.4.1.0
  config file = /opt/app/ansible.cfg
  configured module search path = ['/opt/app/library']
  ansible python module location = /usr/local/lib/python3.5/dist-packages/ansible
  executable location = /usr/local/bin/ansible-playbook
  python version = 3.5.2 (default, Sep 14 2017, 22:51:06) [GCC 5.4.0 20160609]
```

##### CONFIGURATION
```
CACHE_PLUGIN(/opt/app/ansible.cfg) = jsonfile
CACHE_PLUGIN_CONNECTION(/opt/app/ansible.cfg) = /tmp/ansible_fact_cache_stanson
CACHE_PLUGIN_TIMEOUT(/opt/app/ansible.cfg) = 3600
DEFAULT_FILTER_PLUGIN_PATH(/opt/app/ansible.cfg) = ['/opt/app/filters']
DEFAULT_FORKS(/opt/app/ansible.cfg) = 20
DEFAULT_GATHERING(/opt/app/ansible.cfg) = smart
DEFAULT_HOST_LIST(/opt/app/ansible.cfg) = ['/opt/app/inventory']
DEFAULT_MODULE_PATH(/opt/app/ansible.cfg) = ['/opt/app/library']
DEFAULT_MODULE_UTILS_PATH(/opt/app/ansible.cfg) = ['/opt/app/module_utils']
DEFAULT_PRIVATE_KEY_FILE(/opt/app/ansible.cfg) = /home/asdf/my.key
DEFAULT_REMOTE_USER(/opt/app/ansible.cfg) = ubuntu
DEFAULT_SQUASH_ACTIONS(/opt/app/ansible.cfg) = ['apt']
DEFAULT_VAULT_PASSWORD_FILE(/opt/app/ansible.cfg) = /opt/app/vaultpass.py
HOST_KEY_CHECKING(/opt/app/ansible.cfg) = False
RETRY_FILES_ENABLED(/opt/app/ansible.cfg) = False
```

##### OS / ENVIRONMENT
Ubuntu in a Docker container

##### SUMMARY
After upgrading from stable-2.3 to stable-2.4, I get the following trace. I've tried replacing my ec2.py inventory with an updated version, that doesn't matter- it doesn't change anything. see #31274 for a previous case that was obviously similar.

Note I have plays with:
```
- hosts: localhost
```
Adding or removing those doesn't change anything, but what causes this to break (for me) is having this in a play:

```
  pre_tasks:
  - group_by: key=os_{{ansible_distribution}}
```

I tried moving it to 'tasks', still fails. I tried removing the variable (see below), still fails.

```
  - group_by: key=os_MacOSX
```

##### STEPS TO REPRODUCE

##### EXPECTED RESULTS

##### ACTUAL RESULTS
```
PLAYBOOK: rex.yml ******************************************************************************************************
1 plays in rex.yml

PLAY [localhost] *******************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************
Using module file /usr/local/lib/python3.5/dist-packages/ansible/modules/system/setup.py
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: root
<127.0.0.1> EXEC /bin/sh -c 'echo ~ && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /root/.ansible/tmp/ansible-tmp-1508876288.0254786-168044730198300 `" && echo ansible-tmp-1508876288.0254786-168044730198300="` echo /root/.ansible/tmp/ansible-tmp-1508876288.0254786-168044730198300 `" ) && sleep 0'
<127.0.0.1> PUT /tmp/tmpd750uvbw TO /root/.ansible/tmp/ansible-tmp-1508876288.0254786-168044730198300/setup.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1508876288.0254786-168044730198300/ /root/.ansible/tmp/ansible-tmp-1508876288.0254786-168044730198300/setup.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python3 /root/.ansible/tmp/ansible-tmp-1508876288.0254786-168044730198300/setup.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1508876288.0254786-168044730198300/" > /dev/null 2>&1 && sleep 0'
ok: [localhost]

TASK [group_by] ********************************************************************************************************
task path: /opt/app/rex.yml:18
ERROR! Unexpected Exception, this is probably a bug: 'localhost'
the full traceback was:

Traceback (most recent call last):
  File "/usr/local/bin/ansible-playbook", line 106, in <module>
    exit_code = cli.run()
  File "/usr/local/lib/python3.5/dist-packages/ansible/cli/playbook.py", line 130, in run
    results = pbex.run()
  File "/usr/local/lib/python3.5/dist-packages/ansible/executor/playbook_executor.py", line 154, in run
    result = self._tqm.run(play=play)
  File "/usr/local/lib/python3.5/dist-packages/ansible/executor/task_queue_manager.py", line 302, in run
    play_return = strategy.run(iterator, play_context)
  File "/usr/local/lib/python3.5/dist-packages/ansible/plugins/strategy/linear.py", line 292, in run
    results += self._wait_on_pending_results(iterator)
  File "/usr/local/lib/python3.5/dist-packages/ansible/plugins/strategy/__init__.py", line 586, in _wait_on_pending_results
    results = self._process_pending_results(iterator)
  File "/usr/local/lib/python3.5/dist-packages/ansible/plugins/strategy/__init__.py", line 497, in _process_pending_results
    self._add_group(original_host, result_item)
  File "/usr/local/lib/python3.5/dist-packages/ansible/plugins/strategy/__init__.py", line 632, in _add_group
    real_host = self._inventory.hosts[host.name]
KeyError: 'localhost'
```




Discussion:
Files identified in the description:
* [lib/ansible/plugins/strategy](https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/strategy)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
It seems like this is happening because "localhost" doesn't exist in the inventory when you are running your playbook or at least that was the case for me.
in order to fix it in my case I manually added localhost after running the dynamic inventory
`- name: add hosts to localhost group
  add_host:
    name: "127.0.0.1"
    groups: "localhost"`

This is a temporary workaround and it covers both 'localhost' and '127.0.0.1'.
The fix should happen in "lib/ansible/plugins/strategy/__init__.py" I'm not sure whether a clearer exception should be displayed or localhost and 127.0.0.1 should be automatically added to inventories that lack them but both of these fixes are debatable and since I'm not familiar with ansible's best practices and architecture I will leave this here just in-case someone with more experience wants to fix this issue.

Resolving patch:
diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py
--- a/lib/ansible/plugins/strategy/__init__.py
+++ b/lib/ansible/plugins/strategy/__init__.py
@@ -717,7 +717,12 @@ def _add_group(self, host, result_item):
         # the host here is from the executor side, which means it was a
         # serialized/cloned copy and we'll need to look up the proper
         # host object from the master inventory
-        real_host = self._inventory.hosts[host.name]
+        real_host = self._inventory.hosts.get(host.name)
+        if real_host is None:
+            if host.name == self._inventory.localhost.name:
+                real_host = self._inventory.localhost
+            else:
+                raise AnsibleError('%s cannot be matched in inventory' % host.name)
         group_name = result_item.get('add_group')
         parent_group_names = result_item.get('parent_groups', [])
 


Test patch: