Understanding Jinja

Jinja is the default templating language in SLS files.

Jinja in States

Jinja is evaluated before YAML, which means it is evaluated before the States are run.

The most basic usage of Jinja in state files is using control structures to wrap conditional or redundant state elements:

{% if grains['os'] != 'FreeBSD' %}
tcsh:
    pkg:
        - installed
{% endif %}

motd:
  file.managed:
    {% if grains['os'] == 'FreeBSD' %}
    - name: /etc/motd
    {% elif grains['os'] == 'Debian' %}
    - name: /etc/motd.tail
    {% endif %}
    - source: salt://motd

In this example, the first if block will only be evaluated on minions that aren't running FreeBSD, and the second block changes the file name based on the os grain.

Writing if-else blocks can lead to very redundant state files however. In this case, using pillars, or using a previously defined variable might be easier:

{% set motd = ['/etc/motd'] %}
{% if grains['os'] == 'Debian' %}
  {% set motd = ['/etc/motd.tail', '/var/run/motd'] %}
{% endif %}

{% for motdfile in motd %}
{{ motdfile }}:
  file.managed:
    - source: salt://motd
{% endfor %}

Using a variable set by the template, the for loop will iterate over the list of MOTD files to update, adding a state block for each file.

The filter_by function can also be used to set variables based on grains:

{% set auditd = salt['grains.filter_by']({
'RedHat': { 'package': 'audit' },
'Debian': { 'package': 'auditd' },
}) %}

Include and Import

Includes and imports can be used to share common, reusable state configuration between state files and between files.

{% from 'lib.sls' import test %}

This would import the test template variable or macro, not the test state element, from the file lib.sls. In the case that the included file performs checks against grains, or something else that requires context, passing the context into the included file is required:

{% from 'lib.sls' import test with context %}

Including Context During Include/Import

By adding with context to the include/import directive, the current context can be passed to an included/imported template.

{% import 'openssl/vars.sls' as ssl with context %}

Macros

Macros are helpful for eliminating redundant code. Macros are most useful as mini-templates to repeat blocks of strings with a few parameterized variables. Be aware that stripping whitespace from the template block, as well as contained blocks, may be necessary to emulate a variable return from the macro.

# init.sls
{% from 'lib.sls' import pythonpkg with context %}

python-virtualenv:
  pkg.installed:
    - name: {{ pythonpkg('virtualenv') }}

python-fabric:
  pkg.installed:
    - name: {{ pythonpkg('fabric') }}
# lib.sls
{% macro pythonpkg(pkg) -%}
  {%- if grains['os'] == 'FreeBSD' -%}
    py27-{{ pkg }}
  {%- elif grains['os'] == 'Debian' -%}
    python-{{ pkg }}
  {%- endif -%}
{%- endmacro %}

This would define a macro that would return a string of the full package name, depending on the packaging system's naming convention. The whitespace of the macro was eliminated, so that the macro would return a string without line breaks, using whitespace control.

Template Inheritance

Template inheritance works fine from state files and files. The search path starts at the root of the state tree or pillar.

Errors

Saltstack allows to raise custom errors using the raise jinja function.

{{ raise('Custom Error') }}

When rendering the template containing the above statement, a TemplateError exception is raised, causing the rendering to fail with the following message:

TemplateError: Custom Error

Filters

Saltstack extends builtin filters with these custom filters:

strftime

Converts any time related object into a time based string. It requires a valid strftime directives. An exhaustive list can be found in the official Python documentation.

{% set curtime = None | strftime() %}

Fuzzy dates require the timelib Python module is installed.

{{ "2002/12/25"|strftime("%y") }}
{{ "1040814000"|strftime("%Y-%m-%d") }}
{{ datetime|strftime("%u") }}
{{ "tomorrow"|strftime }}

sequence

Ensure that parsed data is a sequence.

yaml_encode

Serializes a single object into a YAML scalar with any necessary handling for escaping special characters. This will work for any scalar YAML data type: ints, floats, timestamps, booleans, strings, unicode. It will not work for multi-objects such as sequences or maps.

