Merge pull request #54993 from mchugh19/port-52187

master-port 52187 and 52351
This commit is contained in:
Daniel Wozniak 2019-11-11 17:03:15 -07:00 committed by GitHub
commit 6472827ff5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 213 additions and 5 deletions

View File

@ -3,3 +3,30 @@
==================================
Salt Release Notes - Codename Neon
==================================
Slot Syntax Updates
===================
The slot syntax has been updated to support parsing dictionary responses and to append text.
.. code-block:: yaml
demo dict parsing and append:
test.configurable_test_state:
- name: slot example
- changes: False
- comment: __slot__:salt:test.arg(shell="/bin/bash").kwargs.shell ~ /appended
.. code-block:: none
local:
----------
ID: demo dict parsing and append
Function: test.configurable_test_state
Name: slot example
Result: True
Comment: /bin/bash/appended
Started: 09:59:58.623575
Duration: 1.229 ms
Changes:

View File

@ -5,6 +5,7 @@ Slots
=====
.. versionadded:: 2018.3.0
.. versionchanged:: Neon
.. note:: This functionality is under development and could be changed in the
future releases
@ -33,7 +34,14 @@ Slot syntax looks close to the simple python function call.
__slot__:salt:<module>.<function>(<args>, ..., <kwargs...>, ...)
Also there are some specifics in the syntax coming from the execution functions
For the Neon release, this syntax has been updated to support parsing functions
which return dictionaries and for appending text to the slot result.
.. code-block:: text
__slot__:salt:<module>.<function>(<args>..., <kwargs...>, ...).dictionary ~ append
There are some specifics in the syntax coming from the execution functions
nature and a desire to simplify the user experience. First one is that you
don't need to quote the strings passed to the slots functions. The second one
is that all arguments handled as strings.
@ -51,3 +59,12 @@ This will execute the :py:func:`test.echo <salt.modules.test.echo>` execution
functions right before calling the state. The functions in the example will
return `/tmp/some_file` and `/etc/hosts` strings that will be used as a target
and source arguments in the state function `file.copy`.
Here is an example of result parsing and appending:
.. code-block:: yaml
file-in-user-home:
file.copy:
- name: __slot__:salt:user.info(someuser).home ~ /subdirectory
- source: salt://somefile

View File

