Chapter 4. Configuration Management: Salt States

The remote execution framework provides the basis for a number of higher-level abstractions. Running remote commands on a number of minions is great. But when you add another web server or another database server, hopefully that new server will have something in common with other servers. Reusing components helps maintain a base level of consistency in your environment. Salt provides a simple but powerful file format that allows you to specify a desired recipe, or state, describing how you want a host to look, and then you simply apply that state. The states can be combined so you can build on simple pieces to make more complicated states.

Tip

You can find the complete list of state modules on the SaltStack website.

State File Overview

You describe a state via Salt state (SLS) files. As with most of Salt’s core, the most basic format is YAML. One of the big advantages of YAML is that it is language-agnostic; it is just a data format. The format of the states uses standard data structure constructs:

  • Strings

  • Numbers

  • Arrays (lists)

  • Hashes (dictionaries)

Note

It is important to remember that YAML is just a simple representation of the data structure. You can alter the underlying file format if you use a different renderer.

SLS Example: Adding a User

In the previous chapter, we added a single user on a host. But we want this user, and the rest of the users, to be added automatically every time we add another machine. Let’s handle just the wilma user for the moment. Here’s a very simple SLS file to add the wilma user:

user_wilma:
  user.present:
    - name: wilma
    - fullname: Wilma Flintstone
    - uid: 2001
    - home: /home/wilma

We are using the same basic information as before, and we have added a little more. We are using the state module called user and the function present. In the previous chapter, we discussed execution modules. Now, we are instead using state modules. They often look very similar and sometimes have the same arguments, but they are different. When we added a user, we used the user.add execution function. Now, we want to make sure the user exists using the user.present state function. At their core, state modules rely on execution modules to actually make the changes needed, but state modules will add further functionality on top of that. In the case of user.present, we only want to call the execution function user.add if we really need to add that user. If the user already exists, then we can skip it. Since there is a logical difference between running an add user command versus running an add user command if the user is missing, the function names may be different. The user state module, like other state modules, will make a change only if it detects there is a delta between the real state and the desired state. The side effect is that you can run a state over and over again, and as long as there is no delta, nothing will change. In other words, state calls are idempotent.

SLS format and state documentation

Let’s explore the format of the SLS file for a moment. As we said, it is a standard YAML-formatted file. The first line is an ID that can be referenced in other state files or later in the file. We will use the state IDs heavily when we order states in “State Ordering”.

Next is the command or state declaration. In the previous chapter, we talked about using sys.doc to look at the documentation for a given module. But execution functions and state functions are not the same. Fortunately, there is another sys function that can help us out: sys.state_doc:

[vagrant@master ~]$ sudo salt-call sys.state_doc user.present
local:
    ----------
    user:
        Management of user accounts
        ===========================
        The user module is used to create and manage user settings, users can be
        set as either absent or present
<snip>

If you run the preceding command, you will see the rest of the options. Those options are what appear next in the state file. In our specific case, this includes options for the full name, the user ID (uid), and the home directory.

One last thing we should mention: the first argument, name, can be used as the ID of the state itself. So we can rewrite the preceding state as the following:

wilma:
  user.present:
    - fullname: Wilma Flintstone
    - uid: 2001
    - home: /home/wilma

In this case, it is implied that the name (aka login) of the user is the same as the ID of the state itself. This can be very handy and can simplify your states a little. However, these state names can be referenced elsewhere and may cause more confusion than it’s worth. With usernames, it isn’t quite as obvious as with, say, names of packages. So be aware of this shortcut, but use it with caution.

When we introduced sys.doc, we also mentioned sys.list_modules and sys.list_functions. There are corresponding calls for state modules and functions: sys.list_state_modules and sys.list_state_functions:

[vagrant@master ~]$ sudo salt-call sys.list_state_modules
local:
    - alias
<snip>
    - user
<snip>

[vagrant@master ~]$ sudo salt-call sys.list_state_functions user
local:
    - user.absent
    - user.present

Explore the various state functions within the sys module to become more familiar with the large list of state modules available within Salt.

Setting the file roots