{%- set bar = 7 %}
{%- set baz = none %}
{%- set zip = true %}
{%- set zap = 'The word of the day is "salty"' %}

{%- load_yaml as foo %}
bar: {{ bar|yaml_encode }}
baz: {{ baz|yaml_encode }}
baz: {{ zip|yaml_encode }}
baz: {{ zap|yaml_encode }}
{%- endload %}

In the above case {{ bar }} and {{ foo.bar }} should be identical and {{ baz }} and {{ foo.baz }} should be identical.

yaml_dquote

Serializes a string into a properly-escaped YAML double-quoted string. This is useful when the contents of a string are unknown and may contain quotes or unicode that needs to be preserved. The resulting string will be emitted with opening and closing double quotes.

{%- set bar = '"The quick brown fox . . ."' %}
{%- set baz = 'The word of the day is "salty".' %}

{%- load_yaml as foo %}
bar: {{ bar|yaml_dquote }}
baz: {{ baz|yaml_dquote }}
{%- endload %}

In the above case {{ bar }} and {{ foo.bar }} should be identical and {{ baz }} and {{ foo.baz }} should be identical. If variable contents are not guaranteed to be a string then it is better to use yaml_encode which handles all YAML scalar types.

yaml_squote

Similar to the yaml_dquote filter but with single quotes. Note that YAML only allows special escapes inside double quotes so yaml_squote is not nearly as useful (viz. you likely want to use yaml_encode or yaml_dquote).

to_bool

New in version 2017.7.0.

Returns the logical value of an element.

Example:

{{ 'yes' | to_bool }}
{{ 'true' | to_bool }}
{{ 1 | to_bool }}
{{ 'no' | to_bool }}

Will be rendered as:

True
True
True
False

exactly_n_true

New in version 2017.7.0.

Tests that exactly N items in an iterable are "truthy" (neither None, False, nor 0).

Example:

{{ ['yes', 0, False, 'True'] | exactly_n_true(2) }}

Returns:

True

exactly_one_true

New in version 2017.7.0.

Tests that exactly one item in an iterable is "truthy" (neither None, False, nor 0).

Example:

{{ ['yes', False, 0, None] | exactly_one_true }}

Returns:

True

quote

New in version 2017.7.0.

This text will be wrapped in quotes.

regex_match

New in version 2017.7.0.

If zero or more characters at the beginning of string match this regular expression, otherwise returns None.

Example:

{{ 'abcdefabcdef' | regex_match('BC(.*)', ignorecase=True) }}

Returns:

None

uuid

New in version 2017.7.0.

Return a UUID.

Example:

{{ 'random' | uuid }}

Returns:

3652b285-26ad-588e-a5dc-c2ee65edc804

is_list

New in version 2017.7.0.

Return if an object is list.

Example:

{{ [1, 2, 3] | is_list }}

Returns:

True

is_iter

New in version 2017.7.0.

Return if an object is iterable.

Example:

{{ [1, 2, 3] | is_iter }}

Returns:

True

min

New in version 2017.7.0.

Return the minimum value from a list.

Example:

{{ [1, 2, 3] | min }}

Returns:

1

max

New in version 2017.7.0.

Returns the maximum value from a list.

Example:

{{ [1, 2, 3] | max }}

Returns:

3

avg

New in version 2017.7.0.

Returns the average value of the elements of a list

Example:

{{ [1, 2, 3] | avg }}

Returns:

2

union

New in version 2017.7.0.

Return the union of two lists.

Example:

{{ [1, 2, 3] | union([2, 3, 4]) | join(', ') }}

Returns:

1, 2, 3, 4

intersect

New in version 2017.7.0.

Return the intersection of two lists.

Example:

{{ [1, 2, 3] | intersect([2, 3, 4]) | join(', ') }}

Returns:

2, 3

difference

New in version 2017.7.0.

Return the difference of two lists.

Example:

{{ [1, 2, 3] | difference([2, 3, 4]) | join(', ') }}

Returns:

1

symmetric_difference

New in version 2017.7.0.

