summaryrefslogtreecommitdiffstats
path: root/docs/docsite/rst/dev_guide/developing_inventory.rst
blob: 8697e943c3079172105aaa81210208ef908db59f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
.. _developing_inventory:

****************************
Developing dynamic inventory
****************************

Ansible can pull inventory information from dynamic sources, including cloud sources, by using the supplied :ref:`inventory plugins <inventory_plugins>`. For details about how to pull inventory information, see :ref:`dynamic_inventory`. If the source you want is not currently covered by existing plugins, you can create your own inventory plugin as with any other plugin type.

In previous versions, you had to create a script or program that could output JSON in the correct format when invoked with the proper arguments.
You can still use and write inventory scripts, as we ensured backwards compatibility through the :ref:`script inventory plugin <script_inventory>`
and there is no restriction on the programming language used.
If you choose to write a script, however, you will need to implement some features yourself such as caching, configuration management, dynamic variable and group composition, and so on.
If you use :ref:`inventory plugins <inventory_plugins>` instead, you can use the Ansible codebase and add these common features automatically.

.. contents:: Topics
   :local:


.. _inventory_sources:

Inventory sources
=================

Inventory sources are the input strings that inventory plugins work with.
An inventory source can be a path to a file or to a script, or it can be raw data that the plugin can interpret.

The table below shows some examples of inventory plugins and the source types that you can pass to them with ``-i`` on the command line.

+--------------------------------------------+-----------------------------------------+
|  Plugin                                    | Source                                  |
+--------------------------------------------+-----------------------------------------+
| :ref:`host list <host_list_inventory>`     | A comma-separated list of hosts         |
+--------------------------------------------+-----------------------------------------+
| :ref:`yaml <yaml_inventory>`               | Path to a YAML format data file         |
+--------------------------------------------+-----------------------------------------+
| :ref:`constructed <constructed_inventory>` | Path to a YAML configuration file       |
+--------------------------------------------+-----------------------------------------+
| :ref:`ini <ini_inventory>`                 | Path to an INI formatted data file      |
+--------------------------------------------+-----------------------------------------+
| :ref:`virtualbox <virtualbox_inventory>`   | Path to a YAML configuration file       |
+--------------------------------------------+-----------------------------------------+
| :ref:`script plugin <script_inventory>`    | Path to an executable that outputs JSON |
+--------------------------------------------+-----------------------------------------+


.. _developing_inventory_inventory_plugins:

Inventory plugins
=================

Like most plugin types (except modules), inventory plugins must be developed in Python. They execute on the controller and should therefore adhere to the :ref:`control_node_requirements`.

Most of the documentation in :ref:`developing_plugins` also applies here. You should read that document first for a general understanding and then come back to this document for specifics on inventory plugins.

Normally, inventory plugins are executed at the start of a run, and before the playbooks, plays, or roles are loaded.
However, you can use the ``meta: refresh_inventory`` task to clear the current inventory and execute the inventory plugins again, and this task will generate a new inventory.

If you use the persistent cache, inventory plugins can also use the configured cache plugin to store and retrieve data. Caching inventory avoids making repeated and costly external calls.

.. _developing_an_inventory_plugin:

Developing an inventory plugin
------------------------------

The first thing you want to do is use the base class:

.. code-block:: python

    from ansible.plugins.inventory import BaseInventoryPlugin

    class InventoryModule(BaseInventoryPlugin):

        NAME = 'myplugin'  # used internally by Ansible, it should match the file name but not required

If the inventory plugin is in a collection, the NAME should be in the 'namespace.collection_name.myplugin' format. The base class has a couple of methods that each plugin should implement and a few helpers for parsing the inventory source and updating the inventory.

After you have the basic plugin working, you can incorporate other features by adding more base classes:

.. code-block:: python

    from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable

    class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):

        NAME = 'myplugin'

For the bulk of the work in a plugin, we mostly want to deal with 2 methods ``verify_file`` and ``parse``.

.. _inventory_plugin_verify_file:

verify_file method
^^^^^^^^^^^^^^^^^^

Ansible uses this method to quickly determine if the inventory source is usable by the plugin. The determination does not need to be 100% accurate, as there might be an overlap in what plugins can handle and by default Ansible will try the enabled plugins as per their sequence.

