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.

How to use more than one inventory file in a playbook in one single command

If you want to run a playbook in more than one inventory file in one command you just need to put every inventory file you want inside a directory and then run:

ansible-playbook -i <inventory_file_directory> <playbook>

For example:

ansible-playbook -i inventory/rackspace_prod/ update_config.yml

So you can easily run a playbook and work with all servers that you need no matter in which environment file they are.

Let’s go a little deeper in the example above because you could think: why not create a bigger inventory file with everything inside it?

Imagine that you have one static inventory file (static) and a dynamic inventory file. For example Rackspace dynamic inventory file (rax.py). In both environments you have a group name called webservers because you use Rackspace Cloud Servers to scale up and down your static webservers.

If you want to operate all your webservers (static and dynamic servers) you could run the playbook twice:

ansible webservers -i inventory/static -m ping
ansible webservers -i inventory/rax.py -m ping

But you can create a directory called rackspace_prod and put there both inventory files and then run:

ansible webservers -i inventory/rackspace_prod/ -m ping

I normally use this feature to update configuration files like in apaches, loadbalancers or /etc/hosts file.

For example you can update haproxy configuration with your static and dynamic webservers by using a template file:

[...]
backend my_backend
option httpchk
cookie JSESSIONID prefix nocache
 balance roundrobin
{% for host in groups['webservers'] %}
 server {{ hostvars[host]['ansible_hostname'] }} {{ hostvars[host]['ansible_eth1']['ipv4']['address'] }}:80 check  {{ hostvars[host]['ansible_hostname'] }}
 {% endfor %}
[...]