From charlesreid1

This page covers how to manage a dynamic inventory file when using Vagrant boxes. This avoids the need to edit a hosts file by hand.


Static vs dynamic inventory

Ansible/Vagrant/Static Inventory - static inventory requires the hosts file (containing the list of machines that Ansible is managing) be kept up to date by hand. This can be a burden if details are changing or if things are allocated automatically.

Ansible/Vagrant/Dynamic Inventory - dynamic inventory uses something like an API or a database to obtain information about the machines that Ansible is managing. This makes scaling and generalization much easier.

Managing a dynamic inventory file for Vagrant boxes

The following script is a dynamic inventory script run by Ansible. This example script is written in Python. It takes two input arguments, --host=X and --list, and interacts with Vagrant to return the requested information about the managed hosts to Ansible.

Getting List of Hosts

Start with how to get a list of hosts for the --list flag.

Vagrant provides a command to show info about all running vagrant machines:

vagrant status

This info can be used to create the list of hosts for the dynamic inventory file. A more convenient format for parsing is:

vagrant status --machine-readable

which prints everything in CSV format. The code:


def list_running_hosts():
    cmd = "vagrant status --machine-readable"
    status = subprocess.check_output(cmd.split()).rstrip().decode('utf-8')
    hosts = []
    for line in status.split('\n'):
        (_, host, key, value) = line.split(',')[:4]
        if key == 'state' and value == 'running':
            hosts.append(host)
    return hosts

Getting Machine Configuration Details

Now we need to get host-specific details for the --host=X flag.

Next we can use the vagrant ssh-config command to get info about the machine-specific configuration details of each machine, which is necessary for Ansible to connect to them. To parse SSH config files, we can use a Python library called pamiko.

def get_host_details(host):
    cmd = "vagrant ssh-config {}".format(host)
    p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
    config = paramiko.SSHConfig()
    config.parse(p.stdout)
    c = config.lookup(host)
    return {'ansible_host': c['hostname'],
            'ansible_port': c['port'],
            'ansible_user': c['user'],
            'ansible_private_key_file': c['identityfile'][0]}

Parsing user arguments

A quick argparser tutorial for the flags we're interested in parsing:

def parse_args():
    parser = argparse.ArgumentParser(description="Vagrant inventory script")
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--list', action='store_true')
    group.add_argument('--host')
    return parser.parse_args()

Now we can call this function and use args.list or args.host to access the values of the flags.

Putting it all together

Here is the final dynamic inventory script:

(In the ansible repo under contrib/inventory: https://github.com/ansible/ansible/tree/devel/contrib/inventory)

dynamic_vagrant.py:

#!/usr/bin/env python
# Adapted from Mark Mandel's implementation
# https://github.com/ansible/ansible/blob/stable-2.1/contrib/inventory/vagrant.py
# License: GNU General Public License, Version 3 <http://www.gnu.org/licenses/>
import argparse
import json
import paramiko
import subprocess
import sys


def parse_args():
    parser = argparse.ArgumentParser(description="Vagrant inventory script")
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--list', action='store_true')
    group.add_argument('--host')
    return parser.parse_args()


def list_running_hosts():
    cmd = "vagrant status --machine-readable"
    status = subprocess.check_output(cmd.split()).rstrip().decode('utf-8')
    hosts = []
    for line in status.split('\n'):
        (_, host, key, value) = line.split(',')[:4]
        if key == 'state' and value == 'running':
            hosts.append(host)
    return hosts


def get_host_details(host):
    cmd = "vagrant ssh-config {}".format(host)
    p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
    config = paramiko.SSHConfig()
    config.parse(p.stdout)
    c = config.lookup(host)
    return {'ansible_host': c['hostname'],
            'ansible_port': c['port'],
            'ansible_user': c['user'],
            'ansible_private_key_file': c['identityfile'][0]}


def main():
    args = parse_args()
    if args.list:
        hosts = list_running_hosts()
        json.dump({'vagrant': hosts}, sys.stdout)
    else:
        details = get_host_details(args.host)
        json.dump(details, sys.stdout)

if __name__ == '__main__':
    main()

Running the dynamic inventory script

Once we've written the dynamic inventory script and we're ready to run Ansible using it, we can pass the inventory script using the -i flag:

ansible -i dynamic_vagrant.py -u ubuntu myvagrantbox -m ping

Flag