DellEMC

Ansible Dynamic Inventory Tutorial

This is the first of a 3 part series on Ansible dynamic inventories. I was looking for some information about dynamic inventories as part of a project and I realized that even though there are a few articles out there, there is little detailed content specially around building your own dynamic inventory script and how to use it in Tower/AWX. So I am planning to share all the lessons I learned to help you in your own journey. This series is structured as follows:

The code for the examples shown in this series is written in Python and is available in this GitHub repo: https://github.com/cermegno/ansible-dynamic-inventory. And now, without further ado let’s start with the introduction.

Static Inventory 1 min recap

If you are reading this article more than likely you are not new to Ansible and you know what a static inventory is, but we need to spend a minute laying out the example we will use in this tutorial. Inventories can be formatted in several ways but INI is perhaps the most common. In an inventory you can define groups and children groups. The following is an example that shows 2 groups: “webprod” and “webdev”, each one with 2 target systems on it.

[webprod]
web1 http_port=123
web2 http_port=456

[webdev]
dev1
dev2

[webdev:vars]
user=admin
pass=password

You also have the ability to define variables for a group and for individual hosts. In this example we have defined a host variable for each host in the “webprod” group. However, the 2 variables we need for the “webdev” group are the same for all systems in that group so it makes sense to use group variables as shown.

Another way you can format inventory files is JSON, and as we will see next, the JSON format is specially useful when creating dynamic inventories

Understanding Dynamic Inventories

Static files like these are the most common type of Ansible inventories and as long as the information contained in them doesn’t change often this is the way to go. However there are many environments that are very dynamic in nature and a static inventory file like this becomes obsolete very quickly and it is hard to maintain. So the solution is to use a dynamic inventory.

Another consideration is that sometimes the targets themselves don’t change much but the variables (for hosts or groups) do. The use case we will play with in the second part of this tutorial is a great example of this.

So, what is a dynamic inventory? It is a script that returns the inventory information in a specific format that Ansible expects. What format is that? JSON. The following is the JSON equivalent of the static inventory we used earlier

{
    "webprod": {
        "hosts": [
            "web1",
            "web2"
        ]
    },
    "webdev": {
        "hosts": [
            "dev1",
            "dev2"
        ],
        "vars": {
            "user": "admin",
            "pass": "password"
        }
    },
    "_meta": {
        "hostvars": {
            "web1": {
                "http_port": "123"
            },
            "web2": {
                "http_port": "456"
            }
        }
    }
}

Notice how there is a top level key for each group. Each of these “group” keys can have the following keys:

  • hosts. This key contains a list of targets
  • vars. This is a dictionary that contains the variables for the group. The “webprod” didn’t have any group variables, so the “vars” key can be omitted
  • children. This key contains a list of children groups of this group. If it is not present Ansible will assume the group doesn’t have any children groups

Also notice how there is another top level key called “_meta” which includes the host variables under a key called “hostvars”.

Ansible expects the “dynamic inventory” script to implement 2 flags. Only one of these will be invoked at once:

  • “–list”. When the script is run with this flag it has to return the entire JSON structure as shown above
  • “–host”. This flag is followed by the name of a specific host in the inventory and when used your script has to return the variables for that host.

IMPORTANT: In the past the “–host” flag was the only way of getting host variables but it is very inefficient to do things when Ansible needs to get the “hostvars” from many hosts, one at a time. So in modern versions of Ansible, the preferred way of implementing this functionality with the “–list” flag is by using the “_meta” key shown above. Also, to ensure Ansible pays attention only to the “_meta” section, the “–host” flag needs to be functional but needs to return only an empty dictionary.

NOTE: If your preference is to use the “–host” flag instead your “_meta” still needs to include an empty “hostvars” dictionary as described in the documentation.

Using the ansible-inventory tool

Ansible provides a very handy tool to display and troubleshoot the inventory. The tool is called “ansible-inventory”. We can use this tool to observe how Ansible sees the inventory. You can see the full help page of the command by typing “ansible-inventory -h” but let’s play with some of the options here. Firstly we have “–list” that shows the full inventory as Ansible sees it.


