RBleug


Regilero's blog; Mostly tech things about web stuff.

This is a detailled example of salt-stack's file.accumulated usage.
This is a detailled example of salt-stack's file.accumulated usage.

On the last salt-stack post we saw a first step by step usage of file.blockreplace. On this post we'll study usage of state accumulators with file.accumulated. Accumulators are used to collect data on several states and let you use this data on other file states (Actually only the blockreplace and managed states).

You can find the examples used on this post on this github repository.

Why using accumulated accumulators?

Let's start by the needs. What sort of problems could be solved by accumulators?. In fact, the idea is to use states executed before a final state. And this state ordering can be solved using requisites or orders. On theses first states you collect data for later usage. Then, on the final state, you have a dictionary containing this collected data, the accumulator dictionary, and you can do what you want with it with jinja, or even without jinja in the blockreplace case. Note that you only collect data in the current highstate execution, so all accumulators must run (be included) if you need the data at the end.

You can use this system to record one or more lists of things likes users, services, addresses, configuration commands or wathever else you want, while running the states. And all theses states recording data will have to run before the one using that data for something (writing theses lists on a targeted file). Doing that you will avoid doing the write operation after each new record, you will wait for the final list before doing the write. This will be faster, but also easier to manage. Inserting, updating and removing data in a file, in one shot.

In this post I will use two examples, one for a managed file and one for a blockreplace edited file. The first example is an apache configuration file with several states adding some inputs inside. The second example will fill a list of DNS IP-name associations that should be added in a hosts file.

States ordering

As I said before states ordering is very important here. The final step must be final. And used states may be split upon several sls files. You can use the order order keyword to ensure the final order, this way:

 1     # first.sls
 2     STATEID1:
 3       cmd.run:
 4         name: echo STATEID1
 5         order: 100
 6   
7 # second.sls 8 STATEID2: 9 cmd.run: 10 name: echo last 11 order: 900 12
13 # third sls 14 STATEID3: 15 cmd.run 16 name: echo STATEID3 17 order: 200

This will make a final execution order of:

1     echo STATEID1
2     echo STATEID3
3     echo last

But to do that you need an ordering schema for all you states. I do not find that very useful when you have a lot of states.

The second way of ordering states is using requisites. Using this method you can declare dependencies between states. But having a list of all the states that collects data into accumulators, and eediting this list on the state using the accumulator would be quite hard. The best thing here is to use the *_in form of the requisites, to declare the dependency from the dependent state. When adding a new state using the accumulator the dependency will have to be set in this state and not in the previous managed state entry.

So using require_in the previous example would be:

 1     # first.sls
 2     STATEID1:
 3       cmd.run:
 4         name: echo STATEID1
 5         require_in:
 6           - cmd: STATEID2
 7   
8 # second.sls 9 STATEID2: 10 cmd.run: 11 name: echo last 12
13 # third sls 14 STATEID3: 15 cmd.run 16 name: echo STATEID3 17 require_in: 18 - cmd: STATEID2

Be careful: the syntax for a requisite is:

1     <requisite>:
2       - <module>: <state id>

It is not:

1     <requisite>:
2       - <module>.<function>: <state id>

So it is not cmd.run: here but only cmd:. And as soon as you start using requisites you see that using meaningfull states ids and not names shortcuts to declare states is quite important, for clarity at least.

file.managed using file.accumulated

All examples are available on github here.

In this first example we'll use several states, the last state will install a managed file in /etc/apache2/sites-available/100-foo.example.com, it's an apache virtualhost file, in the debian way. The jinja template associated with this file will contain the basic rules, but we want to allow some other states to add instructions on this virtualhost. We'll see theses states later. We start by the end, with this state building a virtualhost file in a example_com_apache_virtualhost.sls file:

 1     apache-install:
 2       pkg.installed:
 3         - pkgs:
 4           - apache2
 5   
6 100_example_com_virtualhost: 7 file.managed: 8 - source: salt://files/apache_vhost 9 - name: /etc/apache2/sites-available/100-example.com 10 - user: root 11 - group: root 12 - mode: "0664" 13 - template: jinja 14 - defaults: 15 - docroot: /path/to/www 16 - servername: example.com 17 - require: 18 - pkg: apache-install

