What's the best way for a formula to provide default attributes?
The chef has a very complex (perhaps too large) cookbook schema to provide default attribute values. I think Puppet is doing something similar with class parameters, where the default values usually go to params.pp
. With Salt I saw:
- specifying a default value in dictionary / column searches.
-
grains.filter_by
merging default attribute values with user-supplied pillar data (like map.jinja in apache-formula ) - when invoking the state
file.managed
, specifying the default attribute values as a parameterdefaults
and the user-specified column data ascontext
.
Option 1 seems to be the most common, but has the disadvantage that the template file becomes very difficult to read. It also requires the default to be repeated whenever a search is performed, making it very easy to make a mistake.
Option 2 feels closest to a chef approach, but seems to expect the default defaults to break down into a dictionary of cases based on some filtering attribute (like OS type written in beans).
Option 3 isn't bad, but it puts the default attributes in the state file instead of splitting them into its own file like Option 2.
Saltstack best practice doc supports option 2, except that it does not decide how to combine the default values with user-specified values without using grains.filter_by
. Is there a way to get around this?
source to share
The defaults.get behavior changed in 2015 .8, possibly due to a bug. This answer describes a compatible method of getting the same results (minimum) 2015.8 and later.
Let's assume your formula tree looks like this:
something/
files/
template.jinja
init.sls
defaults.yaml
# defaults.yaml
conf_location: /etc/something.conf
conf_source: salt://something/files/template.jinja
# pillar/something.sls
something:
conf_location: /etc/something/something.conf
The idea is that the defaults are in default. yaml, but can be overridden on a column. Anything not listed in the column should use the default value. You can accomplish this with a few lines at the top of any given .sls:
# something/init.sls
{%- set pget = salt['pillar.get'] %} # Convenience alias
{%- import_yaml slspath + "/defaults.yaml" as defaults %}
{%- set something = pget('something', defaults, merge=True) %}
something-conf-file:
file.managed:
- name: {{ something.conf_location }}
- source: {{ something.conf_source }}
- template: jinja
- context:
slspath: {{ slspath }}
... and so on.
What it does: The content of defaults.yaml is loaded as a nested dictionary. This nested dictionary is then concatenated with the content of the column key something
, with collisions causing the pillars. The result is a nested dictionary containing both default values and any column overrides that can then be used directly without regard to where the particular value came from.
slspath
not strictly required for this; it is a magic variable that contains the directory path to the current sls. I like to use it because it separates the formula from any specific location in the directory tree. It is usually not available from managed templates, so I pass it as explicit context above. It may not work as expected in older versions, in which case you will have to specify a path relative to the root of the salt tree.
The downside to this method is that, as far as I know, you cannot access the final dictionary with a honeycomb colon-based nested key syntax; you need to go down through it one level at a time. I had no problem with this (the dot syntax is easier to type anyway), but it's a downside. Another disadvantage is the need for multiple lines of template at the top of any .sls or template using this technology.
There are several sides. One of them is that you can iterate over the final dictionary or its subcommands with the help .items()
and the right thing will happen, which was not the case with defaults.get, and it got me crazy. The other is that if and when the salt command restores the old defaults.get functions, the default / column structure suggested here is already compatible and they will work just fine with each other.
source to share
Note. The behavior of defaults.get has changed in version 2015.8 and therefore the method described here no longer works. I am leaving this answer for users of older versions and will post a similar method for current versions.
defaults.get
combined with the file defaults.yaml
should do what you want. Let's assume your formula tree looks like this:
my-formula/
files/
template.jinja
init.sls
defaults.yaml
# my-formula/init.sls
my-formula-conf-file:
file.managed:
- name: {{ salt['defaults.get']('conf_location') }}
- source: {{ salt['defaults.get']('conf_source') }}
... and so on.
# defaults.yaml
conf_location: /etc/my-formula.conf
conf_source: salt://my-formula/files/template.jinja
# pillar/my-formula.sls
my-formula:
conf_location: /etc/my-formula/something.conf
This will end up with a config file placed in /etc/my-formula/something.conf
(column value) using salt://my-formula/files/template.jinja
as source (the default for which no column override was checked).
Note the default unintuitive column and file structure; defaults.get
expects defaults.yaml
to have its values at the root of the file, but expects the column to be overridden in a dictionary named by the formula, since consistency is weak.
The documentation for defaults.get gives its example using defaults.json
instead defaults.yaml
. It works, but I find the yaml to be much more readable. And is available for recording.
source to share