.. code-block:: python

    def verify_file(self, path):
        ''' return true/false if this is possibly a valid file for this plugin to consume '''
        valid = False
        if super(InventoryModule, self).verify_file(path):
            # base class verifies that file exists and is readable by current user
            if path.endswith(('virtualbox.yaml', 'virtualbox.yml', 'vbox.yaml', 'vbox.yml')):
                valid = True
        return valid

In the above example, 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.

The following is another example that does not use a 'file' but the inventory source string itself,
from the :ref:`host list <host_list_inventory>` plugin:

.. code-block:: python

    def verify_file(self, path):
        ''' don't call base class as we don't expect a path, but a host list '''
        host_list = path
        valid = False
        b_path = to_bytes(host_list, errors='surrogate_or_strict')
        if not os.path.exists(b_path) and ',' in host_list:
            # the path does NOT exist and there is a comma to indicate this is a 'host list'
            valid = True
        return valid

This 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.

.. _inventory_plugin_parse:

parse method
^^^^^^^^^^^^

This method does the bulk of the work in the plugin. 
It takes the following parameters:

 * inventory: inventory object with existing data and the methods to add hosts/groups/variables to inventory
 * loader: Ansible's DataLoader. The DataLoader can read files, auto load JSON/YAML and decrypt vaulted data, and cache read files.
 * path: string with inventory source (this is usually a path, but is not required)
 * cache: indicates whether the plugin should use or avoid caches (cache plugin and/or loader)


The base class does some minimal assignment for reuse in other methods.

.. code-block:: python

       def parse(self, inventory, loader, path, cache=True):

            self.loader = loader
            self.inventory = inventory
            self.templar = Templar(loader=loader)

It is up to the plugin now to parse the provided inventory source and translate it into Ansible inventory.
To facilitate this, the example below uses a few helper functions:

.. code-block:: python

       NAME = 'myplugin'

       def parse(self, inventory, loader, path, cache=True):

            # call base method to ensure properties are available for use with other helper methods
            super(InventoryModule, self).parse(inventory, loader, path, cache)

            # this method will parse 'common format' inventory sources and
            # update any options declared in DOCUMENTATION as needed
            config = self._read_config_data(path)

            # if NOT using _read_config_data you should call set_options directly,
            # to process any defined configuration for this plugin,
            # if you don't define any options you can skip
            #self.set_options()

            # example consuming options from inventory source
            mysession = apilib.session(user=self.get_option('api_user'),
                                       password=self.get_option('api_pass'),
                                       server=self.get_option('api_server')
            )


            # make requests to get data to feed into inventory
            mydata = mysession.getitall()

            #parse data and create inventory objects:
            for colo in mydata:
                for server in mydata[colo]['servers']:
                    self.inventory.add_host(server['name'])
                    self.inventory.set_variable(server['name'], 'ansible_host', server['external_ip'])

The specifics will vary depending on API and structure returned. Remember that if you get an inventory source error or any other issue, 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 plugin, see the source code here:
`lib/ansible/plugins/inventory <https://github.com/ansible/ansible/tree/devel/lib/ansible/plugins/inventory>`_.

.. _inventory_object:

inventory object
^^^^^^^^^^^^^^^^

The ``inventory`` object passed to ``parse`` has helpful methods for populating inventory.

``add_group`` adds a group to inventory if it doesn't already exist. It takes the group name as the only positional argument.

``add_child`` adds a group or host that exists in inventory to a parent group in inventory. It takes two positional arguments, the name of the parent group and the name of the child group or host.

``add_host`` adds a host to inventory if it doesn't already exist, optionally to a specific group. It takes the host name as the first argument and accepts two optional keyword arguments, ``group`` and ``port``. ``group`` is the name of a group in inventory, and ``port`` is an integer.

``set_variable`` adds a variable to a group or host in inventory. It takes three positional arguments: the name of the group or host, the name of the variable, and the value of the variable.

To create groups and variables using Jinja2 expressions, see the section on implementing ``constructed`` features below.

To see other inventory object methods, see the source code here:
`lib/ansible/inventory/data.py <https://github.com/ansible/ansible/tree/devel/lib/ansible/inventory/data.py>`_.

.. _inventory_plugin_caching:

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

