salt/states/module.py
# -*- coding: utf-8 -*-
r'''
Execution of Salt modules from within states
============================================
.. note::
There are two styles of calling ``module.run``. **The legacy style will no
longer be available starting in the Sodium release.** To opt-in early to the
new style you must add the following to your ``/etc/salt/minion`` config
file:
.. code-block:: yaml
use_superseded:
- module.run
With `module.run` these states allow individual execution module calls to be
made via states. Here's a contrived example, to show you how it's done:
.. code-block:: yaml
# New Style
test.random_hash:
module.run:
- test.random_hash:
- size: 42
- hash_type: sha256
# Legacy Style
test.random_hash:
module.run:
- size: 42
- hash_type: sha256
In the new style, the state ID (``test.random_hash``, in this case) is
irrelevant when using ``module.run``. It could have very well been written:
.. code-block:: yaml
Generate a random hash:
module.run:
- test.random_hash:
- size: 42
- hash_type: sha256
For a simple state like that it's not a big deal, but if the module you're
using has certain parameters, things can get cluttered, fast. Using the
contrived custom module (stuck in ``/srv/salt/_modules/foo.py``, or your
configured file_roots_):
.. code-block:: python
def bar(name, names, fun, state, saltenv):
return "Name: {name} Names: {names} Fun: {fun} State: {state} Saltenv: {saltenv}".format(**locals())
Your legacy state has to look like this:
.. code-block:: yaml
# Legacy style
Unfortunate example:
module.run:
- name: foo.bar
- m_name: Some name
- m_names:
- Such names
- very wow
- m_state: Arkansas
- m_fun: Such fun
- m_saltenv: Salty
With the new style it's much cleaner:
.. code-block:: yaml
# New style
Better:
module.run:
- foo.bar:
- name: Some name
- names:
- Such names
- very wow
- state: Arkansas
- fun: Such fun
- saltenv: Salty
The new style also allows multiple modules in one state. For instance, you can
do this:
.. code-block:: yaml
Do many things:
module.run:
- test.random_hash:
- size: 10
- hash_type: md5
# Note the `:` at the end
- test.true:
- test.arg:
- this
- has
- args
- and: kwargs
- isn't: that neat?
# Note the `:` at the end, too
- test.version:
- test.fib:
- 4
Where in the legacy style you would have had to split your states like this:
.. code-block:: yaml
test.random_hash:
module.run:
- size: 10
- hash_type: md5
test.nop:
module.run
test.arg:
module.run:
- args:
- this
- has
- args
- kwargs:
and: kwargs
isn't: that neat?
test.version:
module.run
Another difference is that in the legacy style, unconsumed arguments to the
``module`` state were simply passed into the module function being executed:
.. code-block:: yaml
show off module.run with args:
module.run:
- name: test.random_hash
- size: 42
- hash_type: sha256
The new style is much more explicit, with the arguments and keyword arguments
being nested under the name of the function:
.. code-block:: yaml
show off module.run with args:
module.run:
# Note the lack of `name: `, and trailing `:`
- test.random_hash:
- size: 42
- hash_type: sha256
If the function takes ``*args``, they can be passed in as well:
.. code-block:: yaml
args and kwargs:
module.run:
- test.arg:
- isn't
- this
- fun
- this: that
- salt: stack
Modern Examples
---------------
Here are some other examples using the modern ``module.run``:
.. code-block:: yaml
fetch_out_of_band:
module.run:
- git.fetch:
- cwd: /path/to/my/repo
- user: myuser
- opts: '--all'
A more complex example:
.. code-block:: yaml
eventsviewer:
module.run:
- task.create_task:
- name: events-viewer
- user_name: System
- action_type: Execute
- cmd: 'c:\netops\scripts\events_viewer.bat'
- trigger_type: 'Daily'
- start_date: '2017-1-20'
- start_time: '11:59PM'
It is sometimes desirable to trigger a function call after a state is executed,
for this the :mod:`module.wait <salt.states.module.wait>` state can be used:
.. code-block:: yaml
add example to hosts:
file.append:
- name: /etc/hosts
- text: 203.0.113.13 example.com
# New Style
mine.send:
module.wait:
# Again, note the trailing `:`
- hosts.list_hosts:
- watch:
- file: add example to hosts
Legacy (Default) Examples
-------------------------
If you're using the legacy ``module.run``, due to how the state system works,
if a module function accepts an argument called, ``name``, then ``m_name`` must
be used to specify that argument, to avoid a collision with the ``name``
argument.
Here is a list of keywords hidden by the state system, which must be prefixed
with ``m_``:
* fun
* name
* names
* state
* saltenv
For example:
.. code-block:: yaml
disable_nfs:
module.run:
- name: service.disable
- m_name: nfs
Note that some modules read all or some of the arguments from a list of keyword
arguments. For example:
.. code-block:: yaml
mine.send:
module.run:
- func: network.ip_addrs
- kwargs:
interface: eth0
.. code-block:: yaml
cloud.create:
module.run:
- func: cloud.create
- provider: test-provider
- m_names:
- test-vlad
- kwargs: {
ssh_username: 'ubuntu',
image: 'ami-8d6d9daa',
securitygroup: 'default',
size: 'c3.large',
location: 'ap-northeast-1',
delvol_on_destroy: 'True'
}
Other modules take the keyword arguments using this style:
.. code-block:: yaml
mac_enable_ssh:
module.run:
- name: system.set_remote_login
- enable: True
Another example that creates a recurring task that runs a batch file on a
Windows system:
.. code-block:: yaml
eventsviewer:
module.run:
- name: task.create_task
- m_name: 'events-viewer'
- user_name: System
- kwargs: {
action_type: 'Execute',
cmd: 'c:\netops\scripts\events_viewer.bat',
trigger_type: 'Daily',
start_date: '2017-1-20',
start_time: '11:59PM'
}
.. _file_roots: https://docs.saltstack.com/en/latest/ref/configuration/master.html#file-roots
'''
from __future__ import absolute_import, print_function, unicode_literals
# Import salt libs
import salt.loader
import salt.utils.args
import salt.utils.functools
import salt.utils.jid
from salt.ext import six
from salt.ext.six.moves import range
from salt.ext.six.moves import zip
from salt.exceptions import SaltInvocationError
from salt.utils.decorators import with_deprecated
def wait(name, **kwargs):
'''
Run a single module function only if the watch statement calls it
``name``
The module function to execute
``**kwargs``
Pass any arguments needed to execute the function
.. note::
Like the :mod:`cmd.run <salt.states.cmd.run>` state, this state will
return ``True`` but not actually execute, unless one of the following
two things happens:
1. The state has a :ref:`watch requisite <requisites-watch>`, and
the state which it is watching changes.
2. Another state has a :ref:`watch_in requisite
<requisites-watch-in>` which references this state, and the state
wth the ``watch_in`` changes.
'''
return {'name': name,
'changes': {},
'result': True,
'comment': ''}
# Alias module.watch to module.wait
watch = salt.utils.functools.alias_function(wait, 'watch')
@with_deprecated(globals(), "Sodium", policy=with_deprecated.OPT_IN)
def run(**kwargs):
'''
Run a single module function or a range of module functions in a batch.
Supersedes ``module.run`` function, which requires ``m_`` prefix to
function-specific parameters.
:param returner:
Specify a common returner for the whole batch to send the return data
:param kwargs:
Pass any arguments needed to execute the function(s)
.. code-block:: yaml
some_id_of_state:
module.run:
- network.ip_addrs:
- interface: eth0
- cloud.create:
- names:
- test-isbm-1
- test-isbm-2
- ssh_username: sles
- image: sles12sp2
- securitygroup: default
- size: 'c3.large'
- location: ap-northeast-1
- delvol_on_destroy: True
:return:
'''
if 'name' in kwargs:
kwargs.pop('name')
ret = {
'name': list(kwargs),
'changes': {},
'comment': '',
'result': None,
}
functions = [func for func in kwargs if '.' in func]
missing = []
tests = []
for func in functions:
func = func.split(':')[0]
if func not in __salt__:
missing.append(func)
elif __opts__['test']:
tests.append(func)
if tests or missing:
ret['comment'] = ' '.join([
missing and "Unavailable function{plr}: "
"{func}.".format(plr=(len(missing) > 1 or ''),
func=(', '.join(missing) or '')) or '',
tests and "Function{plr} {func} to be "
"executed.".format(plr=(len(tests) > 1 or ''),
func=(', '.join(tests)) or '') or '',
]).strip()
ret['result'] = not (missing or not tests)
if ret['result'] is None:
ret['result'] = True
failures = []
success = []
for func in functions:
_func = func.split(':')[0]
try:
func_ret = _call_function(_func, returner=kwargs.get('returner'),
func_args=kwargs.get(func))
if not _get_result(func_ret, ret['changes'].get('ret', {})):
if isinstance(func_ret, dict):
failures.append("'{0}' failed: {1}".format(
func, func_ret.get('comment', '(error message N/A)')))
else:
success.append('{0}: {1}'.format(
func, func_ret.get('comment', 'Success') if isinstance(func_ret, dict) else func_ret))
ret['changes'][func] = func_ret
except (SaltInvocationError, TypeError) as ex:
failures.append("'{0}' failed: {1}".format(func, ex))
ret['comment'] = ', '.join(failures + success)
ret['result'] = not bool(failures)
return ret
def _call_function(name, returner=None, **kwargs):
'''
Calls a function from the specified module.
:param name:
:param kwargs:
:return:
'''
argspec = salt.utils.args.get_function_argspec(__salt__[name])
# func_kw is initialized to a dictionary of keyword arguments the function to be run accepts
func_kw = dict(zip(argspec.args[-len(argspec.defaults or []):], # pylint: disable=incompatible-py3-code
argspec.defaults or []))
# func_args is initialized to a list of positional arguments that the function to be run accepts
func_args = argspec.args[:len(argspec.args or []) - len(argspec.defaults or [])]
arg_type, kw_to_arg_type, na_type, kw_type = [], {}, {}, False
for funcset in reversed(kwargs.get('func_args') or []):
if not isinstance(funcset, dict):
# We are just receiving a list of args to the function to be run, so just append
# those to the arg list that we will pass to the func.
arg_type.append(funcset)
else:
for kwarg_key in six.iterkeys(funcset):
# We are going to pass in a keyword argument. The trick here is to make certain
# that if we find that in the *args* list that we pass it there and not as a kwarg
if kwarg_key in func_args:
kw_to_arg_type[kwarg_key] = funcset[kwarg_key]
continue
else:
# Otherwise, we're good and just go ahead and pass the keyword/value pair into
# the kwargs list to be run.
func_kw.update(funcset)
arg_type.reverse()
for arg in func_args:
if arg in kw_to_arg_type:
arg_type.append(kw_to_arg_type[arg])
_exp_prm = len(argspec.args or []) - len(argspec.defaults or [])
_passed_prm = len(arg_type)
missing = []
if na_type and _exp_prm > _passed_prm:
for arg in argspec.args:
if arg not in func_kw:
missing.append(arg)
if missing:
raise SaltInvocationError('Missing arguments: {0}'.format(', '.join(missing)))
elif _exp_prm > _passed_prm:
raise SaltInvocationError('Function expects {0} parameters, got only {1}'.format(
_exp_prm, _passed_prm))
mret = __salt__[name](*arg_type, **func_kw)
if returner is not None:
returners = salt.loader.returners(__opts__, __salt__)
if returner in returners:
returners[returner]({'id': __opts__['id'], 'ret': mret,
'fun': name, 'jid': salt.utils.jid.gen_jid(__opts__)})
return mret
def _run(name, **kwargs):
'''
.. deprecated:: 2017.7.0
Function name stays the same, behaviour will change.
Run a single module function
``name``
The module function to execute
``returner``
Specify the returner to send the return of the module execution to
``kwargs``
Pass any arguments needed to execute the function
'''
ret = {'name': name,
'changes': {},
'comment': '',
'result': None}
if name not in __salt__:
ret['comment'] = 'Module function {0} is not available'.format(name)
ret['result'] = False
return ret
if __opts__['test']:
ret['comment'] = 'Module function {0} is set to execute'.format(name)
return ret
aspec = salt.utils.args.get_function_argspec(__salt__[name])
args = []
defaults = {}
arglen = 0
deflen = 0
if isinstance(aspec.args, list):
arglen = len(aspec.args)
if isinstance(aspec.defaults, tuple):
deflen = len(aspec.defaults)
# Match up the defaults with the respective args
for ind in range(arglen - 1, -1, -1):
minus = arglen - ind
if deflen - minus > -1:
defaults[aspec.args[ind]] = aspec.defaults[-minus]
# overwrite passed default kwargs
for arg in defaults:
if arg == 'name':
if 'm_name' in kwargs:
defaults[arg] = kwargs.pop('m_name')
elif arg == 'fun':
if 'm_fun' in kwargs:
defaults[arg] = kwargs.pop('m_fun')
elif arg == 'state':
if 'm_state' in kwargs:
defaults[arg] = kwargs.pop('m_state')
elif arg == 'saltenv':
if 'm_saltenv' in kwargs:
defaults[arg] = kwargs.pop('m_saltenv')
if arg in kwargs:
defaults[arg] = kwargs.pop(arg)
missing = set()
for arg in aspec.args:
if arg == 'name':
rarg = 'm_name'
elif arg == 'fun':
rarg = 'm_fun'
elif arg == 'names':
rarg = 'm_names'
elif arg == 'state':
rarg = 'm_state'
elif arg == 'saltenv':
rarg = 'm_saltenv'
else:
rarg = arg
if rarg not in kwargs and arg not in defaults:
missing.add(rarg)
continue
if arg in defaults:
args.append(defaults[arg])
else:
args.append(kwargs.pop(rarg))
if missing:
comment = 'The following arguments are missing:'
for arg in missing:
comment += ' {0}'.format(arg)
ret['comment'] = comment
ret['result'] = False
return ret
if aspec.varargs:
if aspec.varargs == 'name':
rarg = 'm_name'
elif aspec.varargs == 'fun':
rarg = 'm_fun'
elif aspec.varargs == 'names':
rarg = 'm_names'
elif aspec.varargs == 'state':
rarg = 'm_state'
elif aspec.varargs == 'saltenv':
rarg = 'm_saltenv'
else:
rarg = aspec.varargs
if rarg in kwargs:
varargs = kwargs.pop(rarg)
if not isinstance(varargs, list):
msg = "'{0}' must be a list."
ret['comment'] = msg.format(aspec.varargs)
ret['result'] = False
return ret
args.extend(varargs)
nkwargs = {}
if aspec.keywords and aspec.keywords in kwargs:
nkwargs = kwargs.pop(aspec.keywords)
if not isinstance(nkwargs, dict):
msg = "'{0}' must be a dict."
ret['comment'] = msg.format(aspec.keywords)
ret['result'] = False
return ret
try:
if aspec.keywords:
mret = __salt__[name](*args, **nkwargs)
else:
mret = __salt__[name](*args)
except Exception as e:
ret['comment'] = 'Module function {0} threw an exception. Exception: {1}'.format(name, e)
ret['result'] = False
return ret
else:
if mret is not None or mret is not {}:
ret['changes']['ret'] = mret
if 'returner' in kwargs:
ret_ret = {
'id': __opts__['id'],
'ret': mret,
'fun': name,
'jid': salt.utils.jid.gen_jid(__opts__)}
returners = salt.loader.returners(__opts__, __salt__)
if kwargs['returner'] in returners:
returners[kwargs['returner']](ret_ret)
ret['comment'] = 'Module function {0} executed'.format(name)
ret['result'] = _get_result(mret, ret['changes'])
return ret
def _get_result(func_ret, changes):
res = True
# if mret is a dict and there is retcode and its non-zero
if isinstance(func_ret, dict) and func_ret.get('retcode', 0) != 0:
res = False
# if its a boolean, return that as the result
elif isinstance(func_ret, bool):
res = func_ret
else:
changes_ret = changes.get('ret', {})
if isinstance(changes_ret, dict):
if isinstance(changes_ret.get('result', {}), bool):
res = changes_ret.get('result', {})
elif changes_ret.get('retcode', 0) != 0:
res = False
# Explore dict in depth to determine if there is a
# 'result' key set to False which sets the global
# state result.
else:
res = _get_dict_result(changes_ret)
return res
def _get_dict_result(node):
ret = True
for key, val in six.iteritems(node):
if key == 'result' and val is False:
ret = False
break
elif isinstance(val, dict):
ret = _get_dict_result(val)
if ret is False:
break
return ret
mod_watch = salt.utils.functools.alias_function(run, 'mod_watch')