Skip to content

Commit

Permalink
Fix inventory cache interface (ansible#50446)
Browse files Browse the repository at this point in the history
* Replace InventoryFileCacheModule with a better developer-interface

Use new interface for inventory plugins with backwards compatibility

Auto-update the backing cache-plugin if the cache has changed after parsing the inventory plugin

* Update CacheModules to use the config system and add a deprecation warning if they are being imported directly rather than using cache_loader

* Fix foreman inventory caching

* Add tests

* Add integration test to check that fact caching works normally with cache plugins using ansible.constants and inventory caching provides a helpful error for non-compatible cache plugins

* Add some developer documentation for inventory and cache plugins

* Add user documentation for inventory caching

* Add deprecation docs

* Apply suggestions from docs review

* Add changelog
  • Loading branch information
s-hertel authored Mar 6, 2019
1 parent 831f068 commit 9687879
Show file tree
Hide file tree
Showing 24 changed files with 831 additions and 86 deletions.
9 changes: 9 additions & 0 deletions changelogs/fragments/restructure_inventory_cache.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
minor_changes:
- inventory plugins - Inventory plugins that support caching can now use any cache plugin
shipped with Ansible.
deprecated_features:
- inventory plugins - Inventory plugins using self.cache is deprecated and will be removed
in 2.12. Inventory plugins should use self._cache as a dictionary to store results.
- cache plugins - Importing cache plugins directly is deprecated and will be removed in 2.12.
Cache plugins should use the cache_loader instead so cache options can be reconciled via
the configuration system rather than constants.
83 changes: 82 additions & 1 deletion docs/docsite/rst/dev_guide/developing_inventory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,90 @@ To facilitate this there are a few of helper functions used in the example below
The specifics will vary depending on API and structure returned. But one thing to keep in mind, if the inventory source or any other issue crops up you should ``raise AnsibleParserError`` to let Ansible know that the source was invalid or the process failed.

For examples on how to implement an inventory plug in, see the source code here:
For examples on how to implement an inventory plugin, see the source code here:
`lib/ansible/plugins/inventory <https://github.com/ansible/ansible/tree/devel/lib/ansible/plugins/inventory>`_.

.. _inventory_plugin_caching:

inventory cache
^^^^^^^^^^^^^^^

Extend the inventory plugin documentation with the inventory_cache documentation fragment and use the Cacheable base class to have the caching system at your disposal.

.. code-block:: yaml
extends_documentation_fragment:
- inventory_cache
.. code-block:: python
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
NAME = 'myplugin'
Next, load the cache plugin specified by the user to read from and update the cache. If your inventory plugin uses YAML based configuration files and the ``_read_config_data`` method, the cache plugin is loaded within that method. If your inventory plugin does not use ``_read_config_data``, you must load the cache explicitly with ``load_cache_plugin``.

.. code-block:: python
NAME = 'myplugin'
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
self.load_cache_plugin()
Before using the cache, retrieve a unique cache key using the ``get_cache_key`` method. This needs to be done by all inventory modules using the cache, so you don't use/overwrite other parts of the cache.

.. code-block:: python
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
self.load_cache_plugin()
cache_key = self.get_cache_key(path)
Now that you've enabled caching, loaded the correct plugin, and retrieved a unique cache key, you can set up the flow of data between the cache and your inventory using the ``cache`` parameter of the ``parse`` method. This value comes from the inventory manager and indicates whether the inventory is being refreshed (such as via ``--flush-cache`` or the meta task ``refresh_inventory``). Although the cache shouldn't be used to populate the inventory when being refreshed, the cache should be updated with the new inventory if the user has enabled caching. You can use ``self._cache`` like a dictionary. The following pattern allows refreshing the inventory to work in conjunction with caching.

.. code-block:: python
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
self.load_cache_plugin()
cache_key = self.get_cache_key(path)
# cache may be True or False at this point to indicate if the inventory is being refreshed
# get the user's cache option too to see if we should save the cache if it is changing
user_cache_setting = self.get_option('cache')
# read if the user has caching enabled and the cache isn't being refreshed
attempt_to_read_cache = user_cache_setting and cache
# update if the user has caching enabled and the cache is being refreshed; update this value to True if the cache has expired below
cache_needs_update = user_cache_setting and not cache
# attempt to read the cache if inventory isn't being refreshed and the user has caching enabled
if attempt_to_read_cache:
try:
results = self._cache[cache_key]
except KeyError:
# This occurs if the cache_key is not in the cache or if the cache_key expired, so the cache needs to be updated
cache_needs_update = True
if cache_needs_updates:
results = self.get_inventory()
# set the cache
self._cache[cache_key] = results
self.populate(results)
After the ``parse`` method is complete, the contents of ``self._cache`` is used to set the cache plugin if the contents of the cache have changed.

You have three other cache methods available:
- ``set_cache_plugin`` forces the cache plugin to be set with the contents of ``self._cache`` before the ``parse`` method completes
- ``update_cache_if_changed`` sets the cache plugin only if ``self._cache`` has been modified before the ``parse`` method completes
- ``clear_cache`` deletes the keys in ``self._cache`` from your cache plugin

.. _inventory_source_common_format:

Inventory source common format
Expand Down
41 changes: 41 additions & 0 deletions docs/docsite/rst/dev_guide/developing_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ To define configurable options for your plugin, describe them in the ``DOCUMENTA
To access the configuration settings in your plugin, use ``self.get_option(<option_name>)``. For most plugin types, the controller pre-populates the settings. If you need to populate settings explicitly, use a ``self.set_options()`` call.


Plugins that support embedded documentation (see :ref:`ansible-doc` for the list) must include well-formed doc strings to be considered for merge into the Ansible repo. If you inherit from a plugin, you must document the options it takes, either via a documentation fragment or as a copy. See :ref:`module_documenting` for more information on correct documentation. Thorough documentation is a good idea even if you're developing a plugin for local use.

Developing particular plugin types
Expand Down Expand Up @@ -144,6 +145,46 @@ the local time, returning the time delta in days, seconds and microseconds.
For practical examples of action plugins,
see the source code for the `action plugins included with Ansible Core <https://github.com/ansible/ansible/tree/devel/lib/ansible/plugins/action>`_

.. _developing_cache_plugins:

Cache plugins
-------------

Cache plugins store gathered facts and data retrieved by inventory plugins.

Import cache plugins using the cache_loader so you can use ``self.set_options()`` and ``self.get_option(<option_name>)``. If you import a cache plugin directly in the code base, you can only access options via ``ansible.constants``, and you break the cache plugin's ability to be used by an inventory plugin.

.. code-block:: python
from ansible.plugins.loader import cache_loader
[...]
plugin = cache_loader.get('custom_cache', **cache_kwargs)
There are two base classes for cache plugins, ``BaseCacheModule`` for database-backed caches, and ``BaseCacheFileModule`` for file-backed caches.

To create a cache plugin, start by creating a new ``CacheModule`` class with the appropriate base class. If you're creating a plugin using an ``__init__`` method you should initialize the base class with any provided args and kwargs to be compatible with inventory plugin cache options. The base class calls ``self.set_options(direct=kwargs)``. After the base class ``__init__`` method is called ``self.get_option(<option_name>)`` should be used to access cache options.

New cache plugins should take the options ``_uri``, ``_prefix``, and ``_timeout`` to be consistent with existing cache plugins.

.. code-block:: python
from ansible.plugins.cache import BaseCacheModule
class CacheModule(BaseCacheModule):
def __init__(self, *args, **kwargs):
super(CacheModule, self).__init__(*args, **kwargs)
self._connection = self.get_option('_uri')
self._prefix = self.get_option('_prefix')
self._timeout = self.get_option('_timeout')
If you use the ``BaseCacheModule``, you must implement the methods ``get``, ``contains``, ``keys``, ``set``, ``delete``, ``flush``, and ``copy``. The ``contains`` method should return a boolean that indicates if the key exists and has not expired. Unlike file-based caches, the ``get`` method does not raise a KeyError if the cache has expired.

If you use the ``BaseFileCacheModule``, you must implement ``_load`` and ``_dump`` methods that will be called from the base class methods ``get`` and ``set``.

If your cache plugin stores JSON, use ``AnsibleJSONEncoder`` in the ``_dump`` or ``set`` method and ``AnsibleJSONDecoder`` in the ``_load`` or ``get`` method.

For example cache plugins, see the source code for the `cache plugins included with Ansible Core <https://github.com/ansible/ansible/tree/devel/lib/ansible/plugins/cache>`_.

.. _developing_callbacks:

Callback plugins
Expand Down
25 changes: 25 additions & 0 deletions docs/docsite/rst/plugins/inventory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,31 @@ Now the output of ``ansible-inventory -i demo.aws_ec2.yml --graph``:
If a host does not have the variables in the configuration above (i.e. ``tags.Name``, ``tags``, ``private_ip_address``), the host will not be added to groups other than those that the inventory plugin creates and the ``ansible_host`` host variable will not be modified.

If an inventory plugin supports caching, you can enable and set caching options for an individual YAML configuration source or for multiple inventory sources using environment variables or Ansible configuration files. If you enable caching for an inventory plugin without providing inventory-specific caching options, the inventory plugin will use fact-caching options. Here is an example of enabling caching for an individual YAML configuration file:

.. code-block:: yaml
# demo.aws_ec2.yml
plugin: aws_ec2
cache: yes
cache_plugin: jsonfile
cache_timeout: 7200
cache_connection: /tmp/aws_inventory
cache_prefix: aws_ec2
Here is an example of setting inventory caching with some fact caching defaults for the cache plugin used and the timeout in an ``ansible.cfg`` file:

.. code-block:: ini
[defaults]
fact_caching = json
fact_caching_connection = /tmp/ansible_facts
cache_timeout = 3600
[inventory]
cache = yes
cache_connection = /tmp/ansible_inventory
.. _inventory_plugin_list:

Plugin List
Expand Down
15 changes: 15 additions & 0 deletions docs/docsite/rst/porting_guides/porting_guide_2.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,19 @@ Deprecated
removed in 2.12. If you need the old behaviour switch to ``FactCache.first_order_merge()``
instead.

* Supporting file-backed caching via self.cache is deprecated and will
be removed in Ansible 2.12. If you maintain an inventory plugin, update it to use ``self._cache`` as a dictionary. For implementation details, see
the :ref:`developer guide on inventory plugins<inventory_plugin_caching>`.

* Importing cache plugins directly is deprecated and will be removed in Ansible 2.12. Use the plugin_loader
so direct options, environment variables, and other means of configuration can be reconciled using the config
system rather than constants.

.. code-block:: python
from ansible.plugins.loader import cache_loader
cache = cache_loader.get('redis', **kwargs)
Modules
=======

Expand Down Expand Up @@ -336,6 +349,8 @@ Plugins

* ``osx_say`` callback plugin was renamed into :ref:`say <say_callback>`.

* Inventory plugins now support caching via cache plugins. To start using a cache plugin with your inventory see the section on caching in the :ref:`inventory guide<using_inventory>`. To port a custom cache plugin to be compatible with inventory see :ref:`developer guide on cache plugins<developing_cache_plugins>`.

Porting custom scripts
======================

Expand Down
2 changes: 2 additions & 0 deletions lib/ansible/inventory/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ def parse_source(self, source, cache=False):
try:
# FIXME in case plugin fails 1/2 way we have partial inventory
plugin.parse(self._inventory, self._loader, source, cache=cache)
if getattr(plugin, '_cache', None):
plugin.update_cache_if_changed()
parsed = True
display.vvv('Parsed %s inventory source with %s plugin' % (source, plugin_name))
break
Expand Down
Loading

0 comments on commit 9687879

Please sign in to comment.