The state file is great, but what do we do with it? The first thing we need to do is tell the Salt master where to find the files. In Salt’s terms, we need to set up the file server. We have mentioned the master configuration file: /etc/salt/master. We could easily edit that file, but we could also create some smaller files in a directory: /etc/salt/master.d. The main configuration file is a bit large and unwieldy, but the default configuration has an include statement that will grab all of the files matching /etc/salt/master.d/*.conf:

[vagrant@master ~]$ sudo grep default_include /etc/salt/master
#default_include: master.d/*.conf

(The master config file has many of the defaults listed, but they’ve been commented out just to highlight what the default settings are.)

[vagrant@master ~]$ sudo cat /etc/salt/master.d/file-roots.conf
file_roots:
  base:
  - /srv/salt/file/base

Add that file and then restart the Salt master:

[vagrant@master ~]$ sudo service salt-master restart
Stopping salt-master daemon:                               [  OK  ]
Starting salt-master daemon:                               [  OK  ]

When we introduced the saltutil execution module, we demonstrated syncing from the master to a minion. For many of the files synced, the file_roots configuration option specifies the directory where you can find them. Salt has a small built-in file server that copies any necessary files between hosts. This file server communicates over the standard ZeroMQ channels that the rest of Salt uses, so the files are transferred securely and without any additional configuration.

Salt can partition minions into overlapping groups called environments. Right now, we are concerned only with the base environment, indicated by the base keyword.

Executing a state file

We have set up our Salt master with our file_roots directory, which is necessary for using states. We will add the preceding example state definition to a file inside file_roots:

[vagrant@master ~]$ cat /srv/salt/file/base/user-wilma.sls
user_wilma:
  user.present:
  - name: wilma
  - fullname: Wilma Flintstone
  - uid: 2001
  - home: /home/wilma

Now we will introduce the state execution module.

These terms may be getting a little confusing. There are many state modules, such as pkg and user. There are also many execution modules, such as cmd and sys. But, in order to execute states, you need to run something (i.e., an execution module). As a result, there is an execution module called state. This is how you run state modules, but state itself is not a state module. If this doesn’t make sense, hopefully it will after you use Salt for a while.

As with all Salt commands, we can use sys.doc to get an idea of state’s capabilities. The first function we will introduce is state.show_sls:

[vagrant@master ~]$ sudo salt master.example state.show_sls user-wilma
master.example:
    ----------
    user_wilma:
        ----------
        __env__:
            base
        __sls__:
            user-wilma
        user:
            |_
              ----------
              name:
                  wilma
            |_
              ----------
              fullname:
                  Wilma Flintstone
            |_
              ----------
              uid:
                  2001
            |_
              ----------
              home:
                  /home/wilma
            - present
            |_
              ----------
              order:
                  10000

This shows the basic data structure Salt uses after reading the file. Most of it should look pretty familiar. You can see all of the various arguments to user.present, as well as the declaration of the state function itself, albeit broken into a couple of different lines. There is the reference to the user state module toward the top. But the specific function, present, is given at the bottom. What is important to recognize is that the module (user), and the specific function (present), are joined in the original state file, but Salt pulls them apart when parsing the file. We won’t be using that fact in this book, but it’s worth noting.

Tip

I (Craig) use state.show_sls almost every day. I use it to verify and debug almost every state (SLS file) I write. It is extremely handy to see how Salt parses the SLS file and if it matches everything I expect. Many simple syntax errors, including common YAML errors, will be caught by state.show_sls, without affecting any minions. So it is a very handy tool that you should learn.

We can run this state against minion2 and we should see very little change since we already added that user. To execute the state, we simply call state.sls:

[vagrant@master ~]$ sudo salt minion2.example state.sls user-wilma
minion2.example:
 ---------
          ID: user_wilma
    Function: user.present
        Name: wilma
      Result: True
     Comment: Updated user wilma
     Started: 06:16:52.541678
    Duration: 193.926 ms
     Changes:
              ----------
              fullname:
                  Wilma Flintstone
              uid:
                  2001

Summary
 -----------
Succeeded: 1 (changed=1)
Failed:    0
 -----------
Total states run:     1

The important thing to notice is that after the state is applied, Salt will show you what changed. In this example, some of the user data already existed. But the fullname and the uid did change and Salt reported those details. If we run this once again, we should see no change this time:

[vagrant@master ~]$ sudo salt minion2.example state.sls user-wilma
minion2.example:
 ---------
          ID: user_wilma
    Function: user.present
        Name: wilma
      Result: True
     Comment: User wilma is present and up to date
     Started: 06:19:32.235330
    Duration: 1.599 ms
     Changes:

Summary
 -----------
Succeeded: 1
Failed:    0
 -----------
Total states run:     1

This time the Changes: section is empty, indicating that nothing changed. This is very handy; we should be able to run this state many times without any undesirable changes. We will take advantage of this fact later using something called a highstate, which is a collection of states that, all together, form our complete definition of a host.

Working with the Multilayered State System

We have discussed repeatedly how the different pieces of Salt build on top of each other to present a great deal of functionality to the user. Even within each piece there can be multiple layers that allow the advanced user a great deal of flexibility and power, and also provide a newcomer sufficient power to get complex tasks done easily.

The state system is no exception.

state.single: Calling a state using data on the command line

At the very bottommost layer are the function calls themselves. They are similar to the execution modules, but they are distinct.

We can call the state functions directly by using the state.single execution module:

[vagrant@master ~]$ sudo salt minion2.example state.single user.present \
name=wilma fullname='Wilma Flintstone' uid=2001 home=/home/wilma
minion2.example:
 ---------
          ID: wilma
    Function: user.present
      Result: True
     Comment: User wilma is present and up to date
     Started: 06:25:18.704908
    Duration: 1.646 ms
     Changes:

Summary
 -----------
Succeeded: 1
Failed:    0
 -----------
Total states run:     1

This call to state.single says to execute the user.present state function in the same way that we specified the state in the state file, /srv/salt/file/base/user-wilma.sls. The arguments are the same between the two. This can come in handy when, say, you’re testing state functions.

Note

Notice how the ID is missing from the state.single call. Since this is a one-time call and only one state module is used, there is no reason to give it a unique ID.

The returned data is exactly the same as what we saw earlier when we used the SLS file:

sudo salt minion2.example state.sls user-wilma

Namely, the user is already present, so no action was taken.

state.low: States with raw data

As we progress up (or down, if you prefer) the state layers, we get further away from the familiar data format we saw in the SLS file. The next layer is called the low chunk. At this layer, the state is completely abstracted out as data. We mentioned that the state function we called, user.present, is actually a combination of two pieces that are just conveniently joined. When we call the low chunk, we see how that is represented by different parts of the data structure:

[vagrant@master ~]$ sudo salt minion2.example state.low \
        '{state: user, fun: present, name: wilma}'
minion2.example:
    ----------
    __run_num__:
        0
    changes:
        ----------
    comment:
        User wilma is present and up to date
    duration:
        1.785
    name:
        wilma
    result:
        True
    start_time:
        06:34:50.250740

In this call, we specify the state (user) and the function (present) as two different parts of the data structure. You can view this data for an existing SLS file by using state.show_low_sls.

Note

A few arguments were simply left off for brevity. You can specify all of the same arguments using state.low.

Hopefully, you won’t need to dig this deep into states when building your own systems. But this functional foundation may prove useful when you get stuck and cannot figure out what is happening with your states; you can start peering down into the rabbit hole. Next, we will go in the opposite direction and talk about higher-level abstractions that allow us to build a complete host recipe.

Highstate and the Top File

Now that we’ve gone into the low levels of the state system, we want to look at the real power that lies with combining states. The example we have used until now has just been one file, and we have called it directly using state.sls. But this is not the power we are referring to. We want to be able to add a new host, annotate it (as, say, a web server), and then just have all of the right packages installed, users set up, and so on. Essentially, we want the correct recipe applied to the given host. This means not only combining many states together, but also knowing which combination to run on which machine. The highstate layer is used to combine various states together. We will discuss that next when we introduce the top file.

The Top File

We want to combine states into more complex highstates. The file that defines this state is called the top file and it normally named top.sls. It appears in the file_roots directory. When we set up file_roots, we mentioned the base environment. The top file goes into this environment. We start with a very simple top file that executes our state to add the wilma user:

[vagrant@master ~]$ cat /srv/salt/file/base/top.sls
base:
  'minion2.example':
  - user-wilma

At the highest level in the top file is the environment. So far, we have dealt only with a single environment: base. In order to keep things simple, we will continue to use only base for a while.

Next, you have the targeted minion. In this case, we want only the state, user-wilma, added to a single minion. But, in the general case, in each environment you give a list of targets. This targeting is exactly the same as what we saw in Chapter 2. But, just as with the single environment, let’s keep it simple for now and focus only on minion IDs.

We can view the effective top file for any minion using the state.show_top command:

[vagrant@master ~]$ sudo salt minion2.example state.show_top
minion2.example:
    ----------
    base:
        - user-wilma

The output shows how the top file would be generated for that specific minion, minion2. For another example, let’s try running against another minion:

[vagrant@master ~]$ sudo salt master.example state.show_top
master.example:
    ----------

In this case, the top file is shown as empty because there is only a single target and it doesn’t apply to master.example.

Before adding the rest of the users, let’s suppose we want to add the vim package on every host. We will use the pkg.installed state function, but this time we will put the file into a subdirectory to give us a little more structure in our file layout. Since we are going to install the package on every host, we will simply call the directory default and the file packages.sls:

[vagrant@master ~]$ cat /srv/salt/file/base/default/packages.sls
packages_vim:
  pkg.installed:
    - name: vim

Then let’s add it for every host (*) in our modified top file:

[vagrant@master ~]$ cat /srv/salt/file/base/top.sls
base:
  '*':
    - default.packages

  'minion2.example':
    - user-wilma

Directories are not denoted with slashes, but with dots. So, a state directory of a/b/c/d would be given as a.b.c.d in the top_file.

Since we have a more interesting top file, we can start to discuss executing a highstate. Since a highstate execution references the top file, there is no need to specify any arguments. The target given in the top file will create a unique run on every minion.

If we run it, we see a problem:

[vagrant@master ~]$ sudo salt \* state.highstate
minion2.example:
 ---------
          ID: packages_vim
    Function: pkg.installed
        Name: vim
      Result: False
     Comment: Package 'vim' not found (possible matches: vim-enhanced)
     Started: 08:05:40.253299
    Duration: 22321.085 ms
     Changes:
 ---------
          ID: user_wilma
    Function: user.present
        Name: wilma
      Result: True
     Comment: User wilma is present and up to date
     Started: 08:06:02.574640
    Duration: 5.033 ms
     Changes:

Summary
 -----------
Succeeded: 1
Failed:    1
 -----------
Total states run:     2
minion3.example:
 ---------
          ID: packages_vim
    Function: pkg.installed
        Name: vim
      Result: True
     Comment: Package vim is already installed.
     Started: 08:05:57.273826
    Duration: 10550.292 ms
     Changes:

Summary
 -----------
Succeeded: 1
Failed:    0
 -----------
Total states run:     1

We have two different operating systems that call the vim package different things. It installed fine on the Ubuntu hosts, but CentOS needs us to install the vim-enhanced package. We can adjust things slightly to handle this for now, which means breaking up the all hosts (*) target. We mentioned the concept of grains back in Chapter 2, and we briefly explored target using grains. We can definitely use this concept within the top file:1

[vagrant@master ~]$ cat /srv/salt/file/base/top.sls
base:
  'os:CentOS':
    - match: grain
    - default.vim-enhanced

  'os:Ubuntu':
    - match: grain
    - default.vim

  'minion2.example':
    - user-wilma

Next we create two new files: vim.sls and vim-enhanced.sls. (You may as well delete the old packages.sls; we won’t reference it, but we will come back to it in Chapter 5.)

[vagrant@master ~]$ cat /srv/salt/file/base/default/vim.sls
packages_vim:
  pkg.installed:
    - name: vim
[vagrant@master ~]$ cat /srv/salt/file/base/default/vim-enhanced.sls
packages_vim:
  pkg.installed:
    - name: vim-enhanced

We can rerun our state.highstate and we should see everything run without any more issues:

[vagrant@master ~]$ sudo salt \* state.highstate
minion3.example:
 ---------
          ID: packages_vim
    Function: pkg.installed
        Name: vim
      Result: True
     Comment: Package vim is already installed.
     Started: 01:28:47.448266
    Duration: 705.834 ms
     Changes:

Summary
 -----------
Succeeded: 1
Failed:    0
 -----------
Total states run:     1
minion4.example:
 ---------
          ID: packages_vim
    Function: pkg.installed
        Name: vim
      Result: True
     Comment: Package vim is already installed.
<snip>

We have a package installed on every host, with some minor differences on our two operating systems. We should return to our users and get all of them installed. Table 3-1 listed our minions with their roles, and Table 3-2 listed the users with their roles. When we combine them, we should get the list of users to add to every host, as shown in Table 4-1.

Table 4-1. Minion IDs and corresponding users
Minion ID Users

minion2

wilma

minion3

wilma, barney, betty

minion4

wilma, barney, betty, fred

We can use this to create more structure for the users, as well. We’ll create a users directory and put our files there. Also, we’ll create a file for each user, a file for both QA users (barney and betty), and a file with all of the users. Since we are creating some more structure with the users, let’s also remove the specific call to add wilma directly. Rather, let’s add all of the DBAs. The include statement will make this easy.

Let’s look at the top file:

[vagrant@master ~]$ cat /srv/salt/file/base/top.sls
base:
  'os:CentOS':
    - match: grain
    - default.vim-enhanced

  'os:Ubuntu':
    - match: grain
    - default.vim

  'minion2.example':
    - users.dba

  'minion3.example':
    - users.dba
    - users.qa

  'minion4.example':
    - users.all

The top file is really taking shape. We do have to individually specify the minion IDs, but we will fix that later.

We will create a state file for each user we want to add. This will give us the necessary flexibility in where we install the users. (We will group them in just a moment.)

[vagrant@master ~]$ cat /srv/salt/file/base/users/{wilma,fred,barney,betty}.sls
user_wilma:
  user.present:
  - name: wilma
  - fullname: Wilma Flintstone
  - uid: 2001
user_fred:
  user.present:
  - name: fred
  - fullname: Fred Flintstone
  - uid: 2002
user_barney:
  user.present:
  - name: barney
  - fullname: Barney Rubble
  - uid: 2003
user_betty:
  user.present:
  - name: betty
  - fullname: Betty Rubble
  - uid: 2004

(We removed the home directory. We just don’t need it any longer.)

Next, we have the grouped user files utilizing include statements:

[vagrant@master ~]$ cat /srv/salt/file/base/users/dba.sls
include:
- users.wilma
[vagrant@master ~]$ cat /srv/salt/file/base/users/qa.sls
include:
- users.barney
- users.betty

This is simple enough. Just as with the top file, directories are separated with dots, not slashes. One thing to note: all files included are referenced from a file root. Since we have only the single directory defined in file_roots, all state files must be specified relative to that single directory: /srv/salt/file/base. This can be a little tedious, especially if we continue to create more subdirectories. There is a shorthand: you can refer to state files in your current directory simply with a leading dot. Let’s use that shorthand with the all users state:

[vagrant@master ~]$ cat /srv/salt/file/base/users/all.sls
include:
- .fred
- .wilma
- .barney
- .betty
Tip

You can use the cp.list_states execution function to see how Salt sees the various states and represents them.

With a more complex top file, we can use state.show_top for a specific minion to make sure it looks as we expect:

[vagrant@master ~]$ sudo salt minion3.example state.show_top
minion3.example:
    ----------
    base:
        - default.vim
        - users.dba
        - users.qa

Now that we have a top file that looks good, we can simply run a highstate (state.highstate) against all of the minions, and the correct users and packages will get installed on every host:

[vagrant@master ~]$ sudo salt '*' state.highstate
minion2.example:
 ---------
          ID: packages_vim
    Function: pkg.installed
        Name: vim-enhanced
      Result: True
     Comment: Package vim-enhanced is already installed.
     Started: 01:37:18.806663
    Duration: 878.505 ms
     Changes:
 ---------
          ID: user_wilma
    Function: user.present
        Name: wilma
      Result: True
     Comment: User wilma is present and up to date
     Started: 01:37:19.685347
    Duration: 1.774 ms
     Changes:

Summary
 -----------
Succeeded: 2
Failed:    0
 -----------
Total states run:     2
minion3.example:
 ---------
<snip>

When we were running individual states, we used state.show_sls to show the lower-level state data structure. There is an analogous command for highstates: state.show_highstate:

[vagrant@master ~]$ sudo salt minion4.example state.show_highstate
minion4.example:
    ----------
    packages_vim:
        ----------
        __env__:
            base
        __sls__:
            default.vim
        pkg:
            |_
              ----------
              name:
                  vim
            - installed
            |_
              ----------
              order:
                  10000
<snip>

As you can see, the high-level declarations given in top.sls and the referenced state files are broken down into a Salt data structure. However, there is an added element: order. When you are running multiple states, the order in which they execute can be important. The states are ordered using a simple numeric sort. If you need to force an order in your states, there are a couple of options.

State Ordering

When you compile the states for highstate, the states will always be ordered and repeatable. However, the order that Salt generates may not be what you need. The require declaration will force a specific state to be executed before a given state. And there is also a way to watch another state and then execute code based on any changes. Lastly, you can peer into the future with prereq, which will look at other states to see if they will change. If they are going to change, then run the referencing state.

require: Depend on Another State

As we mentioned, there are many times when you need to ensure that one action happens before another. The require declaration will enforce that the named state executes before the current state. For example, if state A has a require for state B, then state B will always run before state A.

Before we get to the details of a require, let’s go back to the Nginx package we installed manually in the previous chapter using the pkg.install execution function.

We can verify the package using the pkg.version execution function:

[vagrant@master ~]$ sudo salt minion1\* pkg.version nginx
minion1.example:
    1.0.15-11.el6

Let’s now add a state to automatically install the Nginx package on minion1. Earlier, when we discussed our five example minions, we gave each one a role. We are going to add a little more structure to file_roots by adding a roles directory and then a webserver subdirectory:

[vagrant@master ~]$ cat /srv/salt/file/base/roles/webserver/packages.sls
roles_webserver_packages:
  pkg.installed:
  - name: nginx

We will also need to make sure the Nginx service is running:

[vagrant@master ~]$ cat /srv/salt/file/base/roles/webserver/start.sls
roles_webserver_start:
  service.running:
  - name: nginx
  - require:
    - pkg: nginx

As you can see, before we can actually start the Nginx service, we need to make sure that the Nginx package exists. The require declaration takes a list of dictionaries. The key of the dictionary is the name of the state module—in this case, pkg—and then the value of the dictionary is the name of the state. Remember, in this context it is the state’s name (nginx), not the ID (roles_webserver_packages).

Now we have to add these states to the top file. We could easily just add both of them to the minion1.example target. However, there is another shortcut: init.sls.

init.sls directory shortcut

We have referred to individual states using their filenames, minus the sls extension. However, we have not discussed how to reference a directory instead of an individual file. If there is a file named init.sls in a directory, then you can simply reference the directory name without init.sls.

If we continue our previous example, we can add webserver/roles/init.sls and then reference it in the top file:

[vagrant@master ~]$ cat /srv/salt/file/base/roles/webserver/init.sls
include:
- users.www
- .packages
- .start
[vagrant@master ~]$ cat /srv/salt/file/base/top.sls
base:
  'os:CentOS':
    - match: grain
    - default.vim-enhanced

  'os:Ubuntu':
    - match: grain
    - default.vim

  'minion1.example':
    - roles.webserver

  'minion2.example':
    - users.dba

  'minion3.example':
    - users.dba
    - users.qa

  'minion4.example':
    - users.all

The file roles/webserver/init.sls also makes use of the leading dot shorthand to reference files within the current directory. In our new top file, we have added a target for minion1.example and added a single state: roles.webserver. We also included another state, users/www.sls:

[vagrant@master ~]$ cat /srv/salt/file/base/users/www.sls
user_www:
  user.present:
  - name: www
  - fullname: WebServer User
  - uid: 5001

As you can see, we can include any file into another state. We simply have to reference it based off the main file root. We can now run a highstate on minion1:

[vagrant@master ~]$ sudo salt minion1\* state.highstate
minion1.example:
 ---------
          ID: packages_vim
    Function: pkg.installed
<snip>
 ---------
          ID: user_www
    Function: user.present
<snip>
 ---------
          ID: roles_webserver_packages
    Function: pkg.installed
<snip>
 ---------
          ID: roles_webserver_start
    Function: service.running
        Name: nginx
      Result: True
     Comment: Started Service nginx
     Started: 02:14:13.267952
    Duration: 371.647 ms
     Changes:
              ----------
              nginx:
                  True

Summary
 -----------
Succeeded: 4 (changed=1)
Failed:    0
 -----------
Total states run:     4

Most of the output should look familiar. We have left in the entire output from the roles_webserver_start state. As you can see, it reported back some changes (specifically, that the service was started up). The important part to note is that the package was verified before the service was started. While in a setup this small you may be able to skip the require, there will come a time when you will have to ensure that one state runs before another. Next, we will talk about how to execute an additional action only if another state reports a change.

watch: Run Based on Other Changes

Often when you deploy a new version of an application, you will need to restart the application to pick up these changes. The watch statement will execute additional states if any change is detected. We are going to create a fake website consisting of a single file. (You can easily extrapolate this idea to a package with many configuration files.) We are going to add another state: sites:

[vagrant@master ~]$ cat /srv/salt/file/base/sites/init.sls
sites_first:
  file.managed:
  - name: /usr/share/nginx/html/first.html
  - source: salt://sites/src/first.html
  - user: www
  - mode: 0644
  service.running:
  - name: nginx
  - watch:
    - file: /usr/share/nginx/html/first.html

And the single file we are going to manage:

[vagrant@master ~]$ cat /srv/salt/file/base/sites/src/first.html
<html>
<head><title>First Site</title></head>
<body>
<h3>First Site</h3>
</body></html>

Last, we need to add this new state to the top file so that the given host(s) will always have it applied on a highstate:

[vagrant@master ~]$ cat /srv/salt/file/base/top.sls
base:
  'os:CentOS':
    - match: grain
    - default.vim-enhanced

  'os:Ubuntu':
    - match: grain
    - default.vim

  'minion1.example':
    - roles.webserver
    - sites

  'minion2.example':
    - users.dba

  'minion3.example':
    - users.dba
    - users.qa

  'minion4.example':
    - users.all

As you can see, we have added this state only to minion1 for the moment. We need to execute a highstate on this host to get the new site (aka file) onto that host. Before we actually run this new state, let’s discuss a way to test states using test=true. You can add an argument of test=true to various state functions—most notably, state.sls and state.highstate. Before we run our highstate, let’s look at the new site state and what happens when we add the test flag:

[vagrant@master ~]$ sudo salt minion1.example state.sls sites test=true
minion1.example:
 ---------
          ID: sites_first
    Function: file.managed
        Name: /usr/share/nginx/html/first.html
      Result: None
     Comment: The file /usr/share/nginx/html/first.html is set to be changed
     Started: 21:59:40.821641
    Duration: 243.354 ms
     Changes:
              ----------
              newfile:
                  /usr/share/nginx/html/first.html
 ---------
          ID: sites_first
    Function: service.running
        Name: nginx
      Result: None
     Comment: Service is set to be restarted
     Started: 21:59:41.093111
    Duration: 25.794 ms
     Changes:

Summary
 -----------
Succeeded: 2 (unchanged=2, changed=1)
Failed:    0
 -----------
Total states run:     2
Tip

The test=true flag is very handy for debugging any issues you may see with state ordering.

Running a highstate with the same flag will give very similar results:

[vagrant@master ~]$ sudo salt minion1.example state.highstate test=true
minion1.example:
 ---------
          ID: packages_vim
    Function: pkg.installed
        Name: vim-enhanced
      Result: True
     Comment: Package vim-enhanced is already installed.
     Started: 22:04:16.955372
    Duration: 786.249 ms
     Changes:
<snip>
 ---------
          ID: sites_first
    Function: file.managed
        Name: /usr/share/nginx/html/first.html
      Result: None
     Comment: The file /usr/share/nginx/html/first.html is set to be changed
     Started: 22:04:17.774924
    Duration: 4.676 ms
     Changes:
              ----------
              newfile:
                  /usr/share/nginx/html/first.html
 ---------
          ID: sites_first
    Function: service.running
        Name: nginx
      Result: None
     Comment: Service is set to be restarted
     Started: 22:04:17.807615
    Duration: 26.997 ms
     Changes:

Summary
 -----------
Succeeded: 6 (unchanged=2, changed=1)
Failed:    0
 -----------
Total states run:     6

Now, we simply run the highstate without the test flag and have our new site deployed:

[vagrant@master ~]$ sudo salt minion1.example state.highstate
minion1.example:
<snip>
 ---------
          ID: roles_webserver_start
    Function: service.running
        Name: nginx
      Result: True
     Comment: The service nginx is already running
     Started: 22:06:47.380502
    Duration: 27.698 ms
     Changes:
 ---------
          ID: sites_first
    Function: file.managed
        Name: /usr/share/nginx/html/first.html
      Result: True
     Comment: File /usr/share/nginx/html/first.html updated
     Started: 22:06:47.409228
    Duration: 290.492 ms
     Changes:
              ----------
              diff:
                  New file
              mode:
                  0644
              user:
                  www
 ---------
          ID: sites_first
    Function: service.running
        Name: nginx
      Result: True
     Comment: Service restarted
     Started: 22:06:47.731534
    Duration: 337.565 ms
     Changes:
              ----------
              nginx:
                  True

Summary
 -----------
Succeeded: 6 (changed=2)
Failed:    0
 -----------
Total states run:     6

We can now do a simple test to verify the site is working:

[vagrant@master ~]$ curl 172.31.0.21/first.html
<html>
<head><title>First Site</title></head>
<body>
<h3>First Site</h3>
</body></html>

As you can see, the Nginx service was restarted. At the top of the inserted text, you can see that the state to verify the service is running (roles/webserver/start.sls == roles_webserver_start) was verified as already running. But, thanks to our watch statement, Nginx was restarted. (You can see it in the state with the sites_first ID.) You can play with this by simply updating the source file (/srv/salt/file/base/sites/src/first.html) and rerunning the highstate.

Tip

The watch functionality uses a function named mod_watch inside the state module. Not all states have such a method defined. If a state does not, then it will fall back to using a require. You should verify that any state where you use a watch directive has a mod_watch method declared.

Odds and Ends

This book touches on just a few of the different parts of requisite states. There are just a couple more you should be aware of: order and failhard. When we looked at the detailed state of a highstate, we saw there was an order attribute in the data structure. Salt uses this internally for bookkeeping of the states. Specifically, Salt uses order to track the order of each state as it is parsed from the SLS files. There are a couple of options for order that may be beneficial.

First, if you want to enforce that a certain state runs first, you can add the order: 1 declaration to your state. Salt will see this and put that state at the top of the list:

[vagrant@master ~]$ cat /srv/salt/file/base/run_first.sls
run_first:
  cmd.run:
  - name: 'echo "I am run first."'
  - order: 1

[vagrant@master ~]$ cat /srv/salt/file/base/top.sls
<snip>
  'minion4.example':
  - users.all
  - run_first
[vagrant@master ~]$ sudo salt minion4.example state.highstate
minion4.example:
 ---------
          ID: run_first
    Function: cmd.run
        Name: echo "I am run first."
      Result: True
     Comment: Command "echo "I am run first."" run
     Started: 23:47:18.232285
    Duration: 10.165 ms
     Changes:
              ----------
              pid:
                  15305
              retcode:
                  0
              stderr:

              stdout:
                  I am run first.
 ---------
<snip>
Summary
 -----------
Succeeded: 6 (changed=1)
Failed:    0
 -----------
Total states run:     6

Try removing the order line and then see what the order is. Related to this is the declaration of order: last. As the name suggests, it will make sure that the given state is run last.

Note that using the various requisite states is preferred over using the order command.

The last tidbit is the failhard option. You can add failhard: True to any state. If that state fails to run for any reason, then the entire state (which includes a highstate) will immediately stop. This can prove very useful if you have a service that is absolutely required for your infrastructure to work. If there is any problem deploying this service, stop immediately. You can also add failhard as a global option in the minion configuration.

Tip

cmd.run is very powerful, and it is tempting to use it often. However, there is a caveat here. As you can see, the various requisite states can add a lot of power in ordering your states the way you need. But they need to be able to see any changes in states. Also, the test=true command-line argument can help you determine exactly what will happen when a state is run. But cmd.run will always report a change because it will run a command. It cannot peer into that command to determine if a particular shell script actually makes any changes or not. As a result, you should use cmd.run in your states only as a last resort.

Summary

States are a way for you to define how you want a host, or a set of hosts, to look. You define individual states, like adding a user or installing a package, and then tie them all together using the top file. The top file uses the exact same targeting mechanisms we saw in Chapter 2. You can also define the order in which states run using various requisite states, such as require and watch. But this is only the beginning of what states can do. We will gain significantly more power when we add the templating engine Jinja in Chapter 6. In the same chapter you will learn how to write your own states.

1 As of Salt 2014.7, the compound matcher is the default in the top file. The example shown is still accurate, but we can make it simpler by using a target of G@os:CentOS and then removing the match line.

2 The dockerio execution module is currently being deprecated.

Get Salt Essentials now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.