Ansible provides hooks for running custom callbacks on the management machine (not the nodes) as it invokes modules. These callbacks allow us to trace what Ansible is doing, log operations it is starting on or has completed, and (most importantly to me) collect results from modules it has run. (Ever since Ansible stopped storing facts it collected in a
setup file on the target node, I've been yearning to get at that data. What has been possible all along is to manually run the setup module, dump that into a file and carry on from there, but it's a bit messy.)
Update: Hot off the mailing-list press, the following collects all facts from all machines and dumps them into the specified directory.
ansible all -m setup --tree /tmp/dump_path
Ansible's callback plugins are poorly documented, but after quite a bit of trial and error and error, I've been able to obtain the data I'm looking for, experimenting with the callbacks contained in the example
playbook_on_startis invoked at, well, yes, Playbook start. :)
playbook_on_task_startwhen a task starts.
playbook_on_vars_promptafter a Playbook has prompted the user for variables.
ansible directory, I drop the following bit of Python into
lib/ansible/callback_plugins/inventory.py where Ansible picks it up on its next run.
import os import time import sqlite3 dbname = '/etc/ansible/setup.db' TIME_FORMAT='%Y-%m-%d %H:%M:%S' try: con = sqlite3.connect(dbname) cur = con.cursor() except: pass def log(host, data): if type(data) == dict: invocation = data.pop('invocation', None) if invocation.get('module_name', None) != 'setup': return facts = data.get('ansible_facts', None) now = time.strftime(TIME_FORMAT, time.localtime()) try: # `host` is a unique index cur.execute("REPLACE INTO inventory (now, host, arch, dist, distvers, sys,kernel) VALUES(?,?,?,?,?,?,?);", ( now, facts.get('ansible_hostname', None), facts.get('ansible_architecture', None), facts.get('ansible_distribution', None), facts.get('ansible_distribution_version', None), facts.get('ansible_system', None), facts.get('ansible_kernel', None) )) con.commit() except: pass class CallbackModule(object): def runner_on_ok(self, host, res): log(host, res)
The callback I'm interested in is called
runner_on_ok which is invoked upon a successful run of a module. Each and every module. That means, say, the
command module will also end up in here.
To obtain results from the
setup module only, I inspect the name of the module and return if it isn't
"setup". Once I've determined that, I can grab the facts I want to record in my database table.
sqlite> SELECT * FROM inventory; 2012-09-11 12:25:20|hippo|x86_64|CentOS|6.2|Linux|2.6.32-220.17.1.el6.x86_64 2012-09-11 12:32:09|jmbp|i386|NA|NA|Darwin|10.8.0
If I wanted to obtain results from fact modules we create ourselves, I'd
have to explicitly add check for the appropriate module name: these custom
facts aren't merged in at setup time. What is automatically merged into
ansible_facts however, are "facts" obtained from facter and/or ohai if
these utilities are installed on the nodes.
Exporting the data we obtained to CSV and formatting it "pour le chef" is a cinch:
#!/bin/sh sqlite3 /etc/ansible/setup.db <<EOF .headers on .mode csv .output setup.csv SELECT * FROM inventory; EOF
So, you see: as someone said the other day: Ansible is automation that even a manager can understand. :-)