Ansible Dynamic Inventory Using Plugins

The inventory is a critical component of every automation framework. Most frameworks support multiple options to provide the inventory, either by using a local file or by integrating with third party systems that will dynamically provide the inventory. Ansible supports multiple inventory formats; in this blog post, we’ll start by taking a low resolution look at static inventory, then take a look at a dynamic inventory. In a future blog post, we’ll jump into how to construct an Ansible inventory plugin in order to use a custom API endpoint as an inventory source.

Inventory Structure

In Ansible, the inventory is primarily composed of hosts, that can be organized in groups with each group or host having associated variables. A host represents the entity to automate, usually a host represents a server, a VM, or a network device, but it could be anything that needs to be automated (a VIP, a rack, SDN system, etc.). By default, the tasks defined in a playbook will be executed per host in the inventory. A host can be part of multiple groups and the groups can be organized with an optional hierarchy. Variables are properties of a given host. For instance, a host CORE-SW01 will have a hostname that describes it, a management IP, SNMP servers which it uses, etc. These properties may be specific to a single host, or they may be shared by a group of hosts.
You can specify the variables associated with a given host in the inventory file or by using a pre-defined directory structure. Ansible searches for host and group variable files by searching the host_vars and group_vars directories when a static inventory source is used. It looks for these directories by searching for them relative to the inventory or playbook file being used.

If the same variable is defined in multiple places (e.g. as an attribute of a host and as an attribute of a group), Ansible will use the following hierarchy to decide which variable to use when it parses inventory sources:

1) Use the variable associated at the host level 2) Use the variable associated at the group level 3) Use the variable associated at the all group level

More detail about how to build your inventory can be found here

Locate the Inventory

By default, Ansible leverages local host file in /etc/ansible/hosts to construct an inventory of devices against which playbooks can be executed. The use of a different inventory file (or files) can be specified in one of several ways; the environment variable ANSIBLE_INVENTORY can be set, the ini key inventory in the defaults section of your Ansible config file can be specified, the default path can be relied on, or the -i parameter can be passed at run time. The order of precedence to resolve which inventory declaration is used if multiple are defined can be found here

Dynamic Inventory

The method for defining inventory discussed up to this point is called static inventory. dynamic inventory can also be defined. Dynamic inventory is useful in an environment where the local inventory files aren’t the source of truth for device inventory. For instance, if you define your hosts in NetBox or in a ServiceNow CMDB, rather than replicating these inventory sources to your local host’s static files, a dynamic inventory allows you to pull inventory directly from NetBox, ServiceNow, or a myriad of other tools at run time (it can also be cached).

Ansible supports two forms of dynamic inventories: inventory plugins and inventory scripts. Inventory plugins are the recommended way of interacting with a dynamic inventory source, so we will discuss only inventory plugins here.

Using NetBox Inventory Plugin

There are multiple inventory plugins available for Ansible, most are disabled by default. To understand how plugins work, we’ll use the NetBox inventory plugin that is currently shipping with Ansible.

To use an existing inventory plugin, we need to generate a configuration file (in YAML format) at the root of the project, that will indicate the name of the plugin that we want to use (plugin: netbox). The configuration file usually contains parameters specific to each plugin that will help to define how to connect to the remote system.

For the NetBox inventory plugin, the mandatory information to provide is the api_endpoint and the token; both can be provided either via the configuration file or via environment variables.

# netbox_inventory.yml
plugin: netbox
validate_certs: False
api_endpoint: http://localhost:8000
token: 0123456abcdef0123456789abcdef01234567

netbox_inventory.yml can now be used as our inventory and can be referenced in ansible configuration file to be used by default.

# ansible.cfg
inventory = netbox_inventory.yml

To be able to automatically determine if the inventory file is a static file or a configuration file for a plugin, Ansible is configured by default with the following list of default inventory plugins:

inventory_plugin_defaults

As you can see, one of the default “accepted” plugins is auto. The auto inventory plugin is used to parse an inventory file which references a plugin using the plugin attribute.

