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