Return the symmetric difference of two lists.

Example:

{{ [1, 2, 3] | symmetric_difference([2, 3, 4]) | join(', ') }}

Returns:

1, 4

is_sorted

New in version 2017.7.0.

Return is an iterable object is already sorted.

Example:

{{ [1, 2, 3] | is_sorted }}

Returns:

True

compare_lists

New in version 2017.7.0.

Compare two lists and return a dictionary with the changes.

Example:

{{ [1, 2, 3] | compare_lists([1, 2, 4]) }}

Returns:

{'new': 4, 'old': 3}

compare_dicts

New in version 2017.7.0.

Compare two dictionaries and return a dictionary with the changes.

Example:

{{ {'a': 'b'} | compare_lists({'a': 'c'}) }}

Returns:

{'a': {'new': 'c', 'old': 'b'}}

is_hex

New in version 2017.7.0.

Return True if the value is hexazecimal.

Example:

{{ '0xabcd' | is_hex }}
{{ 'xyzt' | is_hex }}

Returns:

True
False

contains_whitespace

New in version 2017.7.0.

Return True if a text contains whitespaces.

Example:

{{ 'abcd' | contains_whitespace }}
{{ 'ab cd' | contains_whitespace }}

Returns:

False
True

substring_in_list

New in version 2017.7.0.

Return is a substring is found in a list of string values.

Example:

{{ 'abcd' | substring_in_list(['this', 'is', 'an abcd example']) }}

Returns:

True

check_whitelist_blacklist

New in version 2017.7.0.

Check a whitelist and/or blacklist to see if the value matches it.

This filter can be used with either a whitelist or a blacklist individually, or a whitelist and a blacklist can be passed simultaneously.

If whitelist is used alone, value membership is checked against the whitelist only. If the value is found, the function returns True. Otherwise, it returns False.

If blacklist is used alone, value membership is checked against the blacklist only. If the value is found, the function returns False. Otherwise, it returns True.

If both a whitelist and a blacklist are provided, value membership in the blacklist will be examined first. If the value is not found in the blacklist, then the whitelist is checked. If the value isn't found in the whitelist, the function returns False.

Whitelist Example:

{{ 5 | check_whitelist_blacklist(whitelist=[5, 6, 7]) }}

Returns:

True

Blacklist Example:

{{ 5 | check_whitelist_blacklist(blacklist=[5, 6, 7]) }}
False

date_format

New in version 2017.7.0.

Converts unix timestamp into human-readable string.

Example:

{{ 1457456400 | date_format }}
{{ 1457456400 | date_format('%d.%m.%Y %H:%M') }}

Returns:

2017-03-08
08.03.2017 17:00

to_num

New in version 2017.7.0.

New in version Oxygen: Renamed from str_to_num to to_num.

Converts a string to its numerical value.

Example:

{{ '5' | to_num }}

Returns:

5

to_bytes

New in version 2017.7.0.

Converts string-type object to bytes.

Example:

{{ 'wall of text' | to_bytes }}

Note

This option may have adverse effects when using the default renderer, yaml_jinja. This is due to the fact that YAML requires proper handling in regard to special characters. Please see the section on YAML ASCII support in the YAML Idiosyncracies documentation for more information.

json_decode_list

New in version 2017.7.0.

JSON decodes as unicode, Jinja needs bytes.

Example:

{{ [1, 2, 3] | json_decode_list }}

Returns:

[1, 2, 3]

json_decode_dict

New in version 2017.7.0.

JSON decodes as unicode, Jinja needs bytes.

Example:

{{ {'a': 'b'} | json_decode_dict }}

Returns:

{'a': 'b'}

random_hash

New in version 2017.7.0.

New in version Oxygen: Renamed from rand_str to random_hash to more accurately describe what the filter does.

Generates a random number between 1 and the number passed to the filter, and then hashes it. The default hash type is the one specified by the minion's hash_type config option, but an alternate hash type can be passed to the filter as an argument.

Example:

{% set num_range = 99999999 %}
{{ num_range | random_hash }}
{{ num_range | random_hash('sha512') }}

