12 KiB
YAML Idiosyncrasies
One of Salt's strengths, the use of existing serialization systems for representing SLS data, can also backfire. YAML is a general purpose system and there are a number of things that would seem to make sense in an sls file that cause YAML issues. It is wise to be aware of these issues. While reports or running into them are generally rare they can still crop up at unexpected times.
Spaces vs Tabs
YAML uses spaces,
period. Do not use tabs in your SLS files! If strange errors are coming
up in rendering SLS files, make sure to check that no tabs have crept
in! In Vim, after enabling search highlighting with:
:set hlsearch
, you can check with the following key
sequence in normal mode(you can hit ESC
twice to be sure): /
, Ctrl-v, Tab,
then hit Enter. Also, you can convert
tabs to 2 spaces by these commands in Vim:
:set tabstop=2 expandtab
and then :retab
.
Indentation
The suggested syntax for YAML files is to use 2 spaces for indentation, but YAML will follow whatever indentation system that the individual file uses. Indentation of two spaces works very well for SLS files given the fact that the data is uniform and not deeply nested.
Nested Dictionaries
When dictionaries are nested within other data structures
(particularly lists), the indentation logic sometimes changes. Examples
of where this might happen include context
and
default
options from the file.managed
<salt.states.file>
state:
/etc/http/conf/http.conf:
file:
- managed
- source: salt://apache/http.conf
- user: root
- group: root
- mode: 644
- template: jinja
- context:
custom_var: "override"
- defaults:
custom_var: "default value"
other_var: 123
Notice that while the indentation is two spaces per level, for the
values under the context
and defaults
options
there is a four-space indent. If only two spaces are used to indent,
then those keys will be considered part of the same dictionary that
contains the context
key, and so the data will not be
loaded correctly. If using a double indent is not desirable, then a
deeply-nested dict can be declared with curly braces:
/etc/http/conf/http.conf:
file:
- managed
- source: salt://apache/http.conf
- user: root
- group: root
- mode: 644
- template: jinja
- context: {
custom_var: "override" }
- defaults: {
custom_var: "default value",
other_var: 123 }
Here is a more concrete example of how YAML actually handles these indentations, using the Python interpreter on the command line:
>>> import yaml
>>> yaml.safe_load('''mystate:
... file.managed:
... - context:
... some: var''')
'mystate': {'file.managed': [{'context': {'some': 'var'}}]}}
{>>> yaml.safe_load('''mystate:
... file.managed:
... - context:
... some: var''')
'mystate': {'file.managed': [{'some': 'var', 'context': None}]}} {
Note that in the second example, some
is added as
another key in the same dictionary, whereas in the first example, it's
the start of a new dictionary. That's the distinction.
context
is a common example because it is a keyword arg for
many functions, and should contain a dictionary.
True/False, Yes/No, On/Off
PyYAML will load these values as boolean True
or
False
. Un-capitalized versions will also be loaded as
booleans (true
, false
, yes
,
no
, on
, and off
). This can be
especially problematic when constructing Pillar data. Make sure that
your Pillars which need to use the string versions of these values are
enclosed in quotes. Pillars will be parsed twice by salt, so you'll need
to wrap your values in multiple quotes, including double quotation marks
(" "
) and single quotation marks (' '
). Note
that spaces are included in the quotation type examples for clarity.
Multiple quoting examples looks like this:
- '"false"'
- "'True'"
- "'YES'"
- '"No"'
Note
When using multiple quotes in this manner, they must be different.
Using "" ""
or '' ''
won't work in this case
(spaces are included in examples for clarity).
The '%' Sign
The % symbol has a special meaning in YAML, it needs to be passed as a string literal:
cheese:
ssh_auth.present:
- user: tbortels
- source: salt://ssh_keys/chease.pub
- config: '%h/.ssh/authorized_keys'
Time Expressions
PyYAML will load a time expression as the integer value of that,
assuming HH:MM
. So for example, 12:00
is
loaded by PyYAML as 720
. An excellent explanation for why
can be found here.
To keep time expressions like this from being loaded as integers, always quote them.
Note
When using a jinja load_yaml
map, items must be quoted
twice. For example:
{% load_yaml as wsus_schedule %}
FRI_10:
time: '"23:00"'
day: 6 - Every Friday
SAT_10:
time: '"06:00"'
day: 7 - Every Saturday
SAT_20:
time: '"14:00"'
day: 7 - Every Saturday
SAT_30:
time: '"22:00"'
day: 7 - Every Saturday
SUN_10:
time: '"06:00"'
day: 1 - Every Sunday
{% endload %}
YAML does not like "Double Short Decs"
If I can find a way to make YAML accept "Double Short Decs" then I will, since I think that double short decs would be awesome. So what is a "Double Short Dec"? It is when you declare a multiple short decs in one ID. Here is a standard short dec, it works great:
vim:
pkg.installed
The short dec means that there are no arguments to pass, so it is not required to add any arguments, and it can save space.
YAML though, gets upset when declaring multiple short decs, for the record...
THIS DOES NOT WORK:
vim:
pkg.installed
user.present
Similarly declaring a short dec in the same ID dec as a standard dec does not work either...
ALSO DOES NOT WORK:
fred:
user.present
ssh_auth.present:
- name: AAAAB3NzaC...
- user: fred
- enc: ssh-dss
- require:
- user: fred
The correct way is to define them like this:
vim:
pkg.installed: []
user.present: []
fred:
user.present: []
ssh_auth.present:
- name: AAAAB3NzaC...
- user: fred
- enc: ssh-dss
- require:
- user: fred
Alternatively, they can be defined the "old way", or with multiple "full decs":
vim:
pkg:
- installed
user:
- present
fred:
user:
- present
ssh_auth:
- present
- name: AAAAB3NzaC...
- user: fred
- enc: ssh-dss
- require:
- user: fred
YAML supports only plain ASCII
According to YAML specification, only ASCII characters can be used.
Within double-quotes, special characters may be represented with C-style escape sequences starting with a backslash ( \ ).
Examples:
- micro: "\u00b5"
- copyright: "\u00A9"
- A: "\x41"
- alpha: "\u0251"
- Alef: "\u05d0"
List of usable Unicode characters will help you to identify correct numbers.
Python can also be used to discover the Unicode number for a character:
repr(u"Text with wrong characters i need to figure out")
This shell command can find wrong characters in your SLS files:
find . -name '*.sls' -exec grep --color='auto' -P -n '[^\x00-\x7F]' \{} \;
Alternatively you can toggle the yaml_utf8 setting in your master configuration file. This is still an experimental setting but it should manage the right encoding conversion in salt after yaml states compilations.
Underscores stripped in Integer Definitions
If a definition only includes numbers and underscores, it is parsed by YAML as an integer and all underscores are stripped. To ensure the object becomes a string, it should be surrounded by quotes. More information here.
Here's an example:
>>> import yaml
>>> yaml.safe_load('2013_05_10')
20130510
>>> yaml.safe_load('"2013_05_10"')
'2013_05_10'
Automatic datetime
conversion
If there is a value in a YAML file formatted
2014-01-20 14:23:23
or similar, YAML will automatically
convert this to a Python datetime
object. These objects are
not msgpack serializable, and so may break core salt functionality. If
values such as these are needed in a salt YAML file (specifically a
configuration file), they should be formatted with surrounding strings
to force YAML to serialize them as strings:
>>> import yaml
>>> yaml.safe_load('2014-01-20 14:23:23')
2014, 1, 20, 14, 23, 23)
datetime.datetime(>>> yaml.safe_load('"2014-01-20 14:23:23"')
'2014-01-20 14:23:23'
Additionally, numbers formatted like XXXX-XX-XX
will
also be converted (or YAML will attempt to convert them, and error out
if it doesn't think the date is a real one). Thus, for example, if a
minion were to have an ID of 4017-16-20
the minion would
not start because YAML would complain that the date was out of range.
The workaround is the same, surround the offending string with
quotes:
>>> import yaml
>>> yaml.safe_load('4017-16-20')
Traceback (most recent call last):"<stdin>", line 1, in <module>
File "/usr/local/lib/python2.7/site-packages/yaml/__init__.py", line 93, in safe_load
File return load(stream, SafeLoader)
"/usr/local/lib/python2.7/site-packages/yaml/__init__.py", line 71, in load
File return loader.get_single_data()
"/usr/local/lib/python2.7/site-packages/yaml/constructor.py", line 39, in get_single_data
File return self.construct_document(node)
"/usr/local/lib/python2.7/site-packages/yaml/constructor.py", line 43, in construct_document
File = self.construct_object(node)
data "/usr/local/lib/python2.7/site-packages/yaml/constructor.py", line 88, in construct_object
File = constructor(self, node)
data "/usr/local/lib/python2.7/site-packages/yaml/constructor.py", line 312, in construct_yaml_timestamp
File return datetime.date(year, month, day)
ValueError: month must be in 1..12
>>> yaml.safe_load('"4017-16-20"')
'4017-16-20'
Keys Limited to 1024 Characters
Simple keys are limited by the YAML Spec to a single line, and cannot be longer that 1024 characters. PyYAML enforces these limitations (see here), and therefore anything parsed as YAML in Salt is subject to them.