@ -2054,24 +2054,96 @@ class State(object):
'test.arg(\'arg\', kw=\'kwarg\')')
return slot
log.debug('Calling slot: %s(%s, %s)', fun, args, kwargs)
return self.functions[fun](*args, **kwargs)
slot_return = self.functions[fun](*args, **kwargs)
# Given input __slot__:salt:test.arg(somekey="value").not.exist ~ /appended
# slot_text should be __slot...).not.exist
# append_data should be ~ /appended
slot_text = fmt[2].split('~')[0]
append_data = fmt[2].split('~', 1)[1:]
log.debug('slot_text: %s', slot_text)
log.debug('append_data: %s', append_data)
# Support parsing slot dict response
# return_get should result in a kwargs.nested.dict path by getting
# everything after first closing paren: )
return_get = None
try:
return_get = slot_text[slot_text.rindex(')')+1:]
except ValueError:
pass
if return_get:
#remove first period
return_get = return_get.split('.', 1)[1].strip()
log.debug('Searching slot result %s for %s', slot_return, return_get)
slot_return = salt.utils.data.traverse_dict_and_list(slot_return,
return_get,
default=None,
delimiter='.'
)
if append_data:
if isinstance(slot_return, six.string_types):
# Append text to slot string result
append_data = ' '.join(append_data).strip()
log.debug('appending to slot result: %s', append_data)
slot_return += append_data
else:
log.error('Ignoring slot append, slot result is not a string')
return slot_return
def format_slots(self, cdata):
'''
Read in the arguments from the low level slot syntax to make a last
minute runtime call to gather relevant data for the specific routine
Will parse strings, first level of dictionary values, and strings and
first level dict values inside of lists
'''
# __slot__:salt.cmd.run(foo, bar, baz=qux)
SLOT_TEXT = '__slot__:'
ctx = (('args', enumerate(cdata['args'])),
('kwargs', cdata['kwargs'].items()))
for atype, avalues in ctx:
for ind, arg in avalues:
arg = salt.utils.data.decode(arg, keep=True)
if not isinstance(arg, six.text_type) \
or not arg.startswith('__slot__:'):
if isinstance(arg, dict):
# Search dictionary values for __slot__:
for key, value in arg.items():
try:
if value.startswith(SLOT_TEXT):
log.trace("Slot processsing dict value %s", value)
cdata[atype][ind][key] = self.__eval_slot(value)
except AttributeError:
# Not a string/slot
continue
elif isinstance(arg, list):
for idx, listvalue in enumerate(arg):
log.trace("Slot processing list value: %s", listvalue)
if isinstance(listvalue, dict):
# Search dict values in list for __slot__:
for key, value in listvalue.items():
try:
if value.startswith(SLOT_TEXT):
log.trace("Slot processsing nested dict value %s", value)
cdata[atype][ind][idx][key] = self.__eval_slot(value)
except AttributeError:
# Not a string/slot
continue
if isinstance(listvalue, six.text_type):
# Search strings in a list for __slot__:
if listvalue.startswith(SLOT_TEXT):
log.trace("Slot processsing nested string %s", listvalue)
cdata[atype][ind][idx] = self.__eval_slot(listvalue)
elif isinstance(arg, six.text_type) \
and arg.startswith(SLOT_TEXT):
# Search strings for __slot__:
log.trace("Slot processsing %s", arg)
cdata[atype][ind] = self.__eval_slot(arg)
else:
# Not a slot, skip it
continue
cdata[atype][ind] = self.__eval_slot(arg)
def verify_retry_data(self, retry_data):
'''

View File

@ -281,6 +281,60 @@ class StateFormatSlotsTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
mock.assert_called_once_with('fun_arg', fun_key='fun_val')
self.assertEqual(cdata, {'args': ['fun_return'], 'kwargs': {'key': 'val'}})
def test_format_slots_dict_arg(self):
'''
Test the format slots is calling a slot specified in dict arg.
'''
cdata = {
'args': [
{'subarg': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)'},
],
'kwargs': {
'key': 'val',
}
}
mock = MagicMock(return_value='fun_return')
with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
self.state_obj.format_slots(cdata)
mock.assert_called_once_with('fun_arg', fun_key='fun_val')
self.assertEqual(cdata, {'args': [{'subarg': 'fun_return'}], 'kwargs': {'key': 'val'}})
def test_format_slots_listdict_arg(self):
'''
Test the format slots is calling a slot specified in list containing a dict.
'''
cdata = {
'args': [[
{'subarg': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)'},
]],
'kwargs': {
'key': 'val',
}
}
mock = MagicMock(return_value='fun_return')
with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
self.state_obj.format_slots(cdata)
mock.assert_called_once_with('fun_arg', fun_key='fun_val')
self.assertEqual(cdata, {'args': [[{'subarg': 'fun_return'}]], 'kwargs': {'key': 'val'}})
def test_format_slots_liststr_arg(self):
'''
Test the format slots is calling a slot specified in list containing a dict.
'''
cdata = {
'args': [[
'__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)',
]],
'kwargs': {
'key': 'val',
}
}
mock = MagicMock(return_value='fun_return')
with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
self.state_obj.format_slots(cdata)
mock.assert_called_once_with('fun_arg', fun_key='fun_val')
self.assertEqual(cdata, {'args': [['fun_return']], 'kwargs': {'key': 'val'}})
def test_format_slots_kwarg(self):
'''
Test the format slots is calling a slot specified in kwargs with corresponding arguments.
@ -360,3 +414,41 @@ class StateFormatSlotsTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
self.state_obj.format_slots(cdata)
mock.assert_not_called()
self.assertEqual(cdata, sls_data)
def test_slot_traverse_dict(self):
'''
Test the slot parsing of dict response.
'''
cdata = {
'args': [
'arg',
],
'kwargs': {
'key': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val).key1',
}
}
return_data = {'key1': 'value1'}
mock = MagicMock(return_value=return_data)
with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
self.state_obj.format_slots(cdata)
mock.assert_called_once_with('fun_arg', fun_key='fun_val')
self.assertEqual(cdata, {'args': ['arg'], 'kwargs': {'key': 'value1'}})
def test_slot_append(self):
'''
Test the slot parsing of dict response.
'''
cdata = {
'args': [
'arg',
],
'kwargs': {
'key': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val).key1 ~ thing~',
}
}
return_data = {'key1': 'value1'}
mock = MagicMock(return_value=return_data)
with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
self.state_obj.format_slots(cdata)
mock.assert_called_once_with('fun_arg', fun_key='fun_val')
self.assertEqual(cdata, {'args': ['arg'], 'kwargs': {'key': 'value1thing~'}})