SaltStack: Setting a jinja2 variable from an inner block scope

saltstack_logo-thumbnailWhen using jinja2 for SaltStack formulas you may be surprised to find that your global scoped variables do not have ability to be modified inside a loop.  Although this is counter intuitive given the scope behavior of most scripting languages it is unfortunately the case that a jinja2 globally scoped variable cannot be modified from an inner scope.

As an example of a solution that will not work, let’s say you have a global flag ‘foundUser’ set to False, then want to iterate through a group of users, and if a condition is met inside the loop, then ‘foundUser’ would be set to True.

# this will not work!!!

{% set users = ['alice','bob','eve'] %}
{% set foundUser = False %} 

initial-check-on-global-foundUser: 
  cmd.run: 
    name: echo initial foundUser = {{foundUser}} 

{% for user in users %} 
{%- if user == "bob" %} 
{%- set foundUser = True %} 
{%- endif %} 
echo-for-{{user}}: 
  cmd.run: 
    name: echo my name is {{user}}, has bob been found? {{foundUser}}
{% endfor %} 

final-check-on-global-foundUser: 
  cmd.run: 
    name: echo final foundUser = {{foundUser}}

This will render to:

# this will not work!!!

initial-check-on-global-foundUser: 
  cmd.run: 
    name: echo initial foundUser = False 

echo-for-alice: 
  cmd.run: 
    name: echo my name is alice, has bob been found? False 

echo-for-bob: 
  cmd.run: 
    name: echo my name is bob, has bob been found? True 

echo-for-eve: 
  cmd.run: 
    name: echo my name is eve, has bob been found? True 

final-check-on-global-foundUser: 
  cmd.run: 
    name: echo final foundUser = False

So, while you can see that inside the loop, the value of ‘foundUser’ has indeed been modified, once you get outside the loop again, the value of ‘foundUser’ is still False.

This scope behavior seems wrong to those coming from Java, Python, and most other modern programming languages.  But it is unfortunately the way jinja2 works.

The workaround is that instead of setting a simple variable directly, you can instead use a dictionary.  And although the dictionary object pointer itself cannot change, the key/value pair entries can be modified.

# works because dictionary pointer cannot change, but entries can 

{% set users = ['alice','bob','eve'] %} 
{% set foundUser = { 'flag': False } %} 

initial-check-on-global-foundUser: 
  cmd.run: 
    name: echo initial foundUser = {{foundUser.flag}} 

{% for user in users %} 
{%- if user == "bob" %} 
{%-   if foundUser.update({'flag':True}) %}{%- endif %} 
{%- endif %} 
echo-for-{{user}}: 
  cmd.run: 
    name: echo my name is {{user}}, has bob been found? {{foundUser.flag}} 
{% endfor %} 

final-check-on-global-foundUser: 
  cmd.run: 
    name: echo final foundUser = {{foundUser.flag}}

Which renders as:

# works because dictionary pointer cannot change, but entries can 

initial-check-on-global-foundUser: 
  cmd.run: 
    name: echo initial foundUser = False 

echo-for-alice: 
  cmd.run: 
    name: echo my name is alice, has bob been found? False 

echo-for-bob: 
  cmd.run: 
    name: echo my name is bob, has bob been found? True 

echo-for-eve: 
  cmd.run: 
    name: echo my name is eve, has bob been found? True 

final-check-on-global-foundUser: 
  cmd.run: 
    name: echo final foundUser = True

 

REFERENCES

http://stackoverflow.com/questions/9486393/jinja2-change-the-value-of-a-variable-inside-a-loop

http://stackoverflow.com/questions/4870346/can-a-jinja-variables-scope-extend-beyond-in-an-inner-block

https://github.com/pallets/jinja/issues/164