Salt Execution Order
Complex State Trees
You can combine *.sls
files by using import statements or by the use of Top Files. We can use this data for example in our Apache landing page (see previous tutorial):
welcome.sls
# Adding a blank front page
include:
- apache
{% set name = salt.pillar.get('name') %}
check_pillar_values:
test.check_pillar:
- present:
- name
- failhard: True
welcome_page:
file.managed:
- name: /var/www/html/index.html
- contents: |
<!doctype html>
<body>
<h1>{{ name }}.</h1>
</body>
The include statement on top will add the Apache installation - we can now execute the welcome.sls
directly and get the complete Apache setup:
sudo salt ubuntuAsus state.sls apache.welcome
You can accomplish the same by creating a /srv/salt/top.sls
file:
base:
'*':
- apache
- apache.welcome
The Apache setup can now be executed by running:
sudo salt '*' state.highstate
The state.highstate
command will setup the whole infrastructure as defined inside your top file. If you create multiple top files for different setup use the state.top command and specify the file you want to execute:
sudo salt '*' state.top prod_top.sls
Execution Order
Salt's YAML render picks up every instruction inside an *.sls
file and assigns an order key to them. This makes sure that they are executed in the same order they are written down inside your file. If you need to make sure that one of the instruction is ALWAYS either executed FIRST or LAST, you can specify this inside your file:
init.sls
# Install vanilla Apache on Debian/RedHat
{% from 'apache/map.sls' import apache with context %}
install_apache:
pkg.installed:
- name: {{ apache.pkg }}
- order: last
enable_apache:
service.running:
- name: {{ apache.srv }}
# Will be enabled automatically on Debian but has to be enabled manually on RedHat
- enable: True
- order: first
This order stops working reliably once you have include or require statements inside your file.
Requisites
Requisites bring explicit ordering to your file execution:
init.sls
# Install vanilla Apache on Debian/RedHat
{% from 'apache/map.sls' import apache with context %}
install_apache:
pkg.installed:
- name: {{ apache.pkg }}
enable_apache:
service.running:
- name: {{ apache.srv }}
# Will be enabled automatically on Debian but has to be enabled manually on RedHat
- enable: True
- require:
- pkg: install_apache
The require
statement makes sure that Apache is installed before it will attempt to enable the Apache service.
Watch
The watch module reacts to a specified instruction being executed and then triggers another function. A practical use case is to restart the Apache service once the Apache configuration was modified:
init.sls
# Install vanilla Apache on Debian/RedHat
{% from 'apache/map.sls' import apache with context %}
install_apache:
pkg.installed:
- name: {{ apache.pkg }}
enable_apache:
service.running:
- name: {{ apache.srv }}
# Will be enabled automatically on Debian but has to be enabled manually on RedHat
- enable: True
- watch:
- file: danger_config
danger_config:
file.managed:
- name /bar/foo
- contents: foo
We can also extend the watch service to another SLS file:
mods.sls
include:
- apache
extend:
start_apache:
service:
- watch:
- file: danger_config
{% for conf in ['status', 'info'] %}
mod_{{ conf }}:
file.managed:
- name: /etc/apache2/conf-available/mod_{{ conf }}.conf
- contents: |
<Location '/{{ conf }}'>
SetHandler server-{{ conf }}
</Location>
{% if salt.grains.get('os_family') == 'Debian' %}
cmd.run:
- name: a2enmod {{ conf }} && a2enconf mod_{{ conf }}
- creates: /etc/apache2/conf-enabled/mod_{{ conf }}.conf
{% endif %}
{% endfor %}
The mods.sls
configures Apache - by including the init.sls
file we can now execute mods and be certain that Apache will be installed first before the configuration is attempted. We can now define the watch task here instead of the init file.
We can also use the watch_in
statement:
mods.sls
include:
- apache
{% for conf in ['status', 'info'] %}
mod_{{ conf }}:
file.managed:
- name: /etc/apache2/conf-available/mod_{{ conf }}.conf
- contents: |
<Location '/{{ conf }}'>
SetHandler server-{{ conf }}
</Location>
- watch_in:
- service: enable_apache
{% if salt.grains.get('os_family') == 'Debian' %}
cmd.run:
- name: a2enmod {{ conf }} && a2enconf mod_{{ conf }}
- creates: /etc/apache2/conf-enabled/mod_{{ conf }}.conf
- watch_in:
- service: enable_apache
{% endif %}
{% endfor %}
If mod_status
or mod_info
should always be changed BEFORE Apache is restarted with enable_apache
.
You can test the execution order by doing a dry-run:
salt '*' state.sls apache.mods test=true
ubuntuAsus:
----------
ID: install_apache
Function: pkg.installed
Name: apache2
Result: True
Comment: All specified packages are already installed
Started: 18:26:02.852277
Duration: 38.55 ms
Changes:
----------
ID: mod_status
Function: file.managed
Name: /etc/apache2/conf-available/mod_status.conf
Result: None
Comment: The file /etc/apache2/conf-available/mod_status.conf is set to be changed
Note: No changes made, actual changes may
be different due to other states.
Started: 18:26:02.899635
Duration: 1.456 ms
Changes:
----------
newfile:
/etc/apache2/conf-available/mod_status.conf
----------
ID: mod_info
Function: file.managed
Name: /etc/apache2/conf-available/mod_info.conf
Result: None
Comment: The file /etc/apache2/conf-available/mod_info.conf is set to be changed
Note: No changes made, actual changes may
be different due to other states.
Started: 18:26:02.901184
Duration: 1.077 ms
Changes:
----------
newfile:
/etc/apache2/conf-available/mod_info.conf
----------
ID: enable_apache
Function: service.running
Name: apache2
Result: None
Comment: Service is set to be restarted
Started: 18:26:02.940131
Duration: 19.022 ms
Changes:
----------
ID: mod_status
Function: cmd.run
Name: a2enmod status && a2enconf mod_status
Result: None
Comment: Command 'a2enmod status && a2enconf mod_status' would have been executed
Started: 18:26:02.961747
Duration: 378.228 ms
Changes:
----------
ID: mod_info
Function: cmd.run
Name: a2enmod info && a2enconf mod_info
Result: None
Comment: Command 'a2enmod info && a2enconf mod_info' would have been executed
Started: 18:26:03.340098
Duration: 4.92 ms
Changes:
Summary for ubuntuAsus
------------
Succeeded: 6 (unchanged=5, changed=2)
Failed: 0
------------
Total states run: 6
Total run time: 443.253 ms
Conditionals and Branching
onChanges
We can use onChanges
to watch a state and trigger actions when required. For example the Apache modules only need to be (re) enabled if their content changed.
include:
- apache
{% for conf in ['status', 'info'] %}
mod_{{ conf }}:
file.managed:
- name: /etc/apache2/conf-available/mod_{{ conf }}.conf
- contents: |
<Location "/{{ conf }}">
SetHandler server-{{ conf }}
</Location>
- watch_in:
- service: enable_apache
{% if salt.grains.get('os_family') == 'Debian' %}
cmd.run:
- name: a2enmod {{ conf }} && a2enconf mod_{{ conf }}
- onchanges:
- file: mod_{{ conf }}
- watch_in:
- service: enable_apache
{% endif %}
{% endfor %}
You can now re-run the command and the see that the execution was suppressed:
salt ubuntuAsus state.sls apache.mods
----------
ID: mod_status
Function: cmd.run
Name: a2enmod status && a2enconf mod_status
Result: True
Comment: State was not run because none of the onchanges reqs changed
Started: 10:54:17.354494
Duration: 0.004 ms
Changes:
----------
ID: mod_info
Function: cmd.run
Name: a2enmod info && a2enconf mod_info
Result: True
Comment: State was not run because none of the onchanges reqs changed
Started: 10:54:17.354603
Duration: 0.002 ms
Changes:
You can now change the configuration file (by adding some meaningless white space) and re-run the command:
...
<Location "/{{ conf }}">
SetHandler server-{{ conf }}
</Location>
...
The change will be picked up and the modules will be executed once again:
salt ubuntuAsus state.sls apache.mods
ubuntuAsus:
----------
ID: mod_status
Function: file.managed
Name: /etc/apache2/conf-available/mod_status.conf
Result: True
Comment: File /etc/apache2/conf-available/mod_status.conf updated
Started: 11:00:09.916316
Duration: 31.164 ms
Changes:
----------
diff:
---
+++
@@ -1,3 +1,4 @@
<Location "/status">
SetHandler server-status
+
</Location>
----------
ID: enable_apache
Function: service.running
Name: apache2
Result: True
Comment: Service restarted
Started: 11:00:10.005666
Duration: 133.762 ms
Changes:
----------
apache2:
True
----------
ID: mod_info
Function: file.managed
Name: /etc/apache2/conf-available/mod_info.conf
Result: True
Comment: File /etc/apache2/conf-available/mod_info.conf updated
Started: 11:00:09.947841
Duration: 11.464 ms
Changes:
----------
diff:
---
+++
@@ -1,3 +1,4 @@
<Location "/info">
SetHandler server-info
+
</Location>
----------
ID: mod_status
Function: cmd.run
Name: a2enmod status && a2enconf mod_status
Result: True
Comment: Command "a2enmod status && a2enconf mod_status" run
Started: 11:00:10.142729
Duration: 59.029 ms
Changes:
----------
pid:
83853
retcode:
0
stderr:
stdout:
Module status already enabled
Conf mod_status already enabled
----------
ID: mod_info
Function: cmd.run
Name: a2enmod info && a2enconf mod_info
Result: True
Comment: Command "a2enmod info && a2enconf mod_info" run
Started: 11:00:10.202081
Duration: 52.738 ms
Changes:
----------
pid:
83864
retcode:
0
stderr:
stdout:
Module info already enabled
Conf mod_info already enabled
onFail
With onFail
we can run a state if another state does not complete successfully. Create a file nano /srv/salt/apptest.sls
and let it download a git repository for us:
apptest:
git.latest:
- name: https://github.com/mpolinowski/docker-elk.git
- rev: master
- target: /opt/apptest
notify_of_fail:
event.send:
- name: apptest/failed
- onfail:
- git: apptest
Then run the state:
salt ubuntuAsus state.sls apptest
ubuntuAsus:
----------
ID: apptest
Function: git.latest
Name: https://github.com/mpolinowski/docker-elk.git
Result: True
Comment: https://github.com/mpolinowski/docker-elk.git cloned to /opt/apptest
Started: 11:24:51.598681
Duration: 13472.643 ms
Changes:
----------
new:
https://github.com/mpolinowski/docker-elk.git => /opt/apptest
revision:
----------
new:
2358839589c36223984b6a0528289d9efefd5189
old:
None
----------
ID: notify_of_fail
Function: event.send
Name: apptest/failed
Result: True
Comment: State was not run because onfail req did not change
Started: 11:25:05.080430
Duration: 0.013 ms
Changes:
Summary for ubuntuAsus
------------
Succeeded: 2 (changed=1)
Failed: 0
------------
Total states run: 2
Total run time: 13.473 s
The repository cloning was successful and the notification was NOT triggered.
prereq
Only run a state when another state will change the system. E.g. reload your webserver only if a new app version was downloaded by git:
{% from "apache/map.sls" import apache with context %}
include:
- apache
apptest:
git.latest:
- name: https://github.com/mpolinowski/docker-elk.git
- rev: master
- target: /opt/apptest
- watch_in:
- service: enable_apache
reload_apache:
module.run:
- name: service.stop
- m_name: {{ apache.srv }}
- prereq:
- git: apptest
notify_of_fail:
event.send:
- name: apptest/failed
- onfail:
- git: apptest
The prerequisite will run apptest
in Dry-Run. If the code changed, reload_apache
will be triggered once the new source code was cloned by git.
salt ubuntuAsus state.sls apptest
ubuntuAsus:
----------
ID: install_apache
Function: pkg.installed
Name: apache2
Result: True
Comment: All specified packages are already installed
Started: 11:44:57.106219
Duration: 40.43 ms
Changes:
----------
ID: reload_apache
Function: module.run
Name: service.stop
Result: True
Comment: Module function service.stop executed
Started: 11:44:58.331330
Duration: 97.635 ms
Changes:
----------
ret:
True
----------
ID: apptest
Function: git.latest
Name: https://github.com/mpolinowski/docker-elk.git
Result: True
Comment: https://github.com/mpolinowski/docker-elk.git cloned to /opt/apptest
Started: 11:44:58.429290
Duration: 4591.687 ms
Changes:
----------
new:
https://github.com/mpolinowski/docker-elk.git => /opt/apptest
revision:
----------
new:
2358839589c36223984b6a0528289d9efefd5189
old:
None
----------
ID: enable_apache
Function: service.running
Name: apache2
Result: True
Comment: Service apache2 is already enabled, and is running
Started: 11:45:03.021324
Duration: 118.316 ms
Changes:
----------
apache2:
True
----------
ID: notify_of_fail
Function: event.send
Name: apptest/failed
Result: True
Comment: State was not run because onfail req did not change
Started: 11:45:03.141194
Duration: 0.004 ms
Changes:
Summary for ubuntuAsus
------------
Succeeded: 5 (changed=3)
Failed: 0
------------
Total states run: 5
Total run time: 4.848 s
reload_apache
detected that apptest
will add new source code and stopped the webserver. Once the code was cloned the watch task reloaded the webserver.