SaltStack State Files Written In Python

Generally in SaltStack, state file are written in the YAML + Jinja2 format. However, other options are available. One of these options is Python.

In this post, we will display how a simple state written in YAML + Jinja2 can be written in Python instead. In the example, the function of the state is to take data in a pillar, and serialize it as YAML into a file.

The pillar data used in this example is:

breakfast:
egg: scrambled
meats:
- sausage
- bacon
sides:
spam: gross
beans: baked

dinner:
egg: poached
seafood: lobster
other:
- truffle pate
- mornay sauce
sides:
spam: delicious
egg: over easy
meats:
- steak

The Salt State as written in YAML + Jinja2:

{% set application_pillar = ['breakfast', 'dinner'] %}

{%- for app_pillar in application_pillar %}
serialize key { }:
file.serialize:
- name: /tmp/serialize_test.txt
- user: root
- group: root
- mode: 400
- formatter: yaml
- dataset_pillar: {{ app_pillar }}
- merge_if_exists: True
{% endfor %}

The Salt State as written in Python:

#!py

def _get_data():
keys = ['breakfast', 'dinner']
return keys

def run():
config = {}

for key in _get_data():
config['serialize key '.format(key)] = {
'file.serialize': [
{'name': '/tmp/serialize_test.txt'},
{'user': 'root'},
{'group': 'root'},
{'mode': '400'},
{'formatter': 'yaml'},
{'dataset_pillar': key}
]
}

return config

A few notes on the python version.

  • The function name run is required.
  • The run function must return a dictionary.
  • The shebang, must be !py

Bonus

In SaltStack, the shebang really just specifies the order of renderers to use for the particular file. By default, the shebang is #!jinja|yaml

So… does that mean it’s possible to do #!jinja|py? Well, it seems like it should be, but I had no luck actually getting this to work. The jinja renderer would output what looked like a clean python file but then the py renderer would choke and spit out a syntax error due to a jinja line. For reference, I'll leave the code that was used. Maybe this is a bug that will get patched at some point.

#!jinja|py

{% set application_pillar = ['breakfast', 'dinner'] %}
def run():
config = {}

{% for key in application_pillar %}
config['serialize key { }'] = {
'file.serialize': [
{'name': '/tmp/serialize_test.txt'},
{'user': 'root'},
{'group': 'root'},
{'mode': '400'},
{'formatter': 'yaml'},
{'dataset_pillar': '{ }' }
]
}
{% endfor %}

return config

Update

I reached out to the salt community and was informed that any render pipelines that includes #!py only get evaluated with the py renderer. So that answers that.

Bonus Bonus

So… what about json?

Well if you’re a psychopath hell bent on squeezing out all the speed possible…yes

#!jinja|json

{% set application_pillar = ['breakfast', 'dinner'] %}
{
{%- for app_pillar in application_pillar %}
"serialize key { }": {
"file.serialize": [
{"name": "/tmp/serialize_test.txt"},
{"user": "root"},
{"group": "root"},
{"mode": 400},
{"formatter": "yaml"},
{"dataset_pillar": "{ }" },
{"merge_if_exists": "True"}
]
}{% if not loop.last %},{% endif %}
{%- endfor %}
}

All render times are taken from the Salt Minion which is running as a single core vagrant instance on my laptop. YMMV.

jinja|yaml render time: 0.00130581855774

py render time: 0.000488996505737

jinja|json render time: 5.69820404053e-05 or 0.0000569

Generally, render time doesn’t really matter, but if you have a use case where it might, json is clearly what you're going to want to use.

Originally published at https://smartaleksolutions.com on September 15, 2020.

Climber, surfer, yogi, dad who does some IT on the side to get by.

Climber, surfer, yogi, dad who does some IT on the side to get by.