diff --git a/test/integration/targets/inventory_cache/exercise_cache.yml b/test/integration/targets/inventory_cache/exercise_cache.yml new file mode 100644 index 00000000000..6fd87127c65 --- /dev/null +++ b/test/integration/targets/inventory_cache/exercise_cache.yml @@ -0,0 +1,4 @@ +plugin: exercise_cache +cache: true +cache_plugin: jsonfile +cache_connection: ./cache diff --git a/test/integration/targets/inventory_cache/plugins/inventory/exercise_cache.py b/test/integration/targets/inventory_cache/plugins/inventory/exercise_cache.py new file mode 100644 index 00000000000..fece658e37c --- /dev/null +++ b/test/integration/targets/inventory_cache/plugins/inventory/exercise_cache.py @@ -0,0 +1,328 @@ +# Copyright (c) 2022 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + inventory: exercise_cache + short_description: run tests against the specified cache plugin + description: + - This plugin doesn't modify inventory. + - Load a cache plugin and test the inventory cache interface is dict-like. + - Most inventory cache write methods only apply to the in-memory cache. + - The 'flush' and 'set_cache' methods should be used to apply changes to the backing cache plugin. + - The inventory cache read methods prefer the in-memory cache, and fall back to reading from the cache plugin. + extends_documentation_fragment: + - inventory_cache + options: + plugin: + required: true + description: name of the plugin (exercise_cache) + cache_timeout: + ini: [] + env: [] + cli: [] + default: 0 # never expire +''' + +from ansible.errors import AnsibleError +from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable +from ansible.utils.display import Display + +from time import sleep + + +display = Display() + + +class InventoryModule(BaseInventoryPlugin, Cacheable): + + NAME = 'exercise_cache' + + test_cache_methods = [ + 'test_plugin_name', + 'test_update_cache_if_changed', + 'test_set_cache', + 'test_load_whole_cache', + 'test_iter', + 'test_len', + 'test_get_missing_key', + 'test_get_expired_key', + 'test_get', + 'test_items', + 'test_keys', + 'test_values', + 'test_pop', + 'test_del', + 'test_set', + 'test_update', + 'test_flush', + ] + + def verify_file(self, path): + if not path.endswith(('exercise_cache.yml', 'exercise_cache.yaml',)): + return False + return super(InventoryModule, self).verify_file(path) + + def parse(self, inventory, loader, path, cache=None): + super(InventoryModule, self).parse(inventory, loader, path) + self._read_config_data(path) + + try: + self.exercise_test_cache() + except AnsibleError: + raise + except Exception as e: + raise AnsibleError("Failed to run cache tests: {0}".format(e)) from e + + def exercise_test_cache(self): + failed = [] + for test_name in self.test_cache_methods: + try: + getattr(self, test_name)() + except AssertionError: + failed.append(test_name) + finally: + self.cache.flush() + self.cache.update_cache_if_changed() + + if failed: + raise AnsibleError(f"Cache tests failed: {', '.join(failed)}") + + def test_equal(self, a, b): + try: + assert a == b + except AssertionError: + display.warning(f"Assertion {a} == {b} failed") + raise + + def test_plugin_name(self): + self.test_equal(self.cache._plugin_name, self.get_option('cache_plugin')) + + def test_update_cache_if_changed(self): + self.cache._retrieved = {} + self.cache._cache = {'foo': 'bar'} + + self.cache.update_cache_if_changed() + + self.test_equal(self.cache._retrieved, {'foo': 'bar'}) + self.test_equal(self.cache._cache, {'foo': 'bar'}) + + def test_set_cache(self): + cache_key1 = 'key1' + cache1 = {'hosts': {'h1': {'foo': 'bar'}}} + cache_key2 = 'key2' + cache2 = {'hosts': {'h2': {}}} + + self.cache._cache = {cache_key1: cache1, cache_key2: cache2} + self.cache.set_cache() + + self.test_equal(self.cache._plugin.contains(cache_key1), True) + self.test_equal(self.cache._plugin.get(cache_key1), cache1) + self.test_equal(self.cache._plugin.contains(cache_key2), True) + self.test_equal(self.cache._plugin.get(cache_key2), cache2) + + def test_load_whole_cache(self): + cache_data = { + 'key1': {'hosts': {'h1': {'foo': 'bar'}}}, + 'key2': {'hosts': {'h2': {}}}, + } + self.cache._cache = cache_data + self.cache.set_cache() + self.cache._cache = {} + + self.cache.load_whole_cache() + self.test_equal(self.cache._cache, cache_data) + + def test_iter(self): + cache_data = { + 'key1': {'hosts': {'h1': {'foo': 'bar'}}}, + 'key2': {'hosts': {'h2': {}}}, + } + self.cache._cache = cache_data + self.test_equal(sorted(list(self.cache)), ['key1', 'key2']) + + def test_len(self): + cache_data = { + 'key1': {'hosts': {'h1': {'foo': 'bar'}}}, + 'key2': {'hosts': {'h2': {}}}, + } + self.cache._cache = cache_data + self.test_equal(len(self.cache), 2) + + def test_get_missing_key(self): + # cache should behave like a dictionary + # a missing key with __getitem__ should raise a KeyError + try: + self.cache['keyerror'] + except KeyError: + pass + else: + assert False + + # get should return the default instead + self.test_equal(self.cache.get('missing'), None) + self.test_equal(self.cache.get('missing', 'default'), 'default') + + def _setup_expired(self): + self.cache._cache = {'expired': True} + self.cache.set_cache() + + # empty the in-memory info to test loading the key + # keys that expire mid-use do not cause errors + self.cache._cache = {} + self.cache._retrieved = {} + self.cache._plugin._cache = {} + + self.cache._plugin.set_option('timeout', 1) + self.cache._plugin._timeout = 1 + sleep(2) + + def _cleanup_expired(self): + # Set cache timeout back to never + self.cache._plugin.set_option('timeout', 0) + self.cache._plugin._timeout = 0 + + def test_get_expired_key(self): + if not hasattr(self.cache._plugin, '_timeout'): + # DB-backed caches do not have a standard timeout interface + return + + self._setup_expired() + try: + self.cache['expired'] + except KeyError: + pass + else: + assert False + finally: + self._cleanup_expired() + + self._setup_expired() + try: + self.test_equal(self.cache.get('expired'), None) + self.test_equal(self.cache.get('expired', 'default'), 'default') + finally: + self._cleanup_expired() + + def test_get(self): + # test cache behaves like a dictionary + + # set the cache to test getting a key that exists + k1 = {'hosts': {'h1': {'foo': 'bar'}}} + k2 = {'hosts': {'h2': {}}} + self.cache._cache = {'key1': k1, 'key2': k2} + self.cache.set_cache() + + self.test_equal(self.cache['key1'], k1) + self.test_equal(self.cache.get('key1'), k1) + + # empty the in-memory info to test loading the key from the plugin + self.cache._cache = {} + self.cache._retrieved = {} + self.cache._plugin._cache = {} + + self.test_equal(self.cache['key1'], k1) + self.test_equal(self.cache.get('key1'), k1) + + def test_items(self): + self.test_equal(self.cache.items(), {}.items()) + + test_items = {'hosts': {'host1': {'foo': 'bar'}}} + self.cache._cache = test_items + self.test_equal(self.cache.items(), test_items.items()) + + def test_keys(self): + self.test_equal(self.cache.keys(), {}.keys()) + + test_items = {'hosts': {'host1': {'foo': 'bar'}}} + self.cache._cache = test_items + self.test_equal(self.cache.keys(), test_items.keys()) + + def test_values(self): + self.test_equal(list(self.cache.values()), list({}.values())) + + test_items = {'hosts': {'host1': {'foo': 'bar'}}} + self.cache._cache = test_items + self.test_equal(list(self.cache.values()), list(test_items.values())) + + def test_pop(self): + try: + self.cache.pop('missing') + except KeyError: + pass + else: + assert False + + self.test_equal(self.cache.pop('missing', 'default'), 'default') + + self.cache._cache = {'cache_key': 'cache'} + self.test_equal(self.cache.pop('cache_key'), 'cache') + + # test backing plugin cache isn't modified + cache_key1 = 'key1' + cache1 = {'hosts': {'h1': {'foo': 'bar'}}} + cache_key2 = 'key2' + cache2 = {'hosts': {'h2': {}}} + + self.cache._cache = {cache_key1: cache1, cache_key2: cache2} + self.cache.set_cache() + + self.test_equal(self.cache.pop('key1'), cache1) + self.test_equal(self.cache._cache, {cache_key2: cache2}) + self.test_equal(self.cache._plugin._cache, {cache_key1: cache1, cache_key2: cache2}) + + def test_del(self): + try: + del self.cache['missing'] + except KeyError: + pass + else: + assert False + + cache_key1 = 'key1' + cache1 = {'hosts': {'h1': {'foo': 'bar'}}} + cache_key2 = 'key2' + cache2 = {'hosts': {'h2': {}}} + + self.cache._cache = {cache_key1: cache1, cache_key2: cache2} + self.cache.set_cache() + + del self.cache['key1'] + + self.test_equal(self.cache._cache, {cache_key2: cache2}) + self.test_equal(self.cache._plugin._cache, {cache_key1: cache1, cache_key2: cache2}) + + def test_set(self): + cache_key = 'key1' + hosts = {'hosts': {'h1': {'foo': 'bar'}}} + self.cache[cache_key] = hosts + + self.test_equal(self.cache._cache, {cache_key: hosts}) + self.test_equal(self.cache._plugin._cache, {}) + + def test_update(self): + cache_key1 = 'key1' + cache1 = {'hosts': {'h1': {'foo': 'bar'}}} + cache_key2 = 'key2' + cache2 = {'hosts': {'h2': {}}} + + self.cache._cache = {cache_key1: cache1} + self.cache.update({cache_key2: cache2}) + self.test_equal(self.cache._cache, {cache_key1: cache1, cache_key2: cache2}) + + def test_flush(self): + cache_key1 = 'key1' + cache1 = {'hosts': {'h1': {'foo': 'bar'}}} + cache_key2 = 'key2' + cache2 = {'hosts': {'h2': {}}} + + self.cache._cache = {cache_key1: cache1, cache_key2: cache2} + self.cache.set_cache() + + # Unlike the dict write methods, cache.flush() flushes the backing plugin + self.cache.flush() + + self.test_equal(self.cache._cache, {}) + self.test_equal(self.cache._plugin._cache, {}) diff --git a/test/integration/targets/inventory_cache/runme.sh b/test/integration/targets/inventory_cache/runme.sh index 098439eb227..942eb8223bf 100755 --- a/test/integration/targets/inventory_cache/runme.sh +++ b/test/integration/targets/inventory_cache/runme.sh @@ -21,3 +21,5 @@ test "$(ansible-inventory -i cache_host.yml --graph 2>&1 | tee out.txt | grep -c readhost="$(grep 'testhost[0-9]\{1,2\}' out.txt)" test "$readhost" = "$writehost" + +ansible-inventory -i exercise_cache.yml --graph diff --git a/test/support/integration/plugins/cache/jsonfile.py b/test/support/integration/plugins/cache/jsonfile.py deleted file mode 100644 index 80b16f55b56..00000000000 --- a/test/support/integration/plugins/cache/jsonfile.py +++ /dev/null @@ -1,63 +0,0 @@ -# (c) 2014, Brian Coca, Josh Drake, et al -# (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -DOCUMENTATION = ''' - cache: jsonfile - short_description: JSON formatted files. - description: - - This cache uses JSON formatted, per host, files saved to the filesystem. - version_added: "1.9" - author: Ansible Core (@ansible-core) - options: - _uri: - required: True - description: - - Path in which the cache plugin will save the JSON files - env: - - name: ANSIBLE_CACHE_PLUGIN_CONNECTION - ini: - - key: fact_caching_connection - section: defaults - _prefix: - description: User defined prefix to use when creating the JSON files - env: - - name: ANSIBLE_CACHE_PLUGIN_PREFIX - ini: - - key: fact_caching_prefix - section: defaults - _timeout: - default: 86400 - description: Expiration timeout in seconds for the cache plugin data. Set to 0 to never expire - env: - - name: ANSIBLE_CACHE_PLUGIN_TIMEOUT - ini: - - key: fact_caching_timeout - section: defaults - type: integer -''' - -import codecs -import json - -from ansible.parsing.ajson import AnsibleJSONEncoder, AnsibleJSONDecoder -from ansible.plugins.cache import BaseFileCacheModule - - -class CacheModule(BaseFileCacheModule): - """ - A caching module backed by json files. - """ - - def _load(self, filepath): - # Valid JSON is always UTF-8 encoded. - with codecs.open(filepath, 'r', encoding='utf-8') as f: - return json.load(f, cls=AnsibleJSONDecoder) - - def _dump(self, value, filepath): - with codecs.open(filepath, 'w', encoding='utf-8') as f: - f.write(json.dumps(value, cls=AnsibleJSONEncoder, sort_keys=True, indent=4))