Note: Starting in Ansible 2.4 (circa July 2018), Ansible made inventory plugins available as part of its default installation. Starting in Ansible 2.10 (still in development as of this writing), Ansible is undergoing a significant restructure. Most of the plugins included by default with Ansible, including the NetBox plugin, will be moving to Ansible collections.

Querying NetBox as a dynamic inventory source

I’ve set up a NetBox instance on my local host using netbox-community/netbox-docker, which can be found here. This allows me to build a dockerized instance of NetBox on my local computer for testing purposes. After spinning up the instance of NetBox, I can navigate to http://localhost:8000, log in with the credentials admin/admin, and create some devices for testing with the Ansible dynamic inventory. I’ve created a mock-up WAN aggregation router:

netbox_devices_1

After building the device in NetBox, the following command can be run from the directory where the aforementioned ansible.cfg and netbox_inventory.yml files are located:

$ ansible-inventory --list

This effectively uses the inventory specified in the ansible.cfg file to pull from the local NetBox instance, which is reachable at http://localhost:8000. An example what is return is as follows:

{
    "_meta": {
        "hostvars": {
            "HQ-WAN-RT01": {
                "device_roles": [
                    "wan_aggregation_router"
                ],
                "device_types": [
                    "CISCO3945"
                ],
                "manufacturers": [
                    "Cisco"
                ],
                "platforms": [
                    "cisco_ios"
                ],
                "sites": [
                    "HQ"
                ]
            }
        }
    },
    "all": {
        "children": [
            "ungrouped"
        ]
    },
    "ungrouped": {
        "hosts": [
            "HQ-WAN-RT01"
        ]
    }
}

I’ve gone ahead and added a primary IP address to the HQ-WAN-RT01 router and changed it’s role to just ‘WAN Router’ (within the NetBox UI) as a 3945 is a little bit piddly as a WAN Agg ;). After doing so, I’ve re-run the ansible-inventory --list command.

{
    "_meta": {
        "hostvars": {
            "HQ-WAN-RT01": {
                "ansible_host": "192.0.2.1",
                "device_roles": [
                    "wan_router"
                ],
                "device_types": [
                    "CISCO3945"
                ],
                "manufacturers": [
                    "Cisco"
                ],
                "platforms": [
                    "cisco_ios"
                ],
                "primary_ip4": "192.0.2.1",
                "sites": [
                    "HQ"
                ]
            }
        }
    },
    "all": {
        "children": [
            "ungrouped"
        ]
    },
    "ungrouped": {
        "hosts": [
            "HQ-WAN-RT01"
        ]
    }
}

As you can see, the device now has an ansible_host value of 192.0.2.1 and a primary_ip4 value of the same. Additionally, the device_roles value returned a list containing a single element, wan_router instead of wan_aggregation_router as before.

Grouping devices from NetBox dynamic inventory

You can instruct the Ansible Netbox Inventory Plugin to take devices pulled from NetBox and add them to dynamically constructed groups. I’ve added the following lines to the netbox_inventory.yml file:

group_by:
  - device_roles
  - platforms

I also added a device, HQ-CORE-SW to NetBox and assigned it a role of “core_switch”. After doing so, ansible-inventory --list shows the router in two groups, device_roles_wan_router and platform_cisco_ios. The newly added core switch is also in two groups, device_roles_core_switch and platform_cisco_ios.

