- Feature Name: Minion Capabilitles
- Start Date: 2018-11-22
- RFC PR:
- Salt Issue:
- Keywords: capability capabilities introspect introspection
Mechanism to know if a minion is capable of doing something specific, that wasnt known during the version update.
Unlike client-less systems, Salt has should deal with the client systems. These clients supposed to be compatible with the states, sent from the master node. When updates are frequent, two versions backward-compatibility sorts out this problem naturally. But there is a number of use cases when this does not apply. Few examples:
-
if an enterprise system is not updated often, even Salt client stays with old version or yet only required features are back-ported.
-
if a new feature wasn't released to the older version but only back-ported by supporting vendor.
The use-case above makes such client unknown what precise features has been back-ported and what was not. Any back-ported feature was explicitly placed by exact request. That means, that on two outdated minions can be features that are in common, but some might be missing on both minions or just one. Consider the situation, when minions A, B and C, where A has all new features 1, 2 and 3; B has only feature 2 and C has feature 1 and 3.
Some features might be very subtle, e.g. package locking support during their update or some extra parameter has been added to the existing function or data is returned slightly different etc.
How do we know that in the only SLS we're executing on all minions at once?
Introduce introspection variable to Jinja templates, called capable
.
Its proposed design allows to "look inside" into any function or
corner of the minion, if needed, and then by its results SLS logic can
continue on decision what to do next.
This variable works like a tree and can match any of the branches. It has following layout:
capable
|
+--- modules
| |
| +-- <modulename>.<function>
| |
| +-- name
| |
| +-- signature
| | |
| | +-- params
| | |
| | +-- args
| | |
| | +-- kwargs
| | |
| | +-- defaults
| | |
| | +-- spec
| |
| +-- doc
| |
| +-- has_parameter
| |
| +-- contains
|
+--- states
| |
| +-- <statename>.<function>
| |
| +-- ... (see modules)
+--- config
|
+--- pillars
Such tree would be getting the information about particular
function. The capable
variable would lazy-loading particular data on
demand when accessed.
This is an example how syntax would look like:
{% if 'host' in capable.modules.network.ping.signature.params %}
{# do something with network.ping #}
{% endif %}
From this example, capable
loads network
module, introspects
ping
function and reports if its signature contains accepted
parameter, called host
.
However, since not always signature can be explicit. In case, when
parameters aren't enlisted explicitly, but are covered in **kwargs
or *args
, they have to be properly documented. And so the check
can be done by accessing function documentation:
{% if capable.modules.network.ping.doc.has_parameter('host') %}
{# do something with network.ping #}
{% endif %}
Sometimes back-ported feature might only mention the change in documentation of the function, without introducing any parameters to the signature at all. For this can be introspected its content:
{% if capable.modules.network.ping.doc.contains('Performs at ICMP ping') %}
{# do something with network.ping #}
{% endif %}
Note that capable
is designed to never fail if wrong tree branch is
accessed. For example, this will just result to False
:
{% if capable.who.knows.what.is.this %}
...
{% endif %}
Also iterations works the same way:
{% if 'something' in capable.who.knows.what.is.this %}
...
{% endif %}
...or hashes:
{% if capable.who.knows.what.is.this['something'] %}
...
{% endif %}
...or even crazy hashes:
{% if capable.who['knows'].what['is'].this['something'].there() %}
...
{% endif %}
The capable
variable essentially can replace-or-help to grains
,
salt['<function>']
and pillar
dictionaries bringing lazy loading
to the data and syntax sugar. For example, we can get rid of calling
functions as a string keys of salt
dictionary.
For example, this is a current way:
{% if salt['pillar.get']('something') == 'foo' %}
{% set repos = salt['pkg.list_repos']() %}
{% endif %}
Since capable
can be installed as salt
filter, this can be done cleaner:
{% if salt.pillar.get('something') == 'foo' %}
{% set repos = salt.pkg.list_repos() %}
{% endif %}
The second example works exactly as an example above, just no more strings and hashing. Note that the old syntax is preserved as is and can co-exist in parallel without problems.
To wrap this up, this can:
- Lazy load only what is needed at the moment
- Help reading and writing SLS much cleaner than before
There is not much alternatives to this.
Version tracking will not work, because if a particuar little change has been made to the package, it is still unknown how version 1234.5.6 different from 1234.5.6 with a different build. Date/time of last build time also is not helping, if there are specifically patched minions for a particuar user.
Keeping a map of changes next to the version requires thorough maintenance and is prone to errors.
N/A
This feature requires to be back-ported to all supported versions in order to be in use.