1.. _developing_inventory:
2
3****************************
4Developing dynamic inventory
5****************************
6
7.. contents:: Topics
8   :local:
9
10As described in :ref:`dynamic_inventory`, Ansible can pull inventory information from dynamic sources,
11including cloud sources, using the supplied :ref:`inventory plugins <inventory_plugins>`.
12If the source you want is not currently covered by existing plugins, you can create your own as with any other plugin type.
13
14In previous versions you had to create a script or program that can output JSON in the correct format when invoked with the proper arguments.
15You can still use and write inventory scripts, as we ensured backwards compatibility via the :ref:`script inventory plugin <script_inventory>`
16and there is no restriction on the programming language used.
17If you choose to write a script, however, you will need to implement some features yourself.
18i.e caching, configuration management, dynamic variable and group composition, etc.
19While with :ref:`inventory plugins <inventory_plugins>` you can leverage the Ansible codebase to add these common features.
20
21
22.. _inventory_sources:
23
24Inventory sources
25=================
26
27Inventory sources are strings (i.e what you pass to ``-i`` in the command line),
28they can represent a path to a file/script or just be the raw data for the plugin to use.
29Here are some plugins and the type of source they use:
30
31+--------------------------------------------+---------------------------------------+
32|  Plugin                                    | Source                                |
33+--------------------------------------------+---------------------------------------+
34| :ref:`host list <host_list_inventory>`     | A comma separated list of hosts       |
35+--------------------------------------------+---------------------------------------+
36| :ref:`yaml <yaml_inventory>`               | Path to a YAML format data file       |
37+--------------------------------------------+---------------------------------------+
38| :ref:`constructed <constructed_inventory>` | Path to a YAML configuration file     |
39+--------------------------------------------+---------------------------------------+
40| :ref:`ini <ini_inventory>`                 | Path to an INI formatted data file    |
41+--------------------------------------------+---------------------------------------+
42| :ref:`virtualbox <virtualbox_inventory>`   | Path to a YAML configuration file     |
43+--------------------------------------------+---------------------------------------+
44| :ref:`script plugin <script_inventory>`    | Path to an executable outputting JSON |
45+--------------------------------------------+---------------------------------------+
46
47
48.. _developing_inventory_inventory_plugins:
49
50Inventory plugins
51=================
52
53Like most plugin types (except modules) they must be developed in Python, since they execute on the controller they should match the same requirements :ref:`control_node_requirements`.
54
55Most of the documentation in :ref:`developing_plugins` also applies here, so as to not repeat ourselves, you should read that document first and we'll include inventory plugin specifics next.
56
57Inventory plugins normally only execute at the start of a run, before playbooks/plays and roles are loaded,
58but they can be 're-executed' via the ``meta: refresh_inventory`` task, which will clear out the existing inventory and rebuild it.
59
60When using the 'persistent' cache, inventory plugins can also use the configured cache plugin to store and retrieve data to avoid costly external calls.
61
62.. _developing_an_inventory_plugin:
63
64Developing an inventory plugin
65------------------------------
66
67The first thing you want to do is use the base class:
68
69.. code-block:: python
70
71    from ansible.plugins.inventory import BaseInventoryPlugin
72
73    class InventoryModule(BaseInventoryPlugin):
74
75        NAME = 'myplugin'  # used internally by Ansible, it should match the file name but not required
76
77If the inventory plugin is in a collection the NAME should be in the format of 'namespace.collection_name.myplugin'.
78
79This class has a couple of methods each plugin should implement and a few helpers for parsing the inventory source and updating the inventory.
80
81After you have the basic plugin working you might want to to incorporate other features by adding more base classes:
82
83.. code-block:: python
84
85    from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
86
87    class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
88
89        NAME = 'myplugin'
90
91For the bulk of the work in the plugin, We mostly want to deal with 2 methods ``verify_file`` and ``parse``.
92
93.. _inventory_plugin_verify_file:
94
95verify_file
96^^^^^^^^^^^
97
98This method is used by Ansible to make a quick determination if the inventory source is usable by the plugin. It does not need to be 100% accurate as there might be overlap in what plugins can handle and Ansible will try the enabled plugins (in order) by default.
99
100.. code-block:: python
101
102    def verify_file(self, path):
103        ''' return true/false if this is possibly a valid file for this plugin to consume '''
104        valid = False
105        if super(InventoryModule, self).verify_file(path):
106            # base class verifies that file exists and is readable by current user
107            if path.endswith(('virtualbox.yaml', 'virtualbox.yml', 'vbox.yaml', 'vbox.yml')):
108                valid = True
109        return valid
110
111In this case, from the :ref:`virtualbox inventory plugin <virtualbox_inventory>`, we screen for specific file name patterns to avoid attempting to consume any valid yaml file. You can add any type of condition here, but the most common one is 'extension matching'. If you implement extension matching for YAML configuration files the path suffix <plugin_name>.<yml|yaml> should be accepted. All valid extensions should be documented in the plugin description.
112
113Another example that actually does not use a 'file' but the inventory source string itself,
114from the :ref:`host list <host_list_inventory>` plugin:
115
116.. code-block:: python
117
118    def verify_file(self, path):
119        ''' don't call base class as we don't expect a path, but a host list '''
120        host_list = path
121        valid = False
122        b_path = to_bytes(host_list, errors='surrogate_or_strict')
123        if not os.path.exists(b_path) and ',' in host_list:
124            # the path does NOT exist and there is a comma to indicate this is a 'host list'
125            valid = True
126        return valid
127
128This method is just to expedite the inventory process and avoid unnecessary parsing of sources that are easy to filter out before causing a parse error.
129
130.. _inventory_plugin_parse:
131
132parse
133^^^^^
134
135This method does the bulk of the work in the plugin.
136
137It takes the following parameters:
138
139 * inventory: inventory object with existing data and the methods to add hosts/groups/variables to inventory
140 * loader: Ansible's DataLoader. The DataLoader can read files, auto load JSON/YAML and decrypt vaulted data, and cache read files.
141 * path: string with inventory source (this is usually a path, but is not required)
142 * cache: indicates whether the plugin should use or avoid caches (cache plugin and/or loader)
143
144
145The base class does some minimal assignment for reuse in other methods.
146
147.. code-block:: python
148
149       def parse(self, inventory, loader, path, cache=True):
150
151        self.loader = loader
152        self.inventory = inventory
153        self.templar = Templar(loader=loader)
154
155It is up to the plugin now to deal with the inventory source provided and translate that into the Ansible inventory.
156To facilitate this, the example below uses a few helper functions:
157
158.. code-block:: python
159
160       NAME = 'myplugin'
161
162       def parse(self, inventory, loader, path, cache=True):
163
164            # call base method to ensure properties are available for use with other helper methods
165            super(InventoryModule, self).parse(inventory, loader, path, cache)
166
167            # this method will parse 'common format' inventory sources and
168            # update any options declared in DOCUMENTATION as needed
169            config = self._read_config_data(path)
170
171            # if NOT using _read_config_data you should call set_options directly,
172            # to process any defined configuration for this plugin,
173            # if you don't define any options you can skip
174            #self.set_options()
175
176            # example consuming options from inventory source
177            mysession = apilib.session(user=self.get_option('api_user'),
178                                       password=self.get_option('api_pass'),
179                                       server=self.get_option('api_server')
180            )
181
182
183            # make requests to get data to feed into inventory
184            mydata = mysession.getitall()
185
186            #parse data and create inventory objects:
187            for colo in mydata:
188                for server in mydata[colo]['servers']:
189                    self.inventory.add_host(server['name'])
190                    self.inventory.set_variable(server['name'], 'ansible_host', server['external_ip'])
191
192The 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.
193
194For examples on how to implement an inventory plugin, see the source code here:
195`lib/ansible/plugins/inventory <https://github.com/ansible/ansible/tree/devel/lib/ansible/plugins/inventory>`_.
196
197.. _inventory_plugin_caching:
198
199inventory cache
200^^^^^^^^^^^^^^^
201
202Extend the inventory plugin documentation with the inventory_cache documentation fragment and use the Cacheable base class to have the caching system at your disposal.
203
204.. code-block:: yaml
205
206    extends_documentation_fragment:
207      - inventory_cache
208
209.. code-block:: python
210
211    class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
212
213        NAME = 'myplugin'
214
215Next, 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``.
216
217.. code-block:: python
218
219    NAME = 'myplugin'
220
221    def parse(self, inventory, loader, path, cache=True):
222        super(InventoryModule, self).parse(inventory, loader, path)
223
224        self.load_cache_plugin()
225
226Before 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.
227
228.. code-block:: python
229
230    def parse(self, inventory, loader, path, cache=True):
231        super(InventoryModule, self).parse(inventory, loader, path)
232
233        self.load_cache_plugin()
234        cache_key = self.get_cache_key(path)
235
236Now 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.
237
238.. code-block:: python
239
240    def parse(self, inventory, loader, path, cache=True):
241        super(InventoryModule, self).parse(inventory, loader, path)
242
243        self.load_cache_plugin()
244        cache_key = self.get_cache_key(path)
245
246        # cache may be True or False at this point to indicate if the inventory is being refreshed
247        # get the user's cache option too to see if we should save the cache if it is changing
248        user_cache_setting = self.get_option('cache')
249
250        # read if the user has caching enabled and the cache isn't being refreshed
251        attempt_to_read_cache = user_cache_setting and cache
252        # update if the user has caching enabled and the cache is being refreshed; update this value to True if the cache has expired below
253        cache_needs_update = user_cache_setting and not cache
254
255        # attempt to read the cache if inventory isn't being refreshed and the user has caching enabled
256        if attempt_to_read_cache:
257            try:
258                results = self._cache[cache_key]
259            except KeyError:
260                # This occurs if the cache_key is not in the cache or if the cache_key expired, so the cache needs to be updated
261                cache_needs_update = True
262
263        if cache_needs_updates:
264            results = self.get_inventory()
265
266            # set the cache
267            self._cache[cache_key] = results
268
269        self.populate(results)
270
271After 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.
272
273You have three other cache methods available:
274  - ``set_cache_plugin`` forces the cache plugin to be set with the contents of ``self._cache`` before the ``parse`` method completes
275  - ``update_cache_if_changed`` sets the cache plugin only if ``self._cache`` has been modified before the ``parse`` method completes
276  - ``clear_cache`` deletes the keys in ``self._cache`` from your cache plugin
277
278.. _inventory_source_common_format:
279
280Inventory source common format
281------------------------------
282
283To simplify development, most plugins use a mostly standard configuration file as the inventory source, YAML based and with just one required field ``plugin`` which should contain the name of the plugin that is expected to consume the file.
284Depending on other common features used, other fields might be needed, but each plugin can also add its own custom options as needed.
285For example, if you use the integrated caching, ``cache_plugin``, ``cache_timeout`` and other cache related fields could be present.
286
287.. _inventory_development_auto:
288
289The 'auto' plugin
290-----------------
291
292Since Ansible 2.5, we include the :ref:`auto inventory plugin <auto_inventory>` enabled by default, which itself just loads other plugins if they use the common YAML configuration format that specifies a ``plugin`` field that matches an inventory plugin name, this makes it easier to use your plugin w/o having to update configurations.
293
294
295.. _inventory_scripts:
296.. _developing_inventory_scripts:
297
298Inventory scripts
299=================
300
301Even though we now have inventory plugins, we still support inventory scripts, not only for backwards compatibility but also to allow users to leverage other programming languages.
302
303
304.. _inventory_script_conventions:
305
306Inventory script conventions
307----------------------------
308
309Inventory scripts must accept the ``--list`` and ``--host <hostname>`` arguments, other arguments are allowed but Ansible will not use them.
310They might still be useful for when executing the scripts directly.
311
312When the script is called with the single argument ``--list``, the script must output to stdout a JSON-encoded hash or
313dictionary containing all of the groups to be managed.
314Each group's value should be either a hash or dictionary containing a list of each host, any child groups,
315and potential group variables, or simply a list of hosts::
316
317
318    {
319        "group001": {
320            "hosts": ["host001", "host002"],
321            "vars": {
322                "var1": true
323            },
324            "children": ["group002"]
325        },
326        "group002": {
327            "hosts": ["host003","host004"],
328            "vars": {
329                "var2": 500
330            },
331            "children":[]
332        }
333
334    }
335
336If any of the elements of a group are empty they may be omitted from the output.
337
338When called with the argument ``--host <hostname>`` (where <hostname> is a host from above), the script must print either an empty JSON hash/dictionary, or a hash/dictionary of variables to make available to templates and playbooks. For example::
339
340
341    {
342        "VAR001": "VALUE",
343        "VAR002": "VALUE",
344    }
345
346Printing variables is optional. If the script does not do this, it should print an empty hash or dictionary.
347
348.. _inventory_script_tuning:
349
350Tuning the external inventory script
351------------------------------------
352
353.. versionadded:: 1.3
354
355The stock inventory script system detailed above works for all versions of Ansible,
356but calling ``--host`` for every host can be rather inefficient,
357especially if it involves API calls to a remote subsystem.
358
359To avoid this inefficiency, if the inventory script returns a top level element called "_meta",
360it is possible to return all of the host variables in one script execution.
361When this meta element contains a value for "hostvars",
362the inventory script will not be invoked with ``--host`` for each host.
363This results in a significant performance increase for large numbers of hosts.
364
365The data to be added to the top level JSON dictionary looks like this::
366
367    {
368
369        # results of inventory script as above go here
370        # ...
371
372        "_meta": {
373            "hostvars": {
374                "host001": {
375                    "var001" : "value"
376                },
377                "host002": {
378                    "var002": "value"
379                }
380            }
381        }
382    }
383
384To satisfy the requirements of using ``_meta``, to prevent ansible from calling your inventory with ``--host`` you must at least populate ``_meta`` with an empty ``hostvars`` dictionary.
385For example::
386
387    {
388
389        # results of inventory script as above go here
390        # ...
391
392        "_meta": {
393            "hostvars": {}
394        }
395    }
396
397
398.. _replacing_inventory_ini_with_dynamic_provider:
399
400If you intend to replace an existing static inventory file with an inventory script,
401it must return a JSON object which contains an 'all' group that includes every
402host in the inventory as a member and every group in the inventory as a child.
403It should also include an 'ungrouped' group which contains all hosts which are not members of any other group.
404A skeleton example of this JSON object is:
405
406.. code-block:: json
407
408   {
409       "_meta": {
410         "hostvars": {}
411       },
412       "all": {
413         "children": [
414           "ungrouped"
415         ]
416       },
417       "ungrouped": {
418         "children": [
419         ]
420       }
421   }
422
423An easy way to see how this should look is using :ref:`ansible-inventory`, which also supports ``--list`` and ``--host`` parameters like an inventory script would.
424
425.. seealso::
426
427   :ref:`developing_api`
428       Python API to Playbooks and Ad Hoc Task Execution
429   :ref:`developing_modules_general`
430       Get started with developing a module
431   :ref:`developing_plugins`
432       How to develop plugins
433   `AWX <https://github.com/ansible/awx>`_
434       REST API endpoint and GUI for Ansible, syncs with dynamic inventory
435   `Development Mailing List <https://groups.google.com/group/ansible-devel>`_
436       Mailing list for development topics
437   `irc.libera.chat <https://libera.chat/>`_
438       #ansible IRC chat channel
439