Repository: ansible/ansible

Issue:
Using incorrect fact_caching gives weird error
<!---
Please do not report issues/requests related to Ansible modules here !!

Report them to the appropriate modules-core or modules-extras project:
  - https://github.com/ansible/ansible-modules-core/issues
  - https://github.com/ansible/ansible-modules-extras/issues

Also verify first that your issue/request is not already reported in GitHub
-->


##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the plugin/task/feature -->
cache plugin hook

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
v2.2

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->
Any

##### SUMMARY
<!--- Explain the problem briefly -->
When I incorrectly configured Ansible to use a JSON facts cache, I got a very weird error that I initially could not trace back to this configuration item.

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
```
fact_caching = json
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Using /home/dag/home-made/ansible.testing/ansible.cfg as config file
 ERROR! No fact cache plugin found of type "json".

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
Using /home/dag/home-made/ansible.testing/ansible.cfg as config file
 [WARNING]: Failed to load fact cache plugins


PLAY [localhost] ***************************************************************
ERROR! Unexpected Exception: 'NoneType' object has no attribute 'contains'
```


Discussion:
I can reproduce with 2.3

```
Traceback (most recent call last):
  File "/home/adrian/src/ansible/bin/ansible-playbook", line 103, in <module>
    exit_code = cli.run()
  File "/home/adrian/src/ansible/lib/ansible/cli/playbook.py", line 157, in run
    results = pbex.run()
  File "/home/adrian/src/ansible/lib/ansible/executor/playbook_executor.py", line 154, in run
    result = self._tqm.run(play=play)
  File "/home/adrian/src/ansible/lib/ansible/executor/task_queue_manager.py", line 269, in run
    start_at_done = self._start_at_done,
  File "/home/adrian/src/ansible/lib/ansible/executor/play_iterator.py", line 204, in __init__
    if host.name in variable_manager._fact_cache and variable_manager._fact_cache.get('module_setup', False):
  File "/home/adrian/src/ansible/lib/ansible/plugins/cache/__init__.py", line 56, in __contains__
    return self._plugin.contains(key)
AttributeError: 'NoneType' object has no attribute 'contains'
```

Resolving patch:
diff --git a/lib/ansible/plugins/cache/__init__.py b/lib/ansible/plugins/cache/__init__.py
--- a/lib/ansible/plugins/cache/__init__.py
+++ b/lib/ansible/plugins/cache/__init__.py
@@ -20,6 +20,7 @@
 from collections import MutableMapping
 
 from ansible import constants as C
+from ansible.errors import AnsibleError
 from ansible.plugins import cache_loader
 
 try:
@@ -33,6 +34,14 @@ class FactCache(MutableMapping):
 
     def __init__(self, *args, **kwargs):
         self._plugin = cache_loader.get(C.CACHE_PLUGIN)
+        if not self._plugin:
+            raise AnsibleError('Unable to load the facts cache plugin (%s)\n'
+                               'Check fact cache config options :\n'
+                               'Current values:\n'
+                               '    fact_caching: %s\n'
+                               '    fact_caching_connection: %s' %
+                               (C.CACHE_PLUGIN, C.CACHE_PLUGIN, C.CACHE_PLUGIN_CONNECTION))
+
         # Backwards compat: self._display isn't really needed, just import the global display and use that.
         self._display = display
 


Test patch:
diff --git a/test/units/plugins/cache/test_cache.py b/test/units/plugins/cache/test_cache.py
--- a/test/units/plugins/cache/test_cache.py
+++ b/test/units/plugins/cache/test_cache.py
@@ -20,6 +20,7 @@
 __metaclass__ = type
 
 from ansible.compat.tests import unittest, mock
+from ansible.errors import AnsibleError
 from ansible.plugins.cache import FactCache
 from ansible.plugins.cache.base import BaseCacheModule
 from ansible.plugins.cache.memory import CacheModule as MemoryCache
@@ -56,6 +57,14 @@ def test_copy(self):
         self.assertEqual(type(a_copy), dict)
         self.assertEqual(a_copy, dict(avocado='fruit', daisy='flower'))
 
+    def test_plugin_load_failure(self):
+        # See https://github.com/ansible/ansible/issues/18751
+        # Note no fact_connection config set, so this will fail
+        with mock.patch('ansible.constants.CACHE_PLUGIN', 'json'):
+            self.assertRaisesRegexp(AnsibleError,
+                                    "Unable to load the facts cache plugin.*json.*",
+                                    FactCache)
+
 
 class TestAbstractClass(unittest.TestCase):