Writing a Custom Ansible Dynamic Inventory Script

Moo

I’m writing code for a little Raspberry Pi HUD I’m running in my house, project ‘Homehud’. Naturally I manage the Raspbian system I’ve installed on it with Ansible.

One problem is that the Raspberry Pi uses a dynamic local IP, as I haven’t been able to make it use a static IP with my router. Since Ansible needs to know the IP to connect to it, this initially meant I had to manually update the inventory file with the new IP each time the Pi was rebooted, after looking it up from the connected devices list on my router’s admin page.

Of course, this can be automated.

Ansible has the ability to use a dynamic inventory rather than a static list of addresses. If the path you’ve assigned to hostfile in your ansible.cfg (or passed as -i) is executable, it will not open the file directly and instead it will execute it and interpret the output as JSON in a particular format. This means you can run whatever code you like to return your inventory, as long as you adhere to the format.

The format is documented fully in the “Developing Dynamic Inventory Sources” page in the Ansible docs, but here’s a quick sample:

{
    "all": {
        "hosts": ["address"],
        "vars": {},
    },
    "_meta": {
        "hostvars": {
            "address": {
                "variable_name": "value",
            }
        },
    },
    "group_name": ["address"]
}

At the top there’s the all group that should contain hosts, the connection addresses of all hosts (normally IP addresses for SSH), plus any global variables in the vars object. There might also be other optional global keys.

Then there’s the _meta key which can contain individual vars for each host under hostvars, mapped from host address to an object of variable name to value. There may also be other keys usable under _meta, again I’m not sure.

The rest of the top level object is a mapping from group names to lists of addresses that appear in said groups.

My script is very simple - I know the Raspberry Pi is the only device on my local network with port 22 open, so the script simply pings port 22 of each local address and when it finds a match, uses that as the single item appearing in the inventory. It does this by calling arp -a, parsing its output to find the IP’s, and then trying to open a socket on port 22 of each. The first match is used.

My full script follows. It might be useful as a reference for making your own dynamic inventory scripts, considering that it’s simpler than the fully featured cloud provider scripts included with Ansible itself. It’s written in Python 2.7 but should be fully forwards compatible with Python 3 thanks to the __future__ imports. Enjoy!

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import json
import socket
import subprocess


def main():
    print(json.dumps(inventory(), sort_keys=True, indent=2))


def inventory():
    ip_address = find_pi()

    return {
        'all': {
            'hosts': [ip_address],
            'vars': {},
        },
        '_meta': {
            'hostvars': {
                ip_address: {
                    'ansible_ssh_user': 'pi',
                }
            },
        },
        'pi': [ip_address]
    }


def find_pi():
    for ip in all_local_ips():
        if port_22_is_open(ip):
            return ip


def all_local_ips():
    lines = subprocess.check_output(['arp', '-a']).split('\n')
    for line in lines:
        if '(' not in line:
            continue
        after_open_bracket = line.split('(')[1]
        ip = after_open_bracket.split(')')[0]
        yield ip


def port_22_is_open(ip):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    result = sock.connect_ex((ip, 22))
    return result == 0


if __name__ == '__main__':
    main()

Tags: ansible