Writing a Dynamic Inventory Script for Ansible

Posted on Tue 09 May 2017 in automation

One of the cool features of Ansible is the ability to separate out your hosts in different groups in an inventory file. For example, you can have a webserver group, a database group, and a proxy group.

[webserver]
webserv01
webserv02
webserv03
webserv04
webserv05

[database]
postgres01
postgres02
oracle01
oracle02
influxdb01

[proxy]
134.72.87.5
134.72.86.5
134.72.91.5

This allows your playbooks to target one or more groups at a time, while keeping a comprehensive list of all your servers (and you can also have sub-groups, attach variables to different hosts, and other cool stuff)

But what if your hostnames or IPs are more ephemeral? Maybe this morning you’d have webserv01-webserv05 but this afternoon you might have webserv09-webserv13 or some other random combination. You might have some method of gathering the host information, then you could update the hosts file manually or even have some script that does it. But if your hosts are changing all the time, why even worry about keeping a static file updated? Isn’t there some way to just tell Ansible at runtime what hosts to act on?

Well yes there is.

Ansible allows you to pass a script as a parameter to the inventory flag (-i) when running the ansible-playbook command. This means you could run

ansible-playbook playbooks/build_everything.yml -i my_inventory_script.py

and instead of reading from a hosts file, it will read the output (a JSON object) from your script use that as the hosts list, overriding any hosts files you might have specified in ansible.cfg or via environment variables.

Example

Here’s an example script I wrote that pulls in host info from a dhcp server’s lease file, and plugs that in to Ansible. Some notes on this setup:

  • The DHCP service is dnsmasq. This is a lightweight, easy to setup DHCP server (among other things) that stores its DHCP leases in a file that by default is located at /var/lib/misc/dnsmasq.leases.
  • I’m running Ansible from the machine that DHCP is running on
  • The playbook is looking for a host group called “idrac” (it’s for managing Dell servers)
  • I also wanted to use the same script to tell me how many hosts matched. I’m using it to build out a frame of new Dell R630s, so I need to make sure that all of them have received IP addresses before running the playbook. I get the count by issuing a -c which will just spit out the number of idrac hosts in the leases file. I’ve included it here just to demonstrate that your dynamic inventory script can do other things as well, which means you can repurpose an existing script you might have to work with Ansible.

Now that all of that is out of the way, here’s the script:

#!/usr/bin/env python

import subprocess
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-c', '--count', action='store_true', required=False, dest='count')
parser.add_argument('--list', action='store_true', required=False, dest='list')
args = parser.parse_args()

bash_out = subprocess.Popen("grep idrac /var/lib/misc/dnsmasq.leases | awk '{print $3}'", shell=True, stdout=subprocess.PIPE).stdout.read()

servers = {
    'idrac': {
        'hosts': []
    },
    'local': {
        'hosts': ['127.0.0.1']
    }
}

# Added some logic to account for newer versions of Ansible formatting newlines differently
if '\\n' in bash_out:
    bash_out_list = bash_out.split('\\n')
else:
    bash_out_list = bash_out.split('\n')

# bash_out_list = str(bash_out).split('\n')

server_list = []

for line in bash_out_list:
    server_list.append(line.replace('\'', '').replace("b", ''))

# exception catch in case there aren't any empty lines
try:
    server_list.remove('')
except ValueError:
    pass

for server in server_list:
    servers['idrac']['hosts'].append(server)

if args.list:
    print(servers)
else:
    print(len(servers['idrac']['hosts']))

Here’s what is going on:

  1. The bash_out = line is just making a subprocess call to bash greps for the idrac hosts from the leases file, then awks out the column that actually lists the IP addresses.
  2. Creating the servers dictionary makes the structure that is necessary for Ansible to parse it. Basically, your top keys will be the server groups, and their values will be a list of all the hosts for that group. There are some more complex things you can do, along the lines of setting up host variables and things like that, but I’ll cover that in a later post.
  3. bash_out_list split’s the long string into a list separated by the newline character (\n).
  4. server_list is the list variable I’ll use to store the cleaned up contents of bash_out_list.
  5. The first for loop cleans up the bash output, removing any extraneous characters that can show up, and adds those cleaned up elements to server_list.
  6. Then, remove any blank lines rom server_list.
  7. FInally, we can start building the list of hosts in the idrac group. We just iterate over the hosts in server_list, and append them to idrac hosts list in the servers dictionary.
  8. Next, there’s some logic to determine if the script is being used to supply an inventory to Ansible or just reporting a count of all the idrac hosts with DHCP addresses:
    • If the -c flag was specified, which is saved as True for the boolean variable count, it will merely print out the result of finding the length of the idrac hosts list.
    • If the command didn’t use any flags, it will simply print out the servers dictionary. Ansible expects the JSON format, which fortunately in Python dictionaries are pretty much the same as JSON. But if you were using a different language, know that you’ll need to format it into JSON somehow.
    • I said that if no flags are specified the script will just print out the servers dictionary. This is partly true, in that Ansible actually will specify the argument —list. However it’s not important to the actual logic of this script, it just needs to be an argument that argparse recognizes, otherwise it will error out.

And that’s it for the script. You’d run it just like my example at the beginning of the post, along with any other arguments needed for your run. This is a simple example but but it allows you to not need to perform any steps like updating a hosts file before running your playbook. This could be really useful in other cases where you might be running a playbook based off a cron job or similar scheduler, as it could just pull in the inventory dynamically without any intervention. And lots of other times. Note that there are modules like this available for the major cloud providers like AWS, OpenStack, Zabbix, and others that would obviously be preferred if you’re using those environments. But it can be really handy to know how to roll your own for your own use cases.

Go Forth and Ansible

Hopefully this example will be of use to you. If you want to learn more about what you can do with dynamic inventories in Ansible, check out the links below from Ansible’s documentation. There is a ton of flexibility with Ansible’s inventory (using both static and dynamic, static groups of dynamic inventories, etc.) so most things you could think of doing would probably be possible. Check out the documentation, and definitely comment here to show off how you use dynamic inventories.

Dynamic Inventory - What you can do with dynamic inventories

Developing Dynamic Inventory Sources - What we did, making our own sources