This file should be put somewhere on your state tree, in this example I will make the function calls as if it were on the root of this tree (like the top.sls file). But there's also a source template for the managed file, which is called salt://files/apache_vhost, this file should be present under the salt master tree, in the directory files (or alter the used path). This is this very simple basic virtualhost example template content:

 1     # Main Virtualhost for {{ servername }}
 2     <VirtualHost *:80>
 3         ServerAdmin foo@example.com
 4   
5 DocumentRoot {{ docroot }} 6
7 ServerName {{ servername }} 8
9 LogLevel info 10
11 <Directory /> 12 AllowOverride None 13 Order allow, deny 14 deny from all 15 </Directory> 16
17 <Directory {{ docroot }}> 18 Options FollowSymLinks 19 AllowOverride None 20 Order allow,deny 21 Allow from all 22 </Directory> 23
24 </VirtualHost>

Let's test this simple state:

1     $# salt-call -linfo state.sls example_com_apache_virtualhost

You should en up with two states in success and a /etc/apache2/sites-available/100-example.com file created. Now let's say we want other states, an infinite list of other states, to be able to alter this file and add content either in the main Directory section or at the end of the file. This way other states could add some apache configuration (this is an example, another way of doing it could be apache's include directive).

Here comes the accumulator jinja variable. It's a dictionnary, with several keys. Each key of this dictionary is the result of one or more file.accumulated states. So you may have this variable (or not) and it may contain some keys with text data inside. Let's see how to use this on the jinja template (and at first, we known it's empty, we did not used any accumulated state yet).

 1     # Main Virtualhost for {{ servername }}
 2     <VirtualHost *:80>
 3         ServerAdmin foo@example.com
 4     
 5         DocumentRoot {{ docroot }}
 6     
 7         ServerName {{ servername }}
 8     
 9         LogLevel info
10     
11       <Directory />
12         AllowOverride None
13         Order allow, deny
14         deny from all
15       </Directory>
16     
17       <Directory {{ docroot }}>
18         Options FollowSymLinks
19         AllowOverride None
20         Order allow,deny
21         Allow from all
22 
23         # Here any extra configuration settings if any:
24         {% if accumulator|default(False) %}
25         {%   if 'extra-settings-example-virtualhost-maindir' in accumulator %}
26         {%     for line in accumulator['extra-settings-example-virtualhost-maindir'] %}
27         {{ line }}
28         {%     endfor %}
29         {%   endif %}
30         {% endif %}
31 
32       </Directory>
33 
34     # Here any extra configuration settings if any:
35     {% if accumulator|default(False) %}
36     {%   if 'extra-settings-example-virtualhost' in accumulator %}
37     {%     for line in accumulator['extra-settings-example-virtualhost'] %}
38     {{ line }}
39     {%     endfor %}
40     {%   endif %}
41     {% endif %}
42   
43     </VirtualHost>

If you run the state nothing should move, except maybe the two comments lines. The thing we need to do now is to feed this accumulator variable with file.accumulated states. On theses states the name of the state will match the accumulator key.

So, for this example, we will use a second sls file more-things-for-virtualhost.sls :

 1     {# Include dependencies #}
 2     include:
 3       - example_com_apache_virtualhost
 4   
5 example-a-first-rewrite-rule: 6 file.accumulated: 7 - name: extra-settings-example-virtualhost-maindir 8 - filename: /etc/apache2/sites-available/100-example.com 9 - text: | 10 # this is an example of thing added in the middle 11 RewriteEngine On 12 RewriteCond %{REQUEST_FILENAME} -d 13 RewriteRule ^(.+[^/])$ $1/ [R] 14 - require_in: 15 - file: 100_example_com_virtualhost 16
17 example-some-icons-added: 18 file.accumulated: 19 - name: extra-settings-example-virtualhost 20 - filename: /etc/apache2/sites-available/100-example.com 21 - text: | 22 # this is an example of thing added at the end' 23 Alias /icons /path/to/icons> 24 <Directory /path/to/icons 25 Order allow,deny 26 Allow from all 27 </Directory> 28 - require_in: 29 - file: 100_example_com_virtualhost 30
31 example-another-thing: 32 file.accumulated: 33 - name: extra-settings-example-virtualhost-maindir 34 - filename: /etc/apache2/sites-available/100-example.com 35 - text: | 36 # this is another example of thing added in the middle 37 RewriteRule ^/cgi-bin/imagemap(.*) $1 [PT] 38 - require_in: 39 - file: 100_example_com_virtualhost 40 - require: 41 - file: example-a-first-rewrite-rule 42
43 example-another-thing-again: 44 file.accumulated: 45 - name: extra-settings-example-virtualhost-maindir 46 - filename: /etc/apache2/sites-available/100-example.com 47 - text: | 48 # this is another example of thing added in the middle 49 <FilesMatch ".(gif|jpe?g|png)$"> 50 ExpiresDefault A2592000 51 </FilesMatch> 52 - require_in: 53 - file: 100_example_com_virtualhost

Now let's run this new sls:

1     $# salt-call -linfo state.sls more-things-for-virtualhost

You should get a nice diff showing you that all theses states added content on the right place:

 1     +++
 2     @@ -21,9 +21,41 @@
 3          Allow from all
 4          # Here any extra configuration settings if any:
 5        
6 +
7 +
8 + # this is an example of thing added in the middle 9 +RewriteEngine On 10 +RewriteCond %{REQUEST_FILENAME} -d 11 +RewriteRule ^(.+[^/])$ $1/ [R] 12 + 13 +
14 + # this is another example of thing added in the middle 15 +RewriteRule ^/cgi-bin/imagemap(.*) $1 [PT] 16 + 17 +
18 + # this is another example of thing added in the middle 19 +<FilesMatch ".(gif|jpe?g|png)$"> 20 + ExpiresDefault A2592000 21 +</FilesMatch> 22 + 23 +
24 +
25 +
26 </Directory> 27
28 # Here any extra configuration settings if any: 29
30
31 + 32 +# this is an example of thing added at the end' 33 +Alias /icons /path/to/icons> 34 +<Directory /path/to/icons 35 + Order allow,deny 36 + Allow from all 37 +</Directory> 38 + 39 + 40 + 41 + 42 + 43 </VirtualHost>

You can see that some extra spaces were added by my jinja control commands. We can strip down those whitespaces with jinja's-. Instead of:

1     {% if accumulator|default(False) %}
2     {%   if 'extra-settings-example-virtualhost-maindir' in accumulator %}
3     {%     for line in accumulator['extra-settings-example-virtualhost-maindir'] %}
4     {{ line }}
5     {%     endfor %}
6     {%   endif %}
7     {% endif %}

Use:

1     {% if accumulator|default(False) -%}
2     {%   if 'extra-settings-example-virtualhost-maindir' in accumulator -%}
3     {%     for line in accumulator['extra-settings-example-virtualhost-maindir'] -%}
4     {{ line }}
5     {%-     endfor %}
6     {%-   endif %}
7     {%- endif %}

And to get the right number of spaces on the resulting file use the indent filter:

1     {{ line|indent(4) }}

file.blockreplace using file.accumulated

Now if you read the previous post on file.blockreplace you may wonder how to use it with accumulators. It will differ a little from the file.managed usage of accumulators.

With file.managed you have this accumulator jinja variable and several keys inside. With file.blockreplace you have nothing to do.

  • If one accumulator is targeted on the same file (the same as the one targeted by the blockreplace), then the blockreplace content attribute will be filled with all the lines contained on this accumulator. Any data directly set in the content attribute is not loose, accumulator data is only added, but the content attribute is not required so it could also be empty.
  • If several accumulators are targeted on this file they will be merged, but if you use several blockreplace states on the same file the accumulators are merged using the requisites dependencies you've made and accumulators names.

This last sentence is maybe weird. We'll make an example to see it, but another way of saying that is that it's magical and it should do the things you think it should do (if not, make bug reports).

So in this example we'll reuse the last example of managing entries in an /etc/hosts file. But we will manage two blocks of edition. So we have theses two states in an hostsedit_acc.sls file:

 1     test-etc-hosts-blockreplace-services-local:
 2       file.blockreplace:
 3         - name: /etc/hosts
 4         - marker_start: "# BLOCK TOP : salt managed zone : local services : please do not edit"
 5         - marker_end: "# BLOCK BOTTOM : local : end of salt managed zone --"
 6         - show_changes: True
 7         - append_if_not_found: True
 8   
9 test-etc-hosts-blockreplace-services-central: 10 file.blockreplace: 11 - name: /etc/hosts 12 - marker_start: "# BLOCK TOP : salt managed zone : central services : please do not edit" 13 - marker_end: "# BLOCK BOTTOM : central : end of salt managed zone --" 14 - show_changes: True 15 - append_if_not_found: True

Same blocks as in the previous guide on blockreplace. But there I removed the content attribute (it could work with a content attribute adding more static stuff, but I do not need it).

So now if I want to use file.accumulated to push some content in theses blocks I just need to do two things:

  • target the blockreplace state id in a requisite to ensure my current state will run before
  • target the same file (name attribute)

Quite simple, but here we have two managed blocks, my accumulated content will be set in the block targeted by my requisite.

Let's try it in a second sls file: hosts_data.sls, but you could split that on more files if everything gets included at the end:

 1     {# Include dependencies #}
 2     include:
 3         - hostsedit_acc
 4   
5 hostadata1-external-google-dns: 6 file.accumulated: 7 - filename: /etc/hosts 8 - text: | 9 8.8.8.8 ns1.google.com 10 8.8.8.4 ns2.google.com 11 - require_in: 12 - file: test-etc-hosts-blockreplace-services-central 13
14 hostadata2-external-thing: 15 file.accumulated: 16 - filename: /etc/hosts 17 - text: "93.184.216.119 : www.example.com" 18 - require_in: 19 - file: test-etc-hosts-blockreplace-services-central 20
21 hostdata3-internal-stuff1: 22 file.accumulated: 23 - filename: /etc/hosts 24 - text: "127.0.0.1 foo bar foo.local.net bar.local.net" 25 - require_in: 26 - file: test-etc-hosts-blockreplace-services-local 27
28 hostdata4-internal-stuff2: 29 file.accumulated: 30 - filename: /etc/hosts 31 - text: | 32 127.0.0.1 db.local.net 33 127.0.0.1 http.local.net 34 127.0.0.1 foobar 35 - require_in: 36 - file: test-etc-hosts-blockreplace-services-local

Note that I did not use the name attribute in theses states. Using name I could name the dictionary key, or we could say I would set the accumulator name. I could use names, but using the same accumulator name with the four states, strange things would happen, data from all theses accumulators would be merged in the same accumulator name. So, either avoid name attributes or use it with different names if the targeted blockedit is different. And test your recipes :-)

Now run it with:

1     $# salt-call -linfo state.sls hosts_data

You should get theses two managed blocks in /etc/hosts, filled from several states:

 1     # BLOCK TOP : salt managed zone : local services : please do not edit
 2     127.0.0.1 foo bar foo.local.net bar.local.net
 3     127.0.0.1 db.local.net
 4     127.0.0.1 http.local.net
 5     127.0.0.1 foobar
 6   
7 # BLOCK BOTTOM : local : end of salt managed zone -- 8 # BLOCK TOP : salt managed zone : central services : please do not edit 9 93.184.216.119 : www.example.com 10 8.8.8.8 ns1.google.com 11 8.8.8.4 ns2.google.com 12
13 # BLOCK BOTTOM : central : end of salt managed zone --

Last words

Note that this example is based on the development github repository of salt. You may not be able to run theses examples on versions prior to 0.18.0. The multiple blockreplace case is one of the last fix added by @kiorky. If you use accumulators you may need to subscribe on this reload_module vs accumulated issue on github. It's a quite general issue, but this actually prevents using accumulators on states with too much distance, as you may loose the accumulated data if something restarted the minion while your states are running.

Stay tuned on twitter, @regilero, @makinacorpus


comments powered by Disqus