The basics of grains, pillars, and templates
Grains and pillars provide a means of allowing user-defined variables to be used in conjunction with a minion. Templates can take advantage of those variables to create files on a minion that are specific to that minion.
Before we get into details, let me start off by clarifying a couple of things: grains are defined by the minion which they are specific to, while pillars are defined on the master. Either can be defined statically or dynamically (this book will focus on static), but grains are generally used to provide data that is unlikely to change, at least without restarting the minion, while pillars tend to be more dynamic.
Using grains for minion-specific data
Grains were originally designed to describe the static components of a minion, so that execution modules could detect how to behave appropriately. For instance, minions which contain the Debian os_family grain are likely to use the apt suite of tools for package management. Minions which contain the RedHat os_family grain are likely to use yum
for package management.
A number of grains will automatically be discovered by Salt. Grains such as os, os_family, saltversion, and pythonversion are likely to be always available. Grains such as shell, systemd, and ps are not likely to be available on, for instance, Windows minions.
Grains are loaded when the minion process starts up, and then cached in memory. This improves minion performance, because the Salt-minion process doesn't need to rescan the system for every operation. This is critical to Salt, because it is designed to execute tasks immediately, and not wait several seconds on each execution.
To discover which grains are set on a minion, use the grains.items
function:
salt myminion grains.items
To look at only a specific grain, pass its name as an argument to grains.item
:
salt myminion grains.item os_family
Custom grains can be defined as well. Previously, static grains were defined in the minion configuration file (/etc/salt/minion
on Linux and some Unix platforms):
grains:
foo: bar
baz: qux
However, while this is still possible, it has fallen out of favor. It is now more common to define static grains in a file called grains (/etc/salt/grains
on Linux and some Unix platforms). Using this file has some advantages:
- Grains are stored in a central, easy-to-find location
- Grains can be modified by the grains execution module
That second point is important: whereas the minion configuration file is designed to accommodate user comments, the grains file is designed to be rewritten by Salt as necessary. Hand-editing the grains file is fine, but don't expect any comments to be preserved. Other than not including the grains top-level declaration, the grains file looks like the grains configuration in the minion file:
foo: bar
baz: qux
To add or modify a grain in the grains file, use the grains.setval
function:
salt myminion grains.setval mygrain 'This is the content of mygrain'
Grains can contain a number of different types of values. Most grains contain only strings, but lists are also possible:
my_items:
- item1
- item2
In order to add an item to this list, use the grains.append
function:
salt myminion grains.append my_items item3
In order to remove a grain from the grains file, use the grains.delval
function:
salt myminion grains.delval my_items
Centralizing variables with pillars
In most instances, pillars behave in much the same way as grains, with one important difference: they are defined on the master, typically in a centralized location. By default, this is the /srv/pillar/
directory on Linux machines. Because one location contains information for multiple minions, there must be a way to target that information to the minions. Because of this, SLS files are used.
The top.sls
file for pillars is identical in configuration and function to the top.sls
file for states: first an environment is declared, then a target, then a list of SLS files that will be applied to that target:
base:
'*':
- bash
Pillar SLS files are much simpler than state SLS files, because they serve only as a static data store. They define key/value pairs, which may also be hierarchical.
skel_dir: /etc/skel/
role: web
web_content:
images:
- jpg
- png
- gif
scripts:
- css
- js
Like state SLS files, pillar SLS files may also include other pillar SLS files.
include:
- users
To view all pillar data, use the pillar.items
function:
salt myminion pillar.items
Take note that in older versions of Salt, when running this command, by default the master's configuration data will appear as a pillar item called master. This can cause problems if the master configuration includes sensitive data. To disable this output, add the following line to the master configuration:
pillar_opts: False
Fortunately, this option defaults to False as of Salt version 2015.5.0. This is also a good time to mention that, outside the master configuration data, pillars are only viewable to the minion or minions to which they are targeted. In other words, no minion is allowed to access another minion's pillar data, at least by default. It is possible to allow a minion to perform master commands using the peer system, but that is outside the scope of this chapter.
Managing files dynamically with templates
Salt is able to use templates, which take advantage of grains and pillars, to make the state system more dynamic. A number of other templating engines are also available, including (as of version 2016.3) the following:
- jinja
- mako
- wempy
- cheetah
- genshi
These are made available via Salt's rendering system. The preceding list only contains renderers that are typically used as templates to create configuration files and the like. Other renderers are available as well, but are designed more to describe data structures:
- yaml
- yamlex
- json
- json5
- msgpack
- py
- pyobjects
- pydsl
Finally, the following Renderer can decrypt GPG data stored on the master, before passing it through another renderer:
By default, state SLS files will be sent through the Jinja renderer, and then the yaml renderer. There are two ways to switch an SLS file to another renderer. First, if only one SLS file needs to be rendered differently, the first line of the file can contain a shabang line that specifies the renderer:
#!py
The shabang can also specify multiple Renderers, separated by pipes, in the order in which they are to be used. This is known as a render pipe. To use Mako and JSON instead of Jinja and YAML, use:
#!mako|json
To change the system default, set the renderer option in the master configuration file. The default is:
renderer: yaml_jinja
It is also possible to specify the templating engine to be used on a file that created the minion using the file.managed
state:
apache2_conf:
file:
- managed
- name: /etc/apache2/apache2.conf
- source: salt://apache2/apache2.conf
- template: jinja
Because Jinja is by far the most commonly-used templating engine in Salt, we will focus on it here. Jinja is not hard to learn, and a few basics will go a long way.
Variables can be referred to by enclosing them in double-braces. Assuming a grain is set called user
, the following will access it:
The user {{ grains['user'] }} is referred to here.
Pillars can be accessed in the same way:
The user {{ pillar['user'] }} is referred to here.
However, if the user
pillar or grain is not set, the template will not render properly. A safer method is to use the salt
built-in to cross-call an execution module:
The user {{ salt['grains.get']('user', 'larry') }} is referred to here.
The user {{ salt['pillar.get']('user', 'larry') }} is referred to here.
In both of these examples, if the user
has not been set, then larry
will be used as the default.
We can also make our templates more dynamic by having them search through grains and pillars for us. Using the config.get
function, Salt will first look inside the minion's configuration. If it does not find the requested variable there, it will check the grains. Then it will search pillar. If it can't find it there, it will look inside the master configuration. If all else fails, it will use the default provided.
The user {{ salt['config.get']('user', 'larry') }} is referred to here.
Codeblocks are enclosed within braces and percent signs. To set a variable that is local to a template (that is, not available via config.get
), use the set
keyword:
{% set myvar = 'My Value' %}
Because Jinja is based on Python, most Python data types are available. For instance, lists and dictionaries:
{% set mylist = ['apples', 'oranges', 'bananas'] %}
{% set mydict = {'favorite pie': 'key lime', 'favorite cake': 'saccher torte'} %}
Jinja also offers logic that can help define which parts of a template are used, and how. Conditionals are performed using if
blocks. Consider the following example:
{% if grains['os_family'] == 'Debian' %}
apache2:
{% elif grains['os_family'] == 'RedHat' %}
httpd:
{% endif %}
pkg:
- installed
service:
- running
The Apache package is called apache2
on Debian-style systems, and httpd
on RedHat-style systems. However, everything else in the state is the same. This template will auto-detect the type of system that it is on, install the appropriate package, and start the appropriate service.
Loops can be performed using for
blocks, as follows:
{% set berries = ['blue', 'rasp', 'straw'] %}
{% for berry in berries %}
{{ berry }}berry
{% endfor %}