[root@alb-ansible3]# ansible-inventory -i staticinv.ini --list
{
    "_meta": {
        "hostvars": {
            "dev1": {
                "pass": "password",
                "user": "admin"
            },
            "dev2": {
                "pass": "password",
                "user": "admin"
            },
            "web1": {
                "http_port": 123
            },
            "web2": {
                "http_port": 456
            }
        }
    },
    "all": {
        "children": [
            "ungrouped",
            "webdev",
            "webprod"
        ]
    },
    "webdev": {
        "hosts": [
            "dev1",
            "dev2"
        ]
    },
    "webprod": {
        "hosts": [
            "web1",
            "web2"
        ]
    }
}

There are a few things to note from the output above:

  • You need to specify the inventory file with the “-i” option. It doesn’t matter whether it is a script or a static file, you need to use the “-i” option. In this case I am using the INI inventory file we showed first. I have called it “staticinv.ini”
  • Notice how Ansible has taken the group variables of the “webdev” group and has turned them into host variables under the “_meta” section. So for us it is more efficient to use group variables but internally Ansible unfolds those into individual hosts variables. Consequently, the group keys show only the list of hosts in the group
  • Ansible has created the “all” group and has add the other groups as “children” of “all”. It has also added the “ungrouped” group. Now we understand how Ansible handles your plays when you specify “hosts: all”

So the “–list” option helps us understand many things. But it would be good if it could provide the optimal JSON version of a static INI inventory file that we can use as a template to write the code of our dynamic inventory script. Luckily for us, that option is also available. It is just a matter of adding the “–export” flag to the previous command. Please note how this is in addition to “–list” not as a replacement.

[root@alb-ansible3]# ansible-inventory -i staticinv.ini --list --export
{
    "_meta": {
        "hostvars": {
            "web1": {
                "http_port": 123
            },
            "web2": {
                "http_port": 456
            }
        }
    },
    "all": {
        "children": [
            "ungrouped",
            "webdev",
            "webprod"
        ]
    },
    "webdev": {
        "hosts": [
            "dev1",
            "dev2"
        ],
        "vars": {
            "pass": "password",
            "user": "admin"
        }
    },
    "webprod": {
        "hosts": [
            "web1",
            "web2"
        ]
    }
}

Now the variables “user” and “pass” have been turned into group variables of the “webdev”. So that’s it, if we want to create a dynamic inventory script we can start by creating a sample INI version of the inventory and use the “–export” flag to create a JSON equivalent. We then write our code to produce that on demand

Another handy option available in the “ansible-inventory” tool is “–graph”. When invoked it provides a graph representation of the inventory. Group names are prepended with “@”. Hosts in the group are shown indented under the group name

[root@alb-ansible3]# ansible-inventory -i staticinv.ini --graph
@all:
  |--@ungrouped:
  |--@webdev:
  |  |--dev1
  |  |--dev2
  |--@webprod:
  |  |--web1
  |  |--web2

Built-in inventory plugins

Ansible comes with some built-in inventory plugins to help you with some of the most common use cases. You can see what inventory plugins were installed by using the “ansible-doc” command as follows. My version of Ansible is 2.9.21 and this is what it came by default

[root@alb-ansible3]# ansible-doc -t inventory -l
advanced_host_list  Parses a 'host list' with ranges
auto                Loads and executes an inventory plugin specified in a YAML config
aws_ec2             EC2 inventory source
aws_rds             rds instance source
azure_rm            Azure Resource Manager inventory plugin
cloudscale          cloudscale.ch inventory source
constructed         Uses Jinja2 to construct vars and groups based on existing inventory
docker_machine      Docker Machine inventory source
docker_swarm        Ansible dynamic inventory plugin for Docker swarm nodes
foreman             foreman inventory source
gcp_compute         Google Cloud Compute Engine inventory source
generator           Uses Jinja2 to construct hosts and groups from patterns
gitlab_runners      Ansible dynamic inventory plugin for GitLab runners
hcloud              Ansible dynamic inventory plugin for the Hetzner Cloud
host_list           Parses a 'host list' string
ini                 Uses an Ansible INI file as inventory source
k8s                 Kubernetes (K8s) inventory source
kubevirt            KubeVirt inventory source
linode              Ansible dynamic inventory plugin for Linode
netbox              NetBox inventory source
nmap                Uses nmap to find hosts to target
online              Online inventory source
openshift           OpenShift inventory source
openstack           OpenStack inventory source
scaleway            Scaleway inventory source
script              Executes an inventory script that returns JSON
toml                Uses a specific TOML file as an inventory source
tower               Ansible dynamic inventory plugin for Ansible Tower
virtualbox          virtualbox inventory source
vmware_vm_inventory VMware Guest inventory source
vultr               Vultr inventory source
yaml                Uses a specific YAML file as an inventory source

