Merging groups and hostvars in Ansible variables

Tools of the trade.

I was googling for information on creating round-robin DNS records in Route53 with Ansible and came across this post by Daniel Hall who’d figured out a way of merging the his host group list with their hostvars in Ansible. After a bit of emailing with him I’ve figured a way to do it without having to add an extra filter as he did originally - by digging more into Jinja2.

What’s the problem?

1. A host group in ansible is a list of their addresses - e.g. ["foo.example.com", "bar.example.com"]

2. hostvars is a dictionary of all the known ‘facts’ for each system, e.g.:

{
    "foo.example.com": {
        "ansible_default_ipv4": {
            "address": "192.168.1.7"
        }
    },
    ...
}

3. Given a host group, we want to create a single string that is the comma-separated concatenation of all of their ip4 addresses from hostvars.

It uses a few of the lesser-used jinja2 features which I’ll talk through.

Here’s the example playbook (test.yml):

- hosts: all
  connection: local
  tasks:
    - debug:
        # Comma-separated string of all ip4 addresses of hosts
        msg: |
          {% set comma = joiner(",") %}
          {% for item in ['127.0.0.1', '127.0.0.1'] -%}
              {{ comma() }}{{ hostvars[item].ansible_default_ipv4.address }}
          {%- endfor %}

You can test it by running with:

ansible-playbook test.yml -i 127.0.0.1,

Yes, the way you test Ansible without an inventory file is a comma-separated list of hosts - but the comma is needed for just one host.

It gives the output (cows disabled for conciseness):

PLAY [127.0.0.1] **************************************************************

GATHERING FACTS ***************************************************************
ok: [127.0.0.1]

TASK: [debug ] ****************************************************************
ok: [127.0.0.1] => {
    "msg": "192.168.1.7,192.168.1.7"
}

PLAY RECAP ********************************************************************
127.0.0.1                  : ok=2    changed=0    unreachable=0    failed=0

Success!

How does it work?

First thing to notice is the inner loop - {% for item in .... I’m looping on an example array but you can easily replace this with a hosts list. At the end of the loop declaration, a dash is added before the end of the tag - -%} - this means jinja2 will collapse any whitespace it encounters in the loop.

Secondly, the ipv4 address is output with a simple lookup: hostvars[item].ansible_default_ipv4.address. There are a lot of facts that ansible gathers; add the task debug: var=hostvars to the playbook to see all variables available.

Thirdly, there’s this joiner thing being used. This is a template function in Jinja2 precisely for the purpose of joining strings with separators. It outputs the string you pass it on its 2nd+ calls. It’s better documented on Jinja2 itself.

Anyway, this was fun… especially since I didn’t need to implement round-robin DNS myself in the end! Also, there is maybe an easier way and I don’t know it. If you know, please tell me.


Tags: ansible