Use namespace as global variable in Ansible Jinja templates

Posted by ads' corner on Thursday, 2020-07-09
Posted in [Ansible][Python]

A simple task, or so I thought: in a Jinja template keep track of the number of items in a loop. And then use that count afterwards.

Disclaimer: the number of items is not equal the number of times the loop runs, so I can’t use the loop variables.

Turns out that Jinja has other opinions, and variables inside a loop are all local. When a variable is changed inside the loop, the scope of the variable stays local, and once the loop ends the original value of the variable is restored. That’s even true for variables which are created outside the loop.

To demonstrate the problem, here is a simple Playbook, justĀ a small template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- hosts: all
  gather_facts: True
  any_errors_fatal: True
  force_handlers: True

  tasks:
    - name: Template
      local_action:
        module: template
        src: "/tmp/template.in"
        dest: "/tmp/template.out"

The Playbook is executed:

1
ansible-playbook -i 127.0.0.1, /tmp/template.yml

The template is setting a variable, running through a loop and increasing the variable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{% set counter = 1 %}

Counter before loop: {{ counter }}

{% for i in range(6) %}
{% set counter = counter + 1 %}
Counter in loop ({{ i }}): {{ counter }}

{% endfor %}

Counter after loop: {{ counter }}

One would expect that the variable starts with 1, the loop runs 5 times (the end value of 6 for the range is never reached), and will increase the counter to 6. Instead this happens:

Counter before loop: 1

Counter in loop (1): 2

Counter in loop (2): 2

Counter in loop (3): 2

Counter in loop (4): 2

Counter in loop (5): 2

Counter after loop: 1

Every run of the loop gets a “fresh” copy of the variable value, and the total is never increased beyond 2.

Now one can argue that not too much work should be done in templates, but at some point the work has to be done. A counter is rarely pre-calculated. To solve this kind of problems, Jinja introduced Namespaces in version 2.10. By using a namespace, the variable in the loop is never localized, but instead the variable in the namespace is used. The same simple loop from above with a namespace:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% set ns = namespace() %}
{% set ns.counter = 1 %}

Counter before loop: {{ ns.counter }}

{% for i in range(1, 6) %}
{% set ns.counter = ns.counter + 1 %}
Counter in loop ({{ i }}): {{ ns.counter }}

{% endfor %}

Counter after loop: {{ ns.counter }}

Results in the following output:

Counter before loop: 1

Counter in loop (1): 2

Counter in loop (2): 3

Counter in loop (3): 4

Counter in loop (4): 5

Counter in loop (5): 6

Counter after loop: 6

Exactly what I’m looking for!


Categories: [Ansible] [Python]