Create your first dynamic inventory

So far we know that a dynamic inventory script needs to return a well-known JSON structure. So how do you obtain the information to populate the JSON? It depends on your use case. Ultimately, you will have a source of truth for your inventory information and you will have to interact with that source programmatically. For example, if the inventory information is on a database you will have to write a script that queries the database to find out what to put in the JSON output. Other examples could be Excel or even a text file. Nowadays it is very common to deal with systems that expose a REST API interface and all programming languages have web client libraries that allow you to talk to such API’s. In the next post we will show you an example of how to create a dynamic inventory for your Dell infrastructure by using CloudIQ’s REST API

To wrap up this post let’s show how to create the most basic dynamic inventory script how to use it . For simplicity we are going to drop the “webdev” group and create a script that returns the details of the “webprod” group only, including the “_meta” with their host variables. In my system I have created a python script called “basicinv.py” that looks as follows:

[root@alb-ansible3]# cat basicinv.py
#!/usr/bin/env python3
import json
output = {
    "_meta": {
        "hostvars": {
            "web1": {
                "http_port": 123
            },
            "web2": {
                "http_port": 456
            }
        }
    },
    "webprod": {
        "hosts": [
            "web1",
            "web2"
        ]
    }
}
print(json.dumps(output))

It is creating a dictionary with the static information and using the “json.dumps” function to dump it on the terminal. Notice how for simplicity I am not checking for “–list” or “–host” flag. So the script dumps everything every time which is the default behavior of “–list”. Hence running tasks for “all” hosts will fine but not for specific hosts. The objective was to create the script as simple as possible so we will leave the “–host” flag out in this post. In the next post of this series we will use the “argparse” library to implement both flags properly.

[root@alb-ansible3]# ansible all -i basic.py -m debug -a "var=http_port"
web2 | SUCCESS => {
    "http_port": 456
}
web1 | SUCCESS => {
    "http_port": 123
}
[root@alb-ansible3]# ansible web1 -i basic.py -m debug -a "var=http_port"
[WARNING]: Unable to parse /root/basic.py as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

The previous screenshot shows the ad-hoc “debug” running successfully against “all” hosts but not against host “web1” as expected as this would require a functional “–host” flag as explained earlier. Notice how we use the “-i” option to specify the script.

Now let’s see if it works with a playbook too. I have created the following “ping.yml” playbook

- name: Check that our targets are reachable
  hosts: webprod
  gather_facts: false

  tasks:
  - ping:

Now we run it like this. Notice above how we are targeting the “webprod” group. Using “all” works as well. If we try with an individual host like “web1” it also works, but this is only because our simple “ping.yml” doesn’t require Ansible to look for the “hostvars”. In that case we would need to make our simple dynamic inventory script award of the “–host” flag and return an empty dictionary as we discussed earlier

[root@alb-ansible3]# ansible-playbook -i basicinv.py ping.yml

PLAY [Check that our targets are reachable] *********************************************************************

TASK [ping] *******************************************************************************
ok: [web2]
ok: [web1]

PLAY RECAP *******************************************************************************
web1 : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0
web2 : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0 

Things to watch out for – Possible errors

There are 2 important things to watch out for when you create your own script:

  • Make sure your script has execute permission, otherwise you will get the error “Unable to parse /root/basic.py as an inventory source”
  • Make sure you add the shebang that matches your Pyhon version, which in my case is “!/usr/bin/env python3“. Otherwise you get the error “Failed to parse with script plugin” below. Notice how it tries to parse it as a script first and then it tries to parse it as an INI before it gives up
[root@alb-ansible3]# ansible-playbook -i basic.py ping.yml
[WARNING]:  * Failed to parse /root/basic.py with script plugin: problem running
/root/basic.py --list ([Errno 8] Exec format error: '/root/basic.py')
[WARNING]:  * Failed to parse /root/basic.py with ini plugin:
/root/basic.py:1: Expected key=value host variable assignment, got: json
[WARNING]: Unable to parse /root/basic.py as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not
match 'all'

I hope you found this introduction valuable. On the second part we will create the full-blown solution for a dynamic inventory script where we extract the data from a REST API at runtime. See you soon!

3 replies »

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.