Jinja2: How to gather_facts from other hosts

If you have this content in your inventory file:

[loadbalancers]
ha01 ansible_host=ec2-52-50-164-246.eu-west-1.compute.amazonaws.com
ha02 ansible_host=ec2-52-48-101-228.eu-west-1.compute.amazonaws.com

[webservers]
web01 ansible_host=ec2-52-50-143-215.eu-west-1.compute.amazonaws.com
web02 ansible_host=ec2-52-31-14-253.eu-west-1.compute.amazonaws.com

And you want to create a template inside your loadbalancers with information from webservers, like for example following haproxy configuration:

{% for host in groups['webservers'] %}
server {{ hostvars[host]['ansible_hostname'] }} {{ hostvars[host]['ansible_default_ipv4']['address'] }}:80
{% endfor %}

You first need to connect to webservers. No need to do anything. So, your playbook will look like:

---
# No need to run a task for webservers. This will gather their facts.
- hosts: webservers
  become: yes

- hosts: loadbalancers
  become: yes
  roles:
- haproxy-ha

This will connect first to your webservers and gather their facts that will be used in haproxy.cfg.j2 template file.

Jinja2: lstrip_blocks to manage indentation

According to Jinja2 documentation you can manage whitespace and tabular indentation with lstrip_blocks and trim_blocks options:

  • trim_blocks: If this is set to True the first newline after a block is removed (block, not variable tag!). Defaults to False.
  • lstrip_blocks: If this is set to True leading spaces and tabs are stripped from the start of a line to a block. Defaults to False.

By default Ansible templates have trim_blocks true and lstrip_blocks false. If you want to manage them you can do it if you set them at the very beginning of your template file as follows:

#jinja2: lstrip_blocks: "True (or False)", trim_blocks: "True (or False)"

So, if you have following Ansible code for /etc/hosts template:

{% for host in groups['webservers'] %}
    {% if inventory_hostname in hostvars[host]['ansible_fqdn'] %}
{{ hostvars[host]['ansible_default_ipv4']['address'] }} {{ hostvars[host]['ansible_fqdn'] }} {{ hostvars[host]['inventory_hostname'] }} MYSELF
    {% else %}
{{ hostvars[host]['ansible_default_ipv4']['address'] }} {{ hostvars[host]['ansible_fqdn'] }} jcs-server{{ loop.index }} {{ hostvars[host]['inventory_hostname'] }}
    {% endif %}
{% endfor %}

By default Ansible will indent as you’re doing in your code:

     192.168.1.2 web01
               192.168.1.3 web02
               192.168.1.4 web03

But if you add at the top of your template the following code:

#jinja2: lstrip_blocks: “True”

Result will be without indentation:

192.168.1.2 web01
192.168.1.3 web02
192.168.1.4 web03

That makes you able to write more readable jinja2 code in your templates.

Please, note that you can’t delete indentation of the code that will be written in your final file. So in our case we’ve defined hostvars variables at the very beginning of our line because we wanted them there. If you indent that code, your result will be indented.

Jinja2: Using loop.index and loop.length. Examples: /etc/hosts and workers.properties

It’s really important to know how Jinja2 works if you want to create powerful templates for your playbooks. Today we’re gonna work with:

  • loop.index: The current iteration of the loop. (1 indexed)
  • loop.length:  The number of items in the sequence

Two different examples for different files: /etc/hosts and workers.properties:

/etc/hosts

We want to generate the following snip of code inside /etc/hosts:

192.168.1.2 web01 BB-WS1
192.168.1.3 web02 BB-WS2
192.168.1.4 web03 BB-WS3
192.168.1.5 web04 BB-WS4

web01, web02, web03 and web04 are hostnames, but how can we generate the second field?

Let’s say we have a group called ‘webservers’ in our inventory file. We can use loop.index if we want to create the code above:

{% for host in groups['webservers'] %}
{{ hostvars[host]['ansible_default_ipv4']['address'] }} {{ hostvars[host]['ansible_fqdn'] }} BB-WS{{ loop.index }}
{% endfor %}

workers.properties

We want to generate the following snip of code for Apache workers properties:

worker.loadbalancer.balance_workers=bb-ws1_gf,bb-ws2_gf,bb-ws3_gf,bb-ws4_gf

In that case, not only it’s important to know the loop.index value. Also we want to know if we’re in the latest item. Because the latest item it’s without comma. To do that we will use loop.index combined with loop.length:

worker.loadbalancer.balance_workers=
{% for host in groups['webservers'] %}
{% if loop.index == loop.length %}
bb-ws{{ loop.index }}_gf
{% else %}
bb-ws{{ loop.index }}_gf,
{% endif %}
{% endfor %}

As you can see if we’re in the latest item ( loop.index == loop.length ) we should write the code without comma.