diff --git a/doc/topics/releases/neon.rst b/doc/topics/releases/neon.rst index 780a8d9e42..75e91ef6e0 100644 --- a/doc/topics/releases/neon.rst +++ b/doc/topics/releases/neon.rst @@ -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: diff --git a/doc/topics/slots/index.rst b/doc/topics/slots/index.rst index 30f796ea11..3b71f04c1b 100644 --- a/doc/topics/slots/index.rst +++ b/doc/topics/slots/index.rst @@ -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:.(, ..., , ...) -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:.(..., , ...).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 ` 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 diff --git a/salt/state.py b/salt/state.py index d063d71db5..751fad9a56 100644 --- a/salt/state.py +++ b/salt/state.py @@ -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): ''' diff --git a/tests/unit/test_state.py b/tests/unit/test_state.py index d6555a03bb..462539b68a 100644 --- a/tests/unit/test_state.py +++ b/tests/unit/test_state.py @@ -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~'}})