Returns:

43ec517d68b6edd3015b3edc9a11367b
d94a45acd81f8e3107d237dbc0d5d195f6a52a0d188bc0284c0763ece1eac9f9496fb6a531a296074c87b3540398dace1222b42e150e67c9301383fde3d66ae5

md5

New in version 2017.7.0.

Return the md5 digest of a string.

Example:

{{ 'random' | md5 }}

Returns:

7ddf32e17a6ac5ce04a8ecbf782ca509

sha256

New in version 2017.7.0.

Return the sha256 digest of a string.

Example:

{{ 'random' | sha256 }}

Returns:

a441b15fe9a3cf56661190a0b93b9dec7d04127288cc87250967cf3b52894d11

sha512

New in version 2017.7.0.

Return the sha512 digest of a string.

Example:

{{ 'random' | sha512 }}

Returns:

811a90e1c8e86c7b4c0eef5b2c0bf0ec1b19c4b1b5a242e6455be93787cb473cb7bc9b0fdeb960d00d5c6881c2094dd63c5c900ce9057255e2a4e271fc25fef1

base64_encode

New in version 2017.7.0.

Encode a string as base64.

Example:

{{ 'random' | base64_encode }}

Returns:

cmFuZG9t

base64_decode

New in version 2017.7.0.

Decode a base64-encoded string.

{{ 'Z2V0IHNhbHRlZA==' | base64_decode }}

Returns:

get salted

hmac

New in version 2017.7.0.

Verify a challenging hmac signature against a string / shared-secret. Returns a boolean value.

Example:

{{ 'get salted' | hmac('shared secret', 'eBWf9bstXg+NiP5AOwppB5HMvZiYMPzEM9W5YMm/AmQ=') }}

Returns:

True

http_query

New in version 2017.7.0.

Return the HTTP reply object from a URL.

Example:

{{ 'http://jsonplaceholder.typicode.com/posts/1' | http_query }}

Returns:

{
  'body': '{
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto"
  }'
}

Networking Filters

The following networking-related filters are supported:

is_ip

New in version 2017.7.0.

Return if a string is a valid IP Address.

{{ '192.168.0.1' | is_ip }}

Additionally accepts the following options:

  • global
  • link-local
  • loopback
  • multicast
  • private
  • public
  • reserved
  • site-local
  • unspecified

Example - test if a string is a valid loopback IP address.

{{ '192.168.0.1' | is_ip(options='loopback') }}

is_ipv4

New in version 2017.7.0.

Returns if a string is a valid IPv4 address. Supports the same options as is_ip.

{{ '192.168.0.1' | is_ipv4 }}

is_ipv6

New in version 2017.7.0.

Returns if a string is a valid IPv6 address. Supports the same options as is_ip.

{{ 'fe80::' | is_ipv6 }}

ipaddr

New in version 2017.7.0.

From a list, returns only valid IP entries. Supports the same options as is_ip. The list can contains also IP interfaces/networks.

Example:

{{ ['192.168.0.1', 'foo', 'bar', 'fe80::'] | ipaddr }}

Returns:

['192.168.0.1', 'fe80::']

ipv4

New in version 2017.7.0.

From a list, returns only valid IPv4 entries. Supports the same options as is_ip. The list can contains also IP interfaces/networks.

Example:

{{ ['192.168.0.1', 'foo', 'bar', 'fe80::'] | ipv4 }}

Returns:

['192.168.0.1']

ipv6

New in version 2017.7.0.

From a list, returns only valid IPv6 entries. Supports the same options as is_ip. The list can contains also IP interfaces/networks.

Example:

{{ ['192.168.0.1', 'foo', 'bar', 'fe80::'] | ipv6 }}

Returns:

['fe80::']

network_hosts

New in version 2017.7.0.

Return the list of hosts within a networks. This utility works for both IPv4 and IPv6.

Note

When running this command with a large IPv6 network, the command will take a long time to gather all of the hosts.

Example:

{{ '192.168.0.1/30' | network_hosts }}

Returns:

['192.168.0.1', '192.168.0.2']