{
    "_meta": {
        "hostvars": {
            "HQ-CORE-SW": {
                "device_roles": [
                    "core_switch"
                ],
                "device_types": [
                    "WS-C4500X-32SFP+"
                ],
                "manufacturers": [
                    "Cisco"
                ],
                "platforms": [
                    "cisco_ios"
                ],
                "sites": [
                    "HQ"
                ]
            },
            "HQ-WAN-RT01": {
                "ansible_host": "192.0.2.1",
                "device_roles": [
                    "wan_router"
                ],
                "device_types": [
                    "CISCO3945"
                ],
                "manufacturers": [
                    "Cisco"
                ],
                "platforms": [
                    "cisco_ios"
                ],
                "primary_ip4": "192.0.2.1",
                "sites": [
                    "HQ"
                ]
            }
        }
    },
    "all": {
        "children": [
            "device_roles_core_switch",
            "device_roles_wan_router",
            "platforms_cisco_ios",
            "ungrouped"
        ]
    },
    "device_roles_core_switch": {
        "hosts": [
            "HQ-CORE-SW"
        ]
    },
    "device_roles_wan_router": {
        "hosts": [
            "HQ-WAN-RT01"
        ]
    },
    "platforms_cisco_ios": {
        "hosts": [
            "HQ-CORE-SW",
            "HQ-WAN-RT01"
        ]
    }
}

Having a dynamic inventory with these dynamically constructed groups allows the user to run playbooks against only a single group of devices. For instance, the user could run a playbook against only core_switches or only wan_routers, the user could also run a playbook against all ios_devices. More ways of grouping devices using the NetBox inventory plugin exist. For more information on the possible ways you can render a NetBox inventory plugin file to group hosts, see the documentation

Specifying Variables in Dynamic Inventory

When interacting with static inventory sources, Ansible dynamically squashes host data from groups it belongs to. For instance, if HQ-CORE-SW belongs to the core_switch group, and the core_switch group has a variable defined of snmp_server=192.0.2.5, Ansible will render the inventory such that HQ-CORE-SW has a variable defined of snmp_server=192.0.2.5. This is assuming that HQ-CORE-SW does not have a snmp_server value defined at the host level, which would over ride the group level definition.

Unlike static inventory, when dynamic inventory in use, no such squashing occurs on the host running Ansible. This squashing is done on the device being used as an inventory source instead. Hierarchical understanding of variables can still be implemented, it just needs to be done on the dynamic inventory source Ansible is querying.

Specifying Variables in Netbox

NetBox allows for the definition of variables, which can be associated with hosts. You can add variables to a host by navigating to the device in NetBox, then clicking edit and adding “Local Config Context Data” in JSON format.

local_config_context_data

After doing so, that data will be included when dynamic inventory is rendered from NetBox. That is, when ansible-inventory --list is run after the above data has been entered, Ansible includes the {“foo”: “bar”} key value pair associated with HQ-CORE-SW:

{
    "_meta": {
        "hostvars": {
            "HQ-CORE-SW": {
                "device_roles": [
                    "core_switch"
                ],
                "device_types": [
                    "WS-C4500X-32SFP+"
                ],
                "foo": "bar",     # <--------
                "manufacturers": [
                    "Cisco"
                ],
                "platforms": [
                    "cisco_ios"
                ],
                "sites": [
                    "HQ"
                ]
            },
            "HQ-WAN-RT01": {
                "ansible_host": "192.0.2.1",
                "device_roles": [
                    "wan_router"
                ],
                "device_types": [
                    "CISCO3945"
                ],
                "manufacturers": [
                    "Cisco"
                ],
                "platforms": [
                    "cisco_ios"
                ],
                "primary_ip4": "192.0.2.1",
                "sites": [
                    "HQ"
                ]
            }
        }
    },
    "all": {
        "children": [
            "device_roles_core_switch",
            "device_roles_wan_router",
            "platforms_cisco_ios",
            "ungrouped"
        ]
    },
    "device_roles_core_switch": {
        "hosts": [
            "HQ-CORE-SW"
        ]
    },
    "device_roles_wan_router": {
        "hosts": [
            "HQ-WAN-RT01"
        ]
    },
    "platforms_cisco_ios": {
        "hosts": [
            "HQ-CORE-SW",
            "HQ-WAN-RT01"
        ]
    }
}

Conclusion

This is the first blog post on Ansible dynamic inventory. In a future blog post, we’ll look at how a custom NetBox inventory plugin can be constructed to query a custom dynamic inventory source (e.g. a custom API endpoint, or one for which no plugin yet exists).

-Phillip



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Author