State requisites and declarations#
ID vs. name#
Technically speaking, the component of the high data that determines the name
argument that is passed to the state function is not called name in the high
data, it is called ID
.
The ID is copied into the name argument unless the name argument is explicitly
declared by specifying a name
value (or names
values).
The following example shows the declaration of both an ID and name:
push_dns_conf: # The ID which identifies the state
file.managed:
- name: /etc/named.conf # The name passed to file.managed function
- source: salt://dns/files/named.conf
Taking into account that the ID could be copied into the name argument, the example could also be written to only use ID:
/etc/named.conf:
file.managed:
- source: salt://dns/files/named.conf
ID must be unique across the entire state tree. If the same ID declaration is used twice, only the first one matched will be used. All subsequent ID declarations with the same name will be ignored.
Note
Salt Best Practices recommend always using a unique, descriptive value for the state ID
One ID to many names#
The names
argument allows for one ID to become many names.
python-pkgs:
pkg.installed:
- names:
- python
- pypy
- python-mako
Another example using directories:
deploy_dirs:
file.directory:
- makedirs: True
- names:
- /opt/code/docs
- /opt/code/config
- /opt/code/images
State execution order#
The following sections of this chapter outline declarations that will set the order of execution as high data is processed and compiled into low data.
Tools exist in Salt to modify state ordering. These tools consist of requisite declarations and order options. When Salt States are executed, they are first ordered in a list (called low data), and then the list is iterated over.
This differs from other configuration management tools which execute everything within an event loop or execute raw code.
The approach of setting up a finite list of executions means that the code can be defined in a truly declarative way, and still execute in a completely predictable manner.
Implicit ordering#
States will always execute in the order that they are defined in your SLS files.
dns_install:
pkg.installed:
- name: bind
dns_service:
service.running:
- name: named
- enable: True
dns_conf:
file.managed:
- name: /etc/named.conf
- source: salt://dns/files/named.conf
What would be the outcome of executing this state based on implicit ordering?
The order declaration#
The order
option is used by adding an order number to a state:
install_app:
pkg.installed:
- name: app
prestage_application_data:
file.recurse:
- name: /app/production
- source: salt://app/source
- order: 1
By setting the order
option to 1 this ensures that the /app/production
directory will be populated before any other states that are executed.
Any state declared without an order
option will be executed after all
states with the order
option are executed in the order they are present
in the State File. This construct can only handle ordering states from the
beginning. Sometimes you may want to send a state to the end of the execution -
to do this, set order: last
update_status:
module.function:
- name: http.query
- args:
- 'https://example.org/update-status'
- kwargs:
- method: POST
- params: 'keyA=valA&keyB=valB'
- order: last
install_app:
pkg.installed:
- name: app
prestage_application_data:
file.recurse:
- name: /app/production
- source: salt://app/source
- order: 1
Before using the order
option, remember that the majority of state ordering
should be done using other Requisite Declarations. A requisite declaration
will override an order
option so a state with an order
option defined
should not require or be required by other states.
Requisite declarations#
Often when setting up states any single action will require or depend on another action. Salt allows you to build relationships between states with requisite declarations.
A requisite declaration ensures that the named state is evaluated before the state requiring it. Failures can also be accounted for when completing the states defined in the SLS file.
Referencing state declarations#
Requisites can match either the ID declaration or the name parameter.
A requisite references another part of a state file (SLS) in the form of:
state_module: [id | name]
For example, consider the previous example:
dns_install:
pkg.installed:
- name: bind
dns_service:
service.running:
- name: named
- enable: True
dns_conf:
file.managed:
- name: /etc/named.conf
- source: salt://dns/files/named.conf
A reference to the package installation would be:
pkg: dns_install
(by ID) or pkg: bind
(by name)
A reference to the service running would be:
service: dns_service
(by ID) or service: named
(by name)
It is recommended as a Salt best practice to always refer to the state ID when adding requisites.
State failure behavior#
The default behavior when a state fails is to continue to execute the remainder of the defined states. This is called a soft fail, meaning that execution of the state continues after a failure.
The situation may exist where you would want all state execution to stop if a single state execution fails. This can be done in states with requisite definitions. The capability to do this is called failing hard. A hard failure can be implemented in two ways:
Defined in a state declaration
Defined globally in the minion configuration
The failhard
option defined within a state declaration:
dns_install:
pkg.installed:
- name: bind
- failhard: True
If the state fails to install the package, then no other states will be executed. Globally, this can be set in the minion configuration:
failhard: True
Standard requisites#
A requisite statement ensures that the named state is evaluated before the state requiring it. There are several direct requisite statements that can be used in Salt that inherently implement failing hard behavior:
require
watch
onfail
onchanges
use
prereq
The two most common types of requisites in Salt are require
and watch
The require requisite#
The requisite system works by finding the states that are required, and executing them before the state that requires them. Then the required states can be evaluated to see if they have executed correctly.
The foundation of the requisite system is the require requisite declaration.
The require requisite ensures that the required states are executed before the state declaring the require
The state declaring the require will only be executed if the required state returns True
dns_install:
pkg.installed:
- name: bind
dns_service:
service.running:
- name: named
- enable: True
dns_conf:
file.managed:
- name: /etc/named.conf
- source: salt://dns/files/named.conf
- require:
- pkg: dns_install
In the previous example, we use a require
to make sure the bind
package is
successfully installed before attempting to copy the configuration file to the
minion. We’ll deal with the service when we describe the watch
requisite.
Circular references#
Salt will detect circular references and not allow them. If a circular reference is detected Salt will return an error such as:
Data failed to compile:
----------
A recursive requisite was found, SLS "named" ID "dns_install" ID "dns_conf"
In this example, dns_install
required dns_conf
and dns_conf
required dns_install
, thus creating a circular reference. Salt must be able
to evaluate which state to test first to decide order and if execution is
needed based on the requirements.
The watch requisite#
The watch
requisite is more advanced than the require
requisite. The
watch
requisite executes the same logic as require
:
If a state is being watched, it does not need to also be required. This logic is built into
watch
to evaluate the watched states as TrueThe
watch
requisite also checks if the watched states have returned any changes
If the watched states returned changes, and the watched states execute
successfully, then the state declaring the watch
will execute a function
that reacts to the changes in the watched states:
dns_install:
pkg.installed:
- name: bind
dns_service:
service.running:
- name: named
- enable: True
- watch:
- file: dns_conf
dns_conf:
file.managed:
- name: /etc/named.conf
- source: salt://dns/files/named.conf
- require:
- pkg: dns_install
Running the previous state file execution will produce the following output if
the /etc/named.conf
is updated:
rebel_01:
----------
ID: /etc/named.conf
Function: file.managed
Result: True
Comment: File /etc/named.conf updated
Started: 22:40:34.126006
Duration: 34.006 ms
Changes:
----------
diff:
---
+++
@@ -10,38 +10,37 @@
- listen-on port 53 { 127.0.0.1; };
+ listen-on port 53 { 0.0.0.0; };
+zone "my.domain" IN {
+ type master;
+ file "master/master.my.domain";
+ // enable slaves only
+ allow-transfer {192.0.2.1;192.0.2.2;);
+};
----------
ID: start_dns
Function: service.running
Name: named
Result: True
Comment: Started Service named
Started: 23:10:36.318223
Duration: 400.123 ms
Changes:
----------
named:
True
Summary for rebel_01
------------
Succeeded: 1 (changed=1)
Failed: 0
------------
Total states run: 1
Total run time: 34.006 ms
In this example the named service will be started (or restarted) since the
file /etc/named.conf
is changed (new or updated). The watch
requisite
is based on the mod_watch
function. Salt Python state modules can include a
function called mod_watch
which is then called if the watch
call is
invoked.
In the case of the service state the underlying service is restarted.
In the case of the cmd state the command is executed.
The watch
requisite only works if the state that is watching has a
mod_watch
function written. If the watching state where the watch
is
set does not have a mod_watch
function (like pkg), then the listed states
will behave only as if they were under a require
statement.
Multiple requisites#
The requisite declaration is passed as a list, allowing for the easy addition of multiple requisites. Multiple requisite types can also be separately declared:
dns_install:
pkg.installed:
- name: bind
create_user:
user.present:
- name: bind
- require:
- pkg: dns_install
create_group:
group.present:
- name: bind
- require:
- pkg: dns_install
dns_service:
service.running:
- name: named
- enable: True
- require:
- pkg: dns_install # Technically not needed since "watch" is on dns_conf
- user: create_user # dns_conf has a "require" defined for dns_install
- group: create_group # Cascading require as "watch" is also a require
- watch:
- file: dns_conf
dns_conf:
file.managed:
- name: /etc/named.conf
- source: salt://dns/files/named.conf
- require:
- pkg: dns_install
It is important to understand the flow of the state file execution.
The onfail declaration#
The onfail
requisite allows for reactions to happen strictly as a response
to the failure of another state.
This can be used in a number of ways, such as executing a second attempt to
set up a service or begin to execute a separate thread of states because of a
failure. The onfail
requisite is applied in the same way as require as watch:
httpd_service:
service.running:
- name: httpd
report_failure:
module.run:
- name: slack_notify.call_hook
- kwargs:
message: Apache failed to start
- onfail:
- service: httpd_service
The onchanges declaration#
The onchanges
requisite makes a state only apply if the required states
generate changes, and if the watched state’s result is True
.
Unlike watch
, the onchange
requisite does not execute if there are no
detected changes, where a watch
does. For example, in a watch
:
dns_service:
service.running:
- name: named
- enable: True
- watch:
- file: dns_conf
dns_conf:
file.managed:
- name: /etc/named.conf
- source: salt://dns/files/named.conf
In the case of using a watch
, even if there are no changes in the
watch
file, the Salt state system will execute this function to put the
service in a running state, or at least check to see if it is running.
When using the onchanges
the behavior changes:
dns_service:
service.running:
- name: named
- enable: True
- onchanges:
- file: dns_conf
dns_conf:
file.managed:
- name: /etc/named.conf
- source: salt://dns/files/named.conf
If an onchanges
is declared instead of a watch
, and if there are no
changes, the service is not set to run if currently stopped. The logic is that
the service will not be started if it is currently not running and there are
no changes to the file.
This can be a useful way to execute a post hook after changing aspects of a
system. An example of using an onchanges
is if you only want salt-cloud
updated if there is a new bootstrap script available:
deploy_bootstrap:
file.managed:
- name: /etc/salt/cloud.deploy.d/bootstrap-salt.sh
- source: salt://conf/boostrap-salt.sh
install_salt_cloud:
pkg.latest:
- name: salt-cloud
- onchanges:
- file: deploy_bootstrap
The use requisite#
The use
requisite declarations allow for the transparent duplication of
data between states.
When a state “uses” another state, it copies the other state’s arguments as
defaults. A simple example of the use
declaration:
manage_eth0:
network.managed:
- name: eth0
- enabled: True
- type: eth
- proto: static
- ipaddr: 192.0.2.7
- netmask: 255.255.255.0
- gateway: 192.0.2.1
- enable_ipv6: true
- ipv6proto: static
- ipv6ipaddrs:
- 2001:db8:dead:beef::3/64
- 2001:db8:dead:beef::7/64
- ipv6gateway: 2001:db8:dead:beef::1
- ipv6netmask: 64
- dns:
- 198.51.100.8
- 203.0.113.4
manage_eth1:
network.managed:
- name: eth1
- ipaddr: 203.0.113.120
- gateway: 203.0.113.1
- ipv6ipaddr: 2001:db8:dead:c0::3
- ipv6gateway: 2001:db8:dead:c0::1
- use:
- network: manage_eth0
The use
statement was developed primarily for the networking states but
can be used on any states in Salt. This makes sense for the network state
because it can define a long list of options that need to be applied to
multiple network interfaces.
The prereq requisite#
The prereq
requisite allows for actions to be taken based on the expected
results of a state that has not yet been executed.
The state containing the prereq
requisite is defined as the pre-requiring
state. When a prereq
requisite is evaluated, the pre-required state
reports if it expects to have any changes. It does this by running the
pre-required single state as a test-run by enabling test=True
.
The best way to define how prereq
operates is displayed in the following
practical example:
gracefulRestart:
module.run:
- name: service.restart
- m_names:
- httpd
- prereq:
- file: site-code
siteCode:
file.recurse:
- name: /opt/site_code
- source: salt://site/code
When the apache
service should be shut down because underlying code is going
to change, the service should be off-line while the update occurs. In this
example, gracefulRestart
is the pre-requiring state and siteCode
is the
pre-required state.
Including other SLS files#
The include
declaration is a top level declaration that defines a list of
SLS files to bring into the current SLS file.
An include
can be used to bring in data from another SLS file for many
reasons.
If you want to combine many states into one.
If the SLS file needs to require or watch components found in another SLS file.
If components of another SLS file need to be extended, or if a shortcut SLS file needs to be made.
If another SLS file needs to be read-only in another environment, but allowed to be included, used, or extended
This example includes all core states for the infrastructure:
include:
- ssh
- sudo
- edit.vim
- edit.emacs
- ntp
Included state files are relative to the file_roots
.
Including for requisites#
Require kvm
before starting libvirt
. Here is the basic kvm
state file:
install_qemu:
pkg.installed:
- name: qemu-kvm
load_kvm:
kmod.present:
- name: kvm_intel
Here is the libvirt
state file including the kvm
state requiring it:
include:
- kvm
install_libvirt:
- pkg.installed:
- name: libvirt
start_libvirt:
- service.running:
- name: libvirt
- require:
- kmod: load_kvm
- pkg: install_qemu
Extending external SLS data#
Sometimes a state defined in one SLS file will need to be modified from a separate SLS file.
A good example of this is when an argument needs to be overwritten or when a service needs to watch an additional state.
The extend declaration#
The extend
declaration is a top level declaration like include
and
encapsulates ID declaration data included from other SLS files.
Using the following Salt State file as a starting point:
install_ssh:
pkg.latest:
- name: openssh
ssh_server:
service.running:
- name: sshd
- enable: True
- watch:
- pkg: install_ssh
- file: sshd_conf
sshd_conf:
file.managed:
- name: /etc/ssh/sshd_config
- source: salt://ssh/files/sshd_config
We can use the ssh
state file as a base, and then build upon it to suit
specific needs:
include:
- ssh
extend:
sshd_conf:
file:
- name: /etc/ssh/sshd_config
- source: salt://ssh/files/dmz_sshd_config
ssh_server:
service:
- watch:
- file: add_banner
add_banner:
file.managed:
- name: /etc/ssh/banner
- source: salt:/ssh/files/banner
A few critical things happened here. First off, the SLS files that are going
to be extended are included, then the extend
declaration is defined. Under
the extend
declaration, two ids are extended: the ssh_conf
file state
is overwritten with a new name and source, then ssh_server
is extended to
watch the banner file in addition to anything it is already watching.
Extend rules and regulation#
The extend
declaration is a top-level declaration. This means that
extend
can only be called once in an SLS file. If it is declared more
than once, then only the second extend
block will be used.
The following example is wrong:
include:
- http
- ssh
extend:
apache:
file:
- name: /etc/httpd/conf/httpd.conf
- source: salt://http/httpd2.conf
# Second overwrites first
extend:
ssh-server:
service:
- watch:
- file: /etc/ssh/banner
Note
If the second extend
is removed or commented, then the state file will
work as intended.
Things to remember when extending states:
Always include the SLS files being extended with an
include
declarationRequisites
watch
andrequire
are appended to, everything else is overwrittenextend
is a top level declaration. Like the state ID, it cannot be declared more than once in a single SLSMany state IDs can be extended using the
extend
declaration
The requisite _in declarations#
Each requisite also has a corresponding _in counterpart:
require_in
watch_in
prereq_in
use_in
onchanges_in
onfail_in
The corresponding _in requisites basically allow the logic of rendering to do the reverse of the declaration.
An example using require_in
and watch_in
could look like this:
install_ssh:
pkg.latest:
- name: openssh
- watch_in:
- service: ssh_server
- require_in:
- file: sshd_conf
ssh_server:
service.running:
- name: sshd
- enable: True
sshd_conf:
file.managed:
- name: /etc/ssh/sshd_config
- source: salt://ssh/files/sshd_config
- watch_in:
- service: ssh_server
An alternate way to extend a state declaration:
include:
- ssh
add_banner:
file.managed:
- name: /etc/ssh/banner
- source: salt:/ssh/files/banner
- watch_in:
- service: ssh_server
Here’s our networking example with the use_in
declaration taken a bit
further:
manage_eth0:
network.managed:
- name: eth0
- enabled: True
- type: eth
- proto: static
- ipaddr: 192.0.2.7
- netmask: 255.255.255.0
- gateway: 192.0.2.1
- enable_ipv6: true
- ipv6proto: static
- ipv6ipaddrs:
- 2001:db8:dead:beef::3/64
- 2001:db8:dead:beef::7/64
- ipv6gateway: 2001:db8:dead:beef::1
- ipv6netmask: 64
- dns:
- 198.51.100.8
- 203.0.113.4
- use_in:
- network: manage_eth1
- network: manage_eth2
manage_eth1:
network.managed:
- name: eth1
- ipaddr: 203.0.113.120
- gateway: 203.0.113.1
- ipv6ipaddr: 2001:db8:dead:c0::3
- ipv6gateway: 2001:db8:dead:c0::1
manage_eth2:
network.managed:
- name: eth2
- ipaddr: 203.0.113.121
- gateway: 203.0.113.1
- ipv6ipaddr: 2001:db8:dead:c0::4
- ipv6gateway: 2001:db8:dead:c0::1
Altering states#
The state altering system is used to make sure that states are evaluated exactly as the user expects. It can be used to double check that a state performed exactly how it was expected to, or to make 100% sure that a state only runs under certain conditions.
The use of unless
or onlyif
options help make states even more
stateful.
Note
Under the hood, these altering states declarations call cmd.retcode
with
python_shell=True
. This means the commands referenced by these
declarations will be parsed by a shell. So be aware of side-effects as this
shell will be run with the same privileges as the Salt Minion.
The onlyif requisite#
The onlyif
requisite is used if all of the commands defined return True.
Then the state will be run.
If any of the specified commands return False, the state will not run. This
example creates a new MySQL database user only if the projectDB
database
exists.
create_db_user:
mysql_user.present:
- name: jdoe
- host: localhost
- password: p@ssw0rd
- onlyif:
- mysql -u ro_user -e 'use projectDB'
The unless requisite#
The unless
requisite specifies that a state should only run when any of the
specified commands return False.
The unless
requisite operates as NAND where it produces a value of True,
if, and only if, at least one of the propositions is False. It is useful in
giving more granular control over when a state should execute. In the example
below, the state will only run if either the vim-enhanced
package is not
installed (returns False) or if /usr/bin/vim
does not exist (returns
False). The state will run if both commands return False.
However, the state will not run if both commands return True.
install_vim:
pkg.installed:
- name: vim
- unless:
- rpm -q vim-enhanced
- ls /usr/bin/vim
The unless
requisite checks are resolved for each name to which they are
associated.
The check_cmd requisite#
Check Command is used for determining that a state did or did not run as expected.
This will attempt to do a replace on all
enabled=0
in the .repo file, and replace them withenabled=1
.
comment-repo:
file.replace:
- name: /etc/yum.repos.d/fedora.repo
- pattern: ^enabled=0
- repl: enabled=1
- check_cmd:
- grep '^enabled=1' /etc/yum.repos.d/fedora.repo
# or
- grep '^enabled=0' /etc/yum.repos.d/fedora.repo && return 1 || return 0
The check_cmd
is just a bash command.
It will do a grep for
enabled=0
in the file, and if it finds any, it will return a 0, which will prompt the&&
portion of the command to return a 1, causingcheck_cmd
to set the state as failed.If it returns a 1, meaning it didn’t find any
enabled=0
it will hit the||
portion of the command, returning a 0, and declaring the function succeeded.
The listen requisite#
listen
and its counterpart listen_in
trigger mod_watch
functions
for states when those states succeed and result in changes, similar to how
watch
and its counterpart watch_in
. Unlike watch
and watch_in
,
listen
, and listen_in
will not modify the order of states and can
be used to ensure your states are executed in the order they are defined. All
listen
/listen_in
actions will occur at the end of a state run, after
all states have completed.
restart_apache2:
service.running:
- name: apache2
- listen:
- file: /etc/apache2/apache2.conf
configure_apache2:
file.managed:
- name: /etc/apache2/apache2.conf
- source: salt://apache2/apache2.conf