network_size

New in version 2017.7.0.

Return the size of the network. This utility works for both IPv4 and IPv6.

Example:

{{ '192.168.0.1/8' | network_size }}

Returns:

16777216

gen_mac

New in version 2017.7.0.

Generates a MAC address with the defined OUI prefix.

Common prefixes:

  • 00:16:3E -- Xen
  • 00:18:51 -- OpenVZ
  • 00:50:56 -- VMware (manually generated)
  • 52:54:00 -- QEMU/KVM
  • AC:DE:48 -- PRIVATE

Example:

{{ '00:50' | gen_mac }}

Returns:

00:50:71:52:1C

mac_str_to_bytes

New in version 2017.7.0.

Converts a string representing a valid MAC address to bytes.

Example:

{{ '00:11:22:33:44:55' | mac_str_to_bytes }}

Note

This option may have adverse effects when using the default renderer, yaml_jinja. This is due to the fact that YAML requires proper handling in regard to special characters. Please see the section on YAML ASCII support in the YAML Idiosyncracies documentation for more information.

dns_check

New in version 2017.7.0.

Return the ip resolved by dns, but do not exit on failure, only raise an exception. Obeys system preference for IPv4/6 address resolution.

Example:

{{ 'www.google.com' | dns_check }}

Returns:

'172.217.3.196'

File filters

is_text_file

New in version 2017.7.0.

Return if a file is text.

Uses heuristics to guess whether the given file is text or binary, by reading a single block of bytes from the file. If more than 30% of the chars in the block are non-text, or there are NUL ('x00') bytes in the block, assume this is a binary file.

Example:

{{ '/etc/salt/master' | is_text_file }}

Returns:

True

is_binary_file

New in version 2017.7.0.

Return if a file is binary.

Detects if the file is a binary, returns bool. Returns True if the file is a bin, False if the file is not and None if the file is not available.

Example:

{{ '/etc/salt/master' | is_binary_file }}

Returns:

False

is_empty_file

New in version 2017.7.0.

Return if a file is empty.

Example:

{{ '/etc/salt/master' | is_empty_file }}

Returns:

False

file_hashsum

New in version 2017.7.0.

Return the hashsum of a file.

Example:

{{ '/etc/salt/master' | file_hashsum }}

Returns:

02d4ef135514934759634f10079653252c7ad594ea97bd385480c532bca0fdda

list_files

New in version 2017.7.0.

Return a recursive list of files under a specific path.

Example:

{{ '/etc/salt/' | list_files | join('\n') }}

Returns:

/etc/salt/master
/etc/salt/proxy
/etc/salt/minion
/etc/salt/pillar/top.sls
/etc/salt/pillar/device1.sls

path_join

New in version 2017.7.0.

Joins absolute paths.

Example:

{{ '/etc/salt/' | path_join('pillar', 'device1.sls') }}

Returns:

/etc/salt/pillar/device1.sls

which

New in version 2017.7.0.

Python clone of /usr/bin/which.

Example:

{{ 'salt-master' | which }}

Returns:

/usr/local/salt/virtualenv/bin/salt-master

Tests

Saltstack extends builtin tests with these custom tests:

equalto

Tests the equality between two values.

Can be used in an if statement directly:

{% if 1 is equalto(1) %}
    < statements >
{% endif %}

If clause evaluates to True

or with the selectattr filter:

{{ [{'value': 1}, {'value': 2} , {'value': 3}] | selectattr('value', 'equalto', 3) | list }}

Returns:

[{'value': 3}]

match

Tests that a string matches the regex passed as an argument.

Can be used in a if statement directly:

{% if 'a' is match('[a-b]') %}
    < statements >
{% endif %}

If clause evaluates to True

or with the selectattr filter:

{{ [{'value': 'a'}, {'value': 'b'}, {'value': 'c'}] | selectattr('value', 'match', '[b-e]') | list }}

Returns:

[{'value': 'b'}, {'value': 'c'}]

Test supports additional optional arguments: ignorecase, multiline

Jinja in Files

