Enhance SLS syntax with simpler way of writing "things".
Writing SLS can be not always easy and friendly. Especially when you want to do various operations on the same object. Main complain is heard that in order to do something again or run the same function e.g. on the same file, one would need to create another ID for it.
The following example will get content of /etc/<something>-release
,
based on os_family
grain, if it is e.g. RedHat
:
rhelrelease:
cmd.run:
- name: cat /etc/redhat-release
- onlyif: test -f /etc/redhat-release
centosrelease:
cmd.run:
- name: cat /etc/centos-release
- onlyif: test -f /etc/centos-release
This will return a structure with an empty rhelrelease
and content
of the release file under centosrelease
. From API point of view, one
would need to check both keys in the structure.
Of course, it is possible to write it a bit better then that, something like:
{% if grains['os_family'] == 'RedHat' %}
{% set releasefile = 'centos' if grains['os'] == 'CentOS' else 'redhat' %}
release:
cmd.run:
- name: cat /etc/{{ releasefile }}-release
- onlyif: test -f /etc/{{ releasefile }}-release
{%endif%}
It is still gets more complicated if older opensuse or SLES machines also needs to be looked up. And yet this SLS is not finished as even more careful logic needed to be added in order to handle cases where no OS is detected. At this point SLS is no longer simple and declarative, but looks more like programming.
We can make it simpler. For example as follows:
release:
cmd:
{% for releasefile in ['redhat', 'centos', 'SuSE', 'sles'] %}
- run:
- name: cat /etc/{{releasefile}}-release
- onlyif: test -f /etc/{{releasefile}}-release
{% endfor %}
In the example above, the for-loop will just generate four times same
statement to call cmd.run
function, reusing already built-in check
on onlyif
. That said, no more need to figure out what OS name
belongs to what family etc.
The example above only made for show-case to let you get the idea.
As simple as not specifying particular function. Typically, in Salt it is done this way:
/etc/some.conf:
somemodule.some_function: # <--- here!
- params...
The simple syntax will be invoked as so:
/etc/some.conf:
somemodule: # <--- just like that, no function
- some_function:
- params ...
Of course, in single call this dosn't make much sense and is actually one line more to write. However, if there is more one operation that needed to be done, this makes a big difference. Since it is impossible to have more than one same ID, you will be required to write different IDs for the same group of tasks, if we grouping them by the targeted object. This results into something like this:
do_something_with_my_config:
somemodule.some_function:
- name: /etc/some.conf
- params ....
do_something_with_my_config_again:
somemodule.some_other_function:
- name: /etc/some.conf
- params ....
do_something_with_my_config_and_over_again:
somemodule.some_third_function:
- name: /etc/some.conf
- params ....
With the batch-mode, the same above can be re-written in much more efficient way as follows:
/etc/some.conf:
somemodule:
- some_function
- params ....
- some_other_function
- params ....
- some_third_function
- params ....
It works in three steps:
During state compile, compiler finds that there is no end function
defined, then it injects __call__
function name into the
state. Essentially, referring to the example above, state will be
simply altered to somemodule.__call__
, but transparently for the
user. LazyLoader does not allow any functions that starts with _
as they are considered private/hidden. The __call__
is an
exception but it should be still impossible to invoke it from the
module directly.
When LazyLoader loads a module, it will inject a generic function
named __call__
at the module level. If such function is already
defined inside the module, LazyLoader will not inject that function
therefore.
The __call__
function takes a list of sets structure, as shown above,
assuming that the key of the set is the function name of the parent
element somemodule
, which is a module name, and essentially
performs the following:
ret = []
for function_name in functions:
args, kwargs = functions[function_name]['args'], functions[function_name]['kwargs']
ret.append(getattr(this_module_instance, function_name)(*args, **kwargs))
Nothing. Also no changes to any future modules, yet to be written. If
module has non-traditional way of exposing methods, e.g. implemented
as class or functions made dynamically, in this case __call__
should
be made separately for that. However, since this is advanced way of
making modules, developer already knows what he is doing in this case.
Or "lazy" implementation, if one wants to just turn this feature off:
__call__ = lambda (*a, **k): raise SaltNotSupportedError()
No impact. Performance and default syntax should stay unchanged.