To cache the inventory, extend the inventory plugin documentation with the inventory_cache documentation fragment and use the Cacheable base class.

.. 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 plugin, you must retrieve a unique cache key by using the ``get_cache_key`` method. This task needs to be done by all inventory modules using the cache, so that 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 by the ``--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 not attempt_to_read_cache or cache_needs_update:
            # parse the provided inventory source
            results = self.get_inventory()
        if cache_needs_update:
            self._cache[cache_key] = results

        # submit the parsed data to the inventory object (add_host, set_variable, etc)
        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`` flushes the cache, ultimately by calling the cache plugin's ``flush()`` method, whose implementation is dependent upon the particular cache plugin in use. Note that if the user is using the same cache backend for facts and inventory, both will get flushed. To avoid this, the user can specify a distinct cache backend in their inventory plugin configuration.

constructed features
^^^^^^^^^^^^^^^^^^^^

Inventory plugins can create host variables and groups from Jinja2 expressions and variables by using features from the ``constructed`` inventory plugin. To do this, use the ``Constructable`` base class and extend the inventory plugin's documentation with the ``constructed`` documentation fragment.

.. code-block:: yaml

    extends_documentation_fragment:
      - constructed

.. code-block:: python

    class InventoryModule(BaseInventoryPlugin, Constructable):

        NAME = 'ns.coll.myplugin'

There are three main options in the ``constructed`` documentation fragment:

``compose`` creates variables using Jinja2 expressions. This is implemented by calling the ``_set_composite_vars`` method.
``keyed_groups`` creates groups of hosts based on variable values. This is implemented by calling the ``_add_host_to_keyed_groups`` method.
``groups`` creates groups based on Jinja2 conditionals. This is implemented by calling the ``_add_host_to_composed_groups`` method.

Each method should be called for every host added to inventory. Three positional arguments are required: the constructed option, a dictionary of variables, and a host name. Calling the method ``_set_composite_vars`` first will allow ``keyed_groups`` and ``groups`` to use the composed variables.

By default, undefined variables are ignored. This is permitted by default for ``compose`` so you can make the variable definitions depend on variables that will be populated later in a play from other sources. For groups, it allows using variables that are not always present without having to use the ``default`` filter. To support configuring undefined variables to be an error, pass the constructed option ``strict`` to each of the methods as a keyword argument.

``keyed_groups`` and ``groups`` use any variables already associated with the host (for example, from an earlier inventory source). ``_add_host_to_keyed_groups`` and ``add_host_to_composed_groups`` can turn this off by passing the keyword argument ``fetch_hostvars``.

Here is an example using all three methods:

.. code-block:: python

   def add_host(self, hostname, host_vars):
       self.inventory.add_host(hostname, group='all')

       for var_name, var_value in host_vars.items():
           self.inventory.set_variable(hostname, var_name, var_value)

       strict = self.get_option('strict')

       # Add variables created by the user's Jinja2 expressions to the host
       self._set_composite_vars(self.get_option('compose'), host_vars, hostname, strict=True)

       # Create user-defined groups using variables and Jinja2 conditionals
       self._add_host_to_composed_groups(self.get_option('groups'), host_vars, hostname, strict=strict)
       self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host_vars, hostname, strict=strict)

By default, group names created with ``_add_host_to_composed_groups()`` and ``_add_host_to_keyed_groups()`` are valid Python identifiers. Invalid characters are replaced with an underscore ``_``. A plugin can change the sanitization used for the constructed features by setting ``self._sanitize_group_name`` to a new function. The core engine also does sanitization, so if the custom function is less strict it should be used in conjunction with the configuration setting ``TRANSFORM_INVALID_GROUP_CHARS``.

.. code-block:: python

   from ansible.inventory.group import to_safe_group_name

   class InventoryModule(BaseInventoryPlugin, Constructable):

       NAME = 'ns.coll.myplugin'

       @staticmethod
       def custom_sanitizer(name):
           return to_safe_group_name(name, replacer='')

       def parse(self, inventory, loader, path, cache=True):
           super(InventoryModule, self).parse(inventory, loader, path)

           self._sanitize_group_name = custom_sanitizer

.. _inventory_source_common_format:

Common format for inventory sources
-----------------------------------

