Jinja2 - global variable update in for loop - ansible

I'm working on a configuration script for a certain service and I'd like to have it templated for our configuration management tools (Ansible). There's a particular action however, which seems to be a Jinja2 limitation (if that word is acceptable in this case) which I can't overcome:
{% set min = 0 %}
{% set max = 5500 %}
{% for item in list_of_items %}
for i in {min..max}; do command {{ item }} --arg 1 commnand_stuff $i; done
{% set min = max + 1 %}
{% set max = max * 2 %}
#fi
{% endfor %}
The expected (desired) result is:
- iteration 1 - min = 0, max = 5500
- iteration 2 - min = 5501, max = 11000
..
The actual result is:
- min and max have a constant value through all loop iterations - min=0 and max=5500.
So, how do I modify a global variable in Jinja2 in for loop?

set does not work inside a loop. See assigning a variable inside a loop.
It is possible to use loop.index instead. The template below
{% for item in list_of_items %}
{{ 5500 * (loop.index-1) + 1 }}..{{ 5500 * loop.index }}
{% endfor %}
gives
1..5500
5501..11000
11001..16500

Related

How to iterate through three lists in parallel in Yaml file

Need 3 variables to change on every iteration.
ver = [1,2,3]
tes = [a,b,c]
bet = [1a.2b.3c]
{% for v,t,b in ver,tes,bet %}
{{ v }} {{ t }} {{ b}}
{% endfor %}
o/p:
1 a 1a
2 b 2b
3 c 3c
I ran the same, got a very messy o/p. There should be only 3 iterations printing 3 variables in each iteration.
How can i perform the above operations using the for loop ? In the above case
Any suggestions on where I'm going wrong, would help Thanks !
You can use the following code:
{% for i in range([ver|count, tes|count, bet|count]|min) %}
{{ ver[i] }} {{ tes[i] }} {{ bet[i] }}
{% endfor %}
It uses only built in filters and functions:
count
min (to handle sequences with different length, similarly as zip in python)
range
Entire code in python:
import jinja2
ver = [1, 2, 3, 4]
tes = ['a', 'b', 'c']
bet = ['1a', '2b', '3c']
t = jinja2.Template('''{% for i in range([ver|count, tes|count, bet|count]|min) %}
{{ ver[i] }} {{ tes[i] }} {{ bet[i] }}
{% endfor %}
''')
print(t.render(ver=ver, tes=tes, bet=bet))
It looks like you need something that behaves like Python's zip function. Here's one option, in which we simply expose zip to the Jinja environment:
>>> import jinja2
>>> ver=[1,2,3]
>>> tes=['a','b','c']
>>> bet=['1a','2b','3c']
>>> t=jinja2.Template('''{% for v,t,b in zip(ver,tes,bet) %}
... {{ v }} {{ t }} {{ b}}
... {% endfor %}
... ''')
>>> t.environment.globals.update(zip=zip)
>>> print(t.render(ver=ver,tes=tes,bet=bet))
1 2 3
a b c
1a 2b 3c
>>>

How to fix "StackLevelError (Stack Overflow)" in Jekyll navigation template

