how to append to a list in jinja2 for ansible
Asked Answered
M

7

44

Below is the jinja2 template that i wrote to use in ansible.

{% set port = 1234 %}
{% set server_ip = [] %}
{% for ip in host_ip  %}
{% do server_ip.append({{ ip }}:{{ port }}) %}
{% endfor %}
{% server_ip|join(', ') %}

Below is the my desired output:

devices = 192.168.56.14:1234,192.168.56.13:1234,192.168.56.10:1234

But when i am running the ansible playbook, it is throwing the error as below:

"AnsibleError: teme templating string: Encountered unknown tag 'do'. Jinja was looking for th: 'endfor' or 'else'

Any help would be appreciated..

Monroe answered 2/4, 2018 at 22:5 Comment(2)
remove do from ` {% do server_ip.append({{ ip }}:{{ port }}) %}` ?Hayashi
Even i tried that but values are not getting appended.Monroe
N
78

Try below code:

{% set port = '1234' %}
{% set server_ip = [] %}
{% for ip in host_ip  %}
{{ server_ip.append( ip+":"+port ) }}
{% endfor %}
{{ server_ip|join(',') }}

You ll get:

192.168.56.14:1234,192.168.56.13:1234,192.168.56.10:1234

Negativism answered 4/4, 2018 at 3:21 Comment(8)
Thank you for the reply. But i was looking to assign the output to a variable called "devices".Monroe
if the answer's code is part of an html served as part of flask app, {{ server_ip.append( ip+":"+port ) }} is going to be producing None as many times as there are steps in the outer for loop. Is there a way around it?Tampa
to avoid printing None, I 'd do {{ server_ip.append(...)|default("", True) }}Lunneta
another way to avoid printing None: {{ server_ip.append( ip+":"+port ) or ""}}Schmid
@Schmid while your method removes the None its still causing a huge amount of whitespace to be printed. Is there really no way to perform these list appends as a {% %} statement rather than {{ }} expression?Prolongation
The cleaner solution to avoid printing anything is to use {% do server_ip.append(ip+":"+port) %}Charmain
And for using: {% do server_ip.append(ip+":"+port) %} you should set this in ansible.cfg: jinja2_extensions = jinja2.ext.doVodka
you could also do {% set dummy = server_ip.append(ip+":"+port) %} to get rid of the None instead of modifying the config file.Astrodome
J
16

I didn't like any of the answers, they feel too hacky (having to worry about outputting None, or spurious whitespace using other techniques), but I think I've found a solution that works well. I took inspiration from this answer on a related question and realized that you can call set multiple times for the same variable and seemingly not incur any penalty.

It's still a tad hacky, because I don't think it's intended to work like this (then again, several design decisions in Jinja make me scratch my head, so who knows).

{% set server_ip = server_ip.append({{ ip }}:{{ port }}) %}

Interestingly, while the value is indeed appended to server_ip, the return value of that append (which we now know very well is None) isn't assigned back to server_ip on the LHS. Which led me to discover that the LHS side of the statement seems to be a no-op.

So you can also do this and the append works:

{% set tmp = server_ip.append({{ ip }}:{{ port }}) %}

Yet, if you print tmp, nothing shows up. Go figure.

Jeep answered 2/2, 2021 at 23:32 Comment(3)
Nice find, however this can be misleading. Remember that the append method works in place and does not return anything (None).Shikoku
I solved it adding or "" at the end: {{ server_ip.append( ip+":"+port ) or "" }}Moniz
Nice. I'm using {%- set _ = server_ip.append(...) %} to not add in an extra line and to follow the python standard that _ is a throwaway variable.Thermography
I
10

That worked for me:

- set_fact:
    devices: >-
      {% for ip in host_ip %}{{ ip }}:1234{% if not loop.last %},{% endif %}{% endfor %}

If you still want to use do then add

jinja2_extensions = jinja2.ext.do

to your ansible config file and change

{% do server_ip.append({{ ip }}:{{ port }}) %}` to `{% do server_ip.append({ip:port}) %}`
Investment answered 15/8, 2018 at 23:44 Comment(1)
this solution appears to be cleaner in jinja, just add commas when it's not the last loop. It doesn't require the use of default to remove NonesXenophon
Z
3

The most voted answer will cause a lot of whitespaces in the rendered result. Beside using do extension from jinja, the alternative solution is using whitespace control from jinja. Add minus sign - inside the block

{%- for ip in host_ip -%}...{%- endfor %}

will remove the whitespace.

Zeeland answered 14/7, 2022 at 4:6 Comment(0)
S
2

In order to avoid having None printed all over using {{ server_ip.append( ip+":"+port ) }} (just spent 20 min debugging this) and if you don't want to use the misleading {% set _ = server_ip.append( ip+":"+port ) %}, you can go back to Python basics and do the following:


# Remember that [1, 2] + [3] = [1, 2, 3]

{% set server_ip = server_ip + [ip+":"+port] %}

In 99.9% situations this will do the job. However in special cases where you work with very large lists there may be a small performance downside here in terms of memory usage: in the above example, [1, 2] + [3] = [1, 2, 3], both [1, 2] and [1, 2, 3] (initial and modified list) will coexist in memory for a brief moment, contrary to the append method which doesn't create additional objects in memory.

Shikoku answered 6/7, 2021 at 21:18 Comment(3)
I tried this solution but everytime I visit the for loop the variable get resets to it's original value. Do I need to use somekind of namespace to preserve variables in netsed loopsByelaw
AFAIK the variable you set inside the template will NOT survive outside the template (after it is rendered), if that's what you mean? Unless you pass it as an argument from within that template to a function that stores it outside, and then from there feed it back inside the templateShikoku
In Flask you can use the g variable/namespace, where you can store stuff and it remains available outside the template.Shikoku
P
1

One-line solution with map() and regex:

{{ ["1.1.1.1","2.2.2.2"]|map('regex_replace', '(.+)', "\\1:1234")|join(', ') }}

map('regex_replace', '(.+)', "\\1:1234") adds :1234 to any non-empty string (.+) in the passed array ["1.1.1.1","2.2.2.2"]. Result:

1.1.1.1:1234, 2.2.2.2:1234
Perfumery answered 7/9, 2021 at 14:59 Comment(3)
I tried but failed. It seems that regex_replace is not a jinja2 filter but located in ansible?Seto
@Seto indeed, this filter is added by Ansible. Here's the list of Jinja built-in filters: jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters .Perfumery
got it. I found it by google. And I solved my problems by implementing my own filter python function. Thanks very much.Seto
S
0

Another solution using namespace:

{% set port = "1234" %}
{% set data = namespace(server_ip=[]) %}
{% for ip in host_ip  %}
  {% set data.server_ip = data.server_ip + [ip + ":" + port] %}
{% endfor %}

{% set data.server_ip = data.server_ip|join(",") %}

{{ data.server_ip }}
Spermary answered 17/4, 2024 at 9:17 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.