To simplify development, most plugins use a standard YAML-based configuration file as the inventory source. The file has only one required field ``plugin``, which should contain the name of the plugin that is expected to consume the file.
Depending on other common features used, you might need other fields, and you can add custom options in each plugin as required.
For example, if you use the integrated caching, ``cache_plugin``, ``cache_timeout`` and other cache-related fields could be present.

.. _inventory_development_auto:

The 'auto' plugin
-----------------

From Ansible 2.5 onwards, we include the :ref:`auto inventory plugin <auto_inventory>` and enable it by default. If the ``plugin`` field in your standard configuration file matches the name of your inventory plugin, the ``auto`` inventory plugin will load your plugin. The 'auto' plugin makes it easier to use your plugin without having to update configurations.


.. _inventory_scripts:
.. _developing_inventory_scripts:

Inventory scripts
=================

Even though we now have inventory plugins, we still support inventory scripts, not only for backwards compatibility but also to allow users to use other programming languages.


.. _inventory_script_conventions:

Inventory script conventions
----------------------------

Inventory scripts must accept the ``--list`` and ``--host <hostname>`` arguments. Although other arguments are allowed, Ansible will not use them.
Such arguments might still be useful for executing the scripts directly.

When the script is called with the single argument ``--list``, the script must output to stdout a JSON object that contains all the groups to be managed. Each group's value should be either an object containing a list of each host, any child groups, and potential group variables, or simply a list of hosts:


.. code-block:: json

    {
        "group001": {
            "hosts": ["host001", "host002"],
            "vars": {
                "var1": true
            },
            "children": ["group002"]
        },
        "group002": {
            "hosts": ["host003","host004"],
            "vars": {
                "var2": 500
            },
            "children":[]
        }

    }

If any of the elements of a group are empty, they may be omitted from the output.

When called with the argument ``--host <hostname>`` (where <hostname> is a host from above), the script must print a JSON object, either empty or containing variables to make them available to templates and playbooks. For example:

.. code-block:: json

    {
        "VAR001": "VALUE",
        "VAR002": "VALUE"
    }

Printing variables is optional. If the script does not print variables, it should print an empty JSON object.

.. _inventory_script_tuning:

Tuning the external inventory script
------------------------------------

.. versionadded:: 1.3

The stock inventory script system mentioned above works for all versions of Ansible, but calling ``--host`` for every host can be rather inefficient, especially if it involves API calls to a remote subsystem.

To avoid this inefficiency, if the inventory script returns a top-level element called "_meta", it is possible to return all the host variables in a single script execution. When this meta element contains a value for "hostvars", the inventory script will not be invoked with ``--host`` for each host. This behavior results in a significant performance increase for large numbers of hosts.

The data to be added to the top-level JSON object looks like this:

.. code-block:: text

    {

        # results of inventory script as above go here
        # ...

        "_meta": {
            "hostvars": {
                "host001": {
                    "var001" : "value"
                },
                "host002": {
                    "var002": "value"
                }
            }
        }
    }

To 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`` object.
For example:

.. code-block:: text

    {

        # results of inventory script as above go here
        # ...

        "_meta": {
            "hostvars": {}
        }
    }


.. _replacing_inventory_ini_with_dynamic_provider:

If you intend to replace an existing static inventory file with an inventory script, it must return a JSON object which contains an 'all' group that includes every host in the inventory as a member and every group in the inventory as a child. It should also include an 'ungrouped' group which contains all hosts which are not members of any other group.
A skeleton example of this JSON object is:

.. code-block:: json

   {
       "_meta": {
         "hostvars": {}
       },
       "all": {
         "children": [
           "ungrouped"
         ]
       },
       "ungrouped": {
         "children": [
         ]
       }
   }

An 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.

.. seealso::

   :ref:`developing_api`
       Python API to Playbooks and Ad Hoc Task Execution
   :ref:`developing_modules_general`
       Get started with developing a module
   :ref:`developing_plugins`
       How to develop plugins
   `AWX <https://github.com/ansible/awx>`_
       REST API endpoint and GUI for Ansible, syncs with dynamic inventory
   `Development Mailing List <https://groups.google.com/group/ansible-devel>`_
       Mailing list for development topics
   :ref:`communication_irc`
       How to join Ansible chat channels