Jinja_ can be used in the same way in managed files:

# redis.sls
/etc/redis/redis.conf:
    file.managed:
        - source: salt://redis.conf
        - template: jinja
        - context:
            bind: 127.0.0.1
# lib.sls
{% set port = 6379 %}
# redis.conf
{% from 'lib.sls' import port with context %}
port {{ port }}
bind {{ bind }}

As an example, configuration was pulled from the file context and from an external template file.

Note

Macros and variables can be shared across templates. They should not be starting with one or more underscores, and should be managed by one of the following tags: macro, set, load_yaml, load_json, import_yaml and import_json.

Escaping Jinja

Occasionally, it may be necessary to escape Jinja syntax. There are two ways to do this in Jinja. One is escaping individual variables or strings and the other is to escape entire blocks.

To escape a string commonly used in Jinja syntax such as {{, you can use the following syntax:

{{ '{{' }}

For larger blocks that contain Jinja syntax that needs to be escaped, you can use raw blocks:

{% raw %}
    some text that contains jinja characters that need to be escaped
{% endraw %}

See the Escaping section of Jinja's documentation to learn more.

A real-word example of needing to use raw tags to escape a larger block of code is when using file.managed with the contents_pillar option to manage files that contain something like consul-template, which shares a syntax subset with Jinja. Raw blocks are necessary here because the Jinja in the pillar would be rendered before the file.managed is ever called, so the Jinja syntax must be escaped:

{% raw %}
- contents_pillar: |
    job "example-job" {
      <snipped>
      task "example" {
          driver = "docker"

          config {
              image = "docker-registry.service.consul:5000/example-job:{{key "nomad/jobs/example-job/version"}}"
      <snipped>
{% endraw %}

Calling Salt Functions

The Jinja renderer provides a shorthand lookup syntax for the salt dictionary of execution function.

New in version 2014.7.0.

# The following two function calls are equivalent.
{{ salt['cmd.run']('whoami') }}
{{ salt.cmd.run('whoami') }}

Debugging

The show_full_context function can be used to output all variables present in the current Jinja context.

New in version 2014.7.0.

Context is: {{ show_full_context()|yaml(False) }}

Logs

New in version 2017.7.0.

Yes, in Salt, one is able to debug a complex Jinja template using the logs. For example, making the call:

{%- do salt.log.error('testing jinja logging') -%}

Will insert the following message in the minion logs:

2017-02-01 01:24:40,728 [salt.module.logmod][ERROR   ][3779] testing jinja logging

Python Methods

A powerful feature of jinja that is only hinted at in the official jinja documentation is that you can use the native python methods of the variable type. Here is the python documentation for string methods.

{% set hostname,domain = grains.id.partition('.')[::2] %}{{ hostname }}
{% set strings = grains.id.split('-') %}{{ strings[0] }}

Custom Execution Modules

Custom execution modules can be used to supplement or replace complex Jinja. Many tasks that require complex looping and logic are trivial when using Python in a Salt execution module. Salt execution modules are easy to write and distribute to Salt minions.

Functions in custom execution modules are available in the Salt execution module dictionary just like the built-in execution modules:

{{ salt['my_custom_module.my_custom_function']() }}

Custom Jinja filters

Given that all execution modules are available in the Jinja template, one can easily define a custom module as in the previous paragraph and use it as a Jinja filter. However, please note that it will not be accessible through the pipe.

For example, instead of:

{{ my_variable | my_jinja_filter }}

The user will need to define my_jinja_filter function under an extension module, say my_filters and use as:

{{ salt.my_filters.my_jinja_filter(my_variable) }}

The greatest benefit is that you are able to access thousands of existing functions, e.g.:

  • get the DNS AAAA records for a specific address using the dnsutil:

    {{ salt.dnsutil.AAAA('www.google.com') }}
    
  • retrieve a specific field value from a Redis hash:

    {{ salt.redis.hget('foo_hash', 'bar_field') }}
    
  • get the routes to 0.0.0.0/0 using the NAPALM route:

    {{ salt.route.show('0.0.0.0/0') }}