I am trying to write a recursive Jekyll navigation template (include) as described in "Nested tree navigation with recursion". I have a minimal example committed in jekyll-min, which basically has:
two top-level dirs, each with one page
another dir under the second top-level dir, containing one page
a navigation template (_includes/docs_contents.html) that loops through the top-level dirs and initiates recursive traversal for each
a recursive include (_includes/nav.html) that accepts a navigation entry, renders its title and child links, and invokes itself recursively for any dirs in its children list
a layout (_layouts/doc.html) that renders the navigation pane and content for each page
I'm using Ruby v2.7.0 and Jekyll v3.8.5.
# docs structure
_docs
|
|_a/
| |_index.md
|
|_b/
|_index.md
|
|_1/
|_index.md
# _data/docs-nav.yml
- title: a
docs:
- link: /a/
- title: b
docs:
- link: /b/
- title: 1
docs:
- link: /b/1/
# _includes/nav.html
{% assign section=include.nav %}
<div class="ui accordion">
<div class="title active">
<i class="dropdown icon"></i>
{{ section.title }}
</div>
<div class="content active">
<div class="ui vertical text menu">
{% for item in section.docs %}
{% if item.link %}
{%- assign p = site.documents | where: "url", item.link | first %}
<a {%- if page.url== p.url %} class="current item" {% endif %} class="item" href="{{ p.url }}">
{{ p.menu_name | default: p.title }}
</a>
{% endif %}
{% if item.docs %}
{% include nav.html nav=item %}
{% endif %}
{% endfor %}
</div>
</div>
</div>
# _includes/docs_contents.html
<div class="unit one-fifth hide-on-mobiles">
<aside>
{% for section in site.data.docs_nav %}
{% include nav.html nav=section %}
{% endfor %}
</aside>
</div>
# _layouts/doc.html
---
title: Docs
description: version 1.0
---
<html>
<body>
{% include docs_contents.html %}
{{ content }}
</body>
</html>
As far as I understand, for each page the navigation template render should work like this:
_layouts/doc.html
_includes/docs_contents.html: iterate root level entries, calling _nav for each
_nav(/a/ entry): render title, iterate docs, render /a/ link, and quit
_nav(/b/ entry): render title, iterate docs, render /b/ link, and then call _nav(/b/1/ entry)
_nav(/b/1/ entry): render title, iterate docs, render /b/1/ link, and quit
_nav(/b/ entry) (already in stack): quit
_includes/docs_contents.html: quit
However, when I perform a bundle exec jekyll build I get:
Liquid Exception: Liquid error (/mnt/e/ThirdParty/jekyll-min/_includes/docs_contents.html line 17):
Nesting too deep included in /_layouts/doc.html
jekyll 3.8.5 | Error: Liquid error (/mnt/e/ThirdParty/jekyll-min/_includes/docs_contents.html line 17):
Nesting too deep included
Traceback (most recent call last):
[...]
What is the problem with my content or the recursive template? I have been struggling with this for hours with no luck.
JEKYLL_LOG_LEVEL=debug
didn't produce any additional useful info.
The actual document structure is more complex and could go arbitrarily deep, so writing a non-recursive template to manually handle nested levels may not be an option.
Excellent question.
With help of {{ myvar | inspect }} and a flag limiting recursion, I've successfully debugged your code and understood why this infinite recursion occurs.
It comes from the fact that the section variable in docs_contents.html is assigned by in a for loop and freezed : it cannot be changed.
The first time you include nav.html, {% assign section=include.nav %} is not changing section and your code just use the one assigned in your for loop.
When you recurse and call nav.html a second time it will use the same freezed global section variable and recurse indefinitely.
The solution is to change your variable name in nav.html from section to something else. eg: sub_section, and it will work, because this new variable will not be freezed and can be reassigned as needed during recursion.
{% assign sub_section=include.nav %}
{{ sub_section.title }}
{% for item in sub_section.docs %}
...
If you want to experiment here is my test code with some comments :
docs_contents.html
{% for section in site.data.docs_nav %}
{% comment %} ++++ Try to reassign "section" ++++ {% endcomment %}
{% assign section = "yolo from docs_contents.html" %}
{% assign recursion = 0 %}
<pre>
>> docs_contents.html
++++ "recursion" var is assigned and becomes global
recursion : {{ recursion | inspect }}
++++ "section" is freezed to loop value ++++
including nav with include nav.html nav=section >> {{ section | inspect }}
</pre>
{% include nav.html nav=section %}
{% endfor %}
nav.html
{% comment %} ++++ Try to reassign "section" ++++ {% endcomment %}
{% assign section = "yolo from nav.html" %}
<pre>
>> nav.hml
recursion : {{ recursion }}
include.nav : {{ include.nav | inspect }}
++++ "section" is freezed to loop value ++++
section : {{ section | inspect }}
</pre>
{% comment %} ++++ useless assignement ++++ {% endcomment %}
{% assign section=include.nav %}
{% for item in section.docs %}
{% if item.link %}
{%- assign p = site.documents | where: "url", item.link | first %}
<a {%- if page.url== p.url %} class="current item" {% endif %} class="item" href="{{ p.url }}">
{{ p.menu_name | default: p.title }}
</a>
{% endif %}
{% comment %}++++ limiting recursion to 2 levels ++++{% endcomment %}
{% if item.docs and recursion < 2 %}
{% comment %}++++ incrementing "recursion" global variable ++++{% endcomment %}
{% assign recursion = recursion | plus: 1 %}
{% include nav.html nav=item %}
{% endif %}
{% endfor %}

NUNJUKS: For loop to create list of variables, but instead creates strings

I'm using nunjucks to create a JSON export file. I have a list of variables that have the same name except for an incrementing number on the end. I'm using a for loop in the following way:
{% for i in range(1, 6) -%}
{% set items = ["{{ answer",i, " }}"] %}
"Solution{{ i }}" : "{{ items | join }}",
{%- endfor %}
I want answer1 to grab the variable answer1, but instead it is giving me a string "{{ anwser1 }}" .
Any idea how to use the for loop to point to each variable (answer1, answer2, answer3, etc)?
You can add some global function or filter to get access to a context (vars scope) by name.
const nunjucks = require('nunjucks');
const env = new nunjucks.Environment();
env.addGlobal('getContext', function (prop) {
return prop ? this.ctx[prop] : this.ctx;
});
const tpl = `{% for i in range(1, 4)%} {{ getContext('a' + i) }} {% endfor %}`;
const output = env.renderString(tpl, {a1: 10, a2:20, a3: 30, b: 1, c: 2});
console.log(output);

