Salt state renderers

The Salt state system operates by gathering information from simple data structures. The state system was designed in this way to make interacting with it generic and simple. This also means that state files (SLS files) can be one of many formats.

By default SLS files are rendered as Jinja templates and then parsed as YAML documents. But since the only thing the state system cares about is raw data, the SLS files can be any structured format.

Currently Salt supports:

  • Jinja + YAML
  • Mako + YAML
  • Wempy + YAML
  • Jinja + JSON
  • Mako + JSON
  • Wempy + JSON

Using the Jinja renderer

SLS modules may require programming logic or inline execution. This is accomplished with module templating. The default module templating system used is Jinja and may be configured by changing the renderer value in the master config.

All states are passed through a templating system when they are initially read. To make use of the templating system, simply add some templating markup. An example of an sls module with templating markup may look like this:

{% for usr in ['moe','larry','curly'] %}
{{ usr }}:
  user.present
{% endfor %}

This templated sls file once generated will look like this:

moe:
  user.present
larry:
  user.present
curly:
  user.present

A more complex example:

# Comments in yaml start with a hash symbol.
# Since jinja rendering occurs before yaml parsing, if you want to include jinja
# in the comments you may need to escape them using 'jinja' comments to prevent
# jinja from trying to render something which is not well-defined jinja.
# e.g.
# {# iterate over the Three Stooges using a {% for %}..{% endfor %} loop
# with the iterator variable {{ usr }} becoming the state ID. #}
{% for usr in 'moe','larry','curly' %}
{{ usr }}:
  group:
    - present
  user:
    - present
    - gid_from_name: True
    - require:
      - group: {{ usr }}
{% endfor %}

The SLS data structure in YAML

One of Salt’s strengths, the use of existing serialization systems for representing SLS data, can also backfire. YAML is a general purpose system and there are a number of things that would seem to make sense in an sls file that cause YAML issues. It is wise to be aware of these issues. While reports or running into them are generally rare they can still crop up at unexpected times.

Indentation

YAML uses a fixed indentation scheme to represent relationships between data layers. Salt requires that the indentation for each level consists of exactly two spaces.

Warning

YAML uses spaces, period. Do not use tabs in your SLS files!

Colons

Python dictionaries are simple key-value pairs. Users from other languages may recognize this data type as hashes or associative arrays.

Dictionary keys are represented in YAML as strings terminated by a trailing colon. Values are represented by either a string following the colon, separated by a space:

my_key: my_value

Dashes

To represent lists of items, a single dash followed by a space is used. Multiple items are a part of the same list as a function of their having the same level of indentation.

item_list:
  - list_value_one
  - list_value_two
  - list_value_three

Salt functions in Jinja

Salt includes the Jinja2 templating engine which can be used in state files, pillar files, and other files managed by Salt. Salt lets you use Jinja to access minion configuration values, grains and pillar data, and call Salt execution modules. This is in additional to the standard control structures and Python data types that are already available in Jinja. Full documentation of Jinja2 features can be found at http://jinja.pocoo.org/docs/dev/.

Conditionals

One of the most common uses of Jinja is to insert conditional statements into pillar files. Because many distros have different package names, you can use the os grain to set platform specific paths, package names, and other values.

For example:

{% if grains['os_family'] == 'RedHat' %}
apache: httpd
git: git
{% elif grains['os_family'] == 'Debian' %}
apache: apache2
git: git-core
{% endif %}

As you can see, grains are available in a dictionary much like pillar. This example checks grain values to set OS specific pillar keys.

Save the snippet above to the /srv/salt/pillar/common.sls file, and then run the following commands to refresh and then list pillar values for each minions:

salt '*' saltutil.refresh_pillar
salt '*' pillar.items

Your minions should list the values set for the Debian OS family.

After setting these values, when you apply the following state:

install_apache:
  pkg.installed:
    - name: {{ pillar['apache'] }}

The httpd package is installed on RedHat, while the apache2 package is installed on Debian.

Using loops

Loops are useful for creating users or folders in states for example.

{% for usr in ['moe','larry','curly'] %}
{{ usr }}:
  user.present
{% endfor %}

{% for user, dir in {'john': 'dir1', 'joe': '/dir2', 'jane': '/dir3'}.iteritems() %}
{{ dir }}:
  file.directory:
    - user: {{ user }}
    - mode: 774
{% endfor %}

In general, you should try to keep your states as simple as possible. If you find yourself writing complex Jinja, you should consider breaking a task into multiple states, or writing a custom Salt execution module for the task (this is easier than it sounds, especially if you know some Python!).

Using grains

Often times a state will need to behave differently on different systems. Salt grains objects are made available in the template context. The grains can be used from within sls modules:

apache:
  pkg.installed:
    {% if grains['os'] == 'RedHat' %}
    - name: httpd
    {% elif grains['os'] == 'Ubuntu' %}
    - name: apache2
    {% endif %}

Using environment variables

You can use salt['environ.get']('VARNAME') to use an environment variable in a Salt state.

MYENVVAR="world"

Create a file with contents from an environment variable:

file.managed:
  - name: /tmp/hello
  - contents: {{ salt['environ.get']('MYENVVAR') }}

And execute the environment variable:

salt-call state.template test.sls

Lab: Conditionals in States

Create webserver2.sls to install package depending on the operating system of managed server.

apache:
  pkg.installed:
    {% if grains['os'] == 'RedHat' %}
    - name: httpd
    {% elif grains['os'] == 'Ubuntu' %}
    - name: apache2
    {% endif %}

Lab: Using loops

Adapt apache.sls pillar to manage Apache modules:

modules:
  enabled:  # List modules to enable
    - ldap
    - ssl
  disabled:  # List modules to disable
    - rewrite

And create apache.sls state file.

include:
  - apache

{% if grains['os_family']=="Debian" %}

{% for module in salt['pillar.get']('apache:modules:enabled', []) %}
a2enmod {{ module }}:
  cmd.run:
    - unless: ls /etc/apache2/mods-enabled/{{ module }}.load
    - order: 225
    - require:
      - pkg: apache
    - watch_in:
      - module: apache-restart
{% endfor %}

{% for module in salt['pillar.get']('apache:modules:disabled', []) %}
a2dismod {{ module }}:
  cmd.run:
    - onlyif: ls /etc/apache2/mods-enabled/{{ module }}.load
    - order: 225
    - require:
      - pkg: apache
    - watch_in:
      - module: apache-restart
{% endfor %}

{% elif grains['os_family']=="RedHat" %}
 
{% for module in salt['pillar.get']('apache:modules:enabled', []) %}
find /etc/httpd/ -name '*.conf' -type f -exec sed -i -e 's/\(^#\)\(\s*LoadModule.{{ module }}_module\)/\2/g' {} \;:
  cmd.run:
    - unless: httpd -M 2> /dev/null | grep "[[:space:]]{{ module }}_module"
    - order: 225
    - require:
      - pkg: apache
    - watch_in:
      - module: apache-restart
{% endfor %}

{% for module in salt['pillar.get']('apache:modules:disabled', []) %}
find /etc/httpd/ -name '*.conf' -type f -exec sed -i -e 's/\(^\s*LoadModule.{{ module }}_module\)/#\1/g' {} \;:
  cmd.run:
    - onlyif: httpd -M 2> /dev/null | grep "[[:space:]]{{ module }}_module"
    - order: 225
    - require:
      - pkg: apache
    - watch_in:
      - module: apache-restart
{% endfor %}

{% endif %}

Run the above script on both svc01 and svc02 nodes.