Mastering SaltStack
上QQ阅读APP看书,第一时间看更新

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, 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

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 2015.5) 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
  • msgpack
  • py
  • pyobjects
  • pydsl

Finally, the following Renderer can decrypt GPG data stored on the Master, before passing it through another renderer:

  • gpg

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

A quick Jinja primer

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.

Code blocks 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 %}