Jinja add automatic indentation

In ansible I use a template written in jinja2,
I have an inner for loop which automatically adds space to my config file, which i do not want to.
stick store-response payload_lv(43,1) if serverhello
option ssl-hello-chk
{% set count = 1 %}
{% for ip in sv.ips %}
server server{{ count }} {{ ip }}:443 check
{% set count = count + 1 %}
{% endfor %}
Result is
stick store-response payload_lv(43,1) if serverhello
option ssl-hello-chk
server server1 10.2.0.16:443 check
server server2 10.2.0.20:443 check
Add this line at the top of your template, will preserve the indentation
#jinja2: trim_blocks: True, lstrip_blocks: True

Why doesn't arithmetic work on variables

I'm stuck getting a basic if statement to work on a custom pagination for a jekyll site. Here's my code:
{% if paginator.total_pages > 1 %}
{% for page in (1..paginator.total_pages) %}
{% if page == paginator.page %}
({{ page }})
{% elsif page >= 6 and page <= 10 %}
{{ page }}
{% else %}
{{ paginator.page }}
{% endif %}
{% endfor %}
{% endif %}
Assuming the current page is number 8, I get the following output:
8 8 8 8 8 6 7 (8) 9 10 8 8 8 8 8 8 8
Now if I replace {% elsif page >= 6 and page <= 10 %} with {% elsif page >= (paginator.page - 2) and page <= (paginator.page + 2) %} I get the following output:
8 8 8 8 8 8 8 (8) 8 8 8 8 8 8 8 8 8
Can someone explain why basic arithmetic doesn't work on the variable (paginator.page) and how I can get around this?
OK, total rewrite of my answer, I'll leave the old stuff below for historical reference to the troubleshooting steps.
According to this: Liquid and Arithmetic Liquid has it's own way of capturing variables and doing arithmetic. Thanks to the OP for the find. Just surprised that I couldn't find this in their own documentation.
OK, since Liquid apparently needs and, not && I would try getting the arithmetic out of the if statement.
{% for page in (1..paginator.total_pages) %}
{% assign two_less = (paginator.page - 2) %}
{% assign two_more = (paginator.page + 2) %}
{% if page == paginator.page %}
({{ page }})
{% elsif page >= two_less and page <= two_more %}
{{ page }}
...
I think you might be seeing a problem with the Liquid template language. There have been problems before with and in Liquid, even in strings. See this closed issue: https://github.com/Shopify/liquid/issues/13
I would try this:
{% elsif (page >= (paginator.page - 2)) && (page <= (paginator.page + 2)) %}
and and && are slightly different in ruby, and possibly Liquid has some special significance for and.
old answer
It is basically doing exactly what you tell it to do. You say in the comments "I know the value is 8 because it prints 16 times". That is not valid debugging. All you know is that
page = 8 #at the time of execution
and that your if statement determined that it needed to print that value every time. So you are assuming that "8" is coming from the various branches of your if statement, but it is far more likely that your if statement keeps evaluating to the branch that prints "8". The simplest debugging you can do here is add:
{% if paginator.total_pages > 1 %}
{% for page in (1..paginator.total_pages) %}
{% if page == paginator.page %}
({{ page }})
{{ puts "if" }}
{% elsif page >= (paginator.page - 2) and page <= (paginator.page + 2) %}
{{ page }}
{{ puts "else if" }}
{% else %}
{{ paginator.page }}
{{ puts "else" }}
{% endif %}
{% endfor %}
{% endif %}
I don't use Jekyll so I am not 100% sure of my syntax, but you see what I'm trying to achieve? This will tell you where in the if statement your output value is coming from. If it is coming from the arithmetic as you suspect, it will tell you that. What I suspect though, is that you will see it never gets into that part of the if as you assume. Also, I'm not sure why you are getting
8 8 8 8 8 6 7 (8) 9 10 8 8 8 8 8 8 8
Do you want the first five and the last seven elements to evaluate to 8 and not:
1 2 3 4 5 6 7 (8) 9 10 11 12 13 14 15 16 17
Also just to be sure you're getting the type of variable return you expect change
(paginator.page - 2) and page <= (paginator.page + 2)
to:
(paginator.page.to_i - 2) and page <= (paginator.page.to_i + 2)
So my answer is pretty much what mcfinnigan said, arithmetic with variables does work. So you are either not getting an integer from the method paginator.page, or there is some other flaw in the if statement's logic.
edit-----
(removed part about "paginator.number" as it doesn't seem to apply to user's case)
I can expand and improve this answer if you can do the debugging I suggested and post the output.

Resources