Allow nested action value formatting
Modify introspection rules to allow formatting to be applied to strings nested in dicts and lists in the actions. Change-Id: Ia53e0de98438f7789e9b9136dcd85c1b1274b713 Story: #1670768 Task: #11362
This commit is contained in:
parent
b071f9802e
commit
0646970f58
@ -124,12 +124,20 @@ Default available actions include:
|
|||||||
set to ``True``, nothing will be added if given value is already in a list.
|
set to ``True``, nothing will be added if given value is already in a list.
|
||||||
|
|
||||||
Starting from Mitaka release, ``value`` field in actions supports fetching data
|
Starting from Mitaka release, ``value`` field in actions supports fetching data
|
||||||
from introspection, it's using `python string formatting notation
|
from introspection, using `python string formatting notation
|
||||||
<https://docs.python.org/2/library/string.html#formatspec>`_ ::
|
<https://docs.python.org/2/library/string.html#formatspec>`_::
|
||||||
|
|
||||||
{"action": "set-attribute", "path": "/driver_info/ipmi_address",
|
{"action": "set-attribute", "path": "/driver_info/ipmi_address",
|
||||||
"value": "{data[inventory][bmc_address]}"}
|
"value": "{data[inventory][bmc_address]}"}
|
||||||
|
|
||||||
|
Note that any value referenced in this way will be converted to a string.
|
||||||
|
|
||||||
|
If ``value`` is a dict or list, strings nested at any level within the
|
||||||
|
structure will be formatted as well::
|
||||||
|
|
||||||
|
{"action": "set-attribute", "path": "/properties/root_device",
|
||||||
|
"value": {"serial": "{data[root_device][serial]}"}}
|
||||||
|
|
||||||
Plugins
|
Plugins
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ app = flask.Flask(__name__)
|
|||||||
LOG = utils.getProcessingLogger(__name__)
|
LOG = utils.getProcessingLogger(__name__)
|
||||||
|
|
||||||
MINIMUM_API_VERSION = (1, 0)
|
MINIMUM_API_VERSION = (1, 0)
|
||||||
CURRENT_API_VERSION = (1, 13)
|
CURRENT_API_VERSION = (1, 14)
|
||||||
DEFAULT_API_VERSION = CURRENT_API_VERSION
|
DEFAULT_API_VERSION = CURRENT_API_VERSION
|
||||||
_LOGGING_EXCLUDED_KEYS = ('logs',)
|
_LOGGING_EXCLUDED_KEYS = ('logs',)
|
||||||
|
|
||||||
|
@ -196,19 +196,13 @@ class IntrospectionRule(object):
|
|||||||
ext = ext_mgr[act.action].obj
|
ext = ext_mgr[act.action].obj
|
||||||
|
|
||||||
for formatted_param in ext.FORMATTED_PARAMS:
|
for formatted_param in ext.FORMATTED_PARAMS:
|
||||||
value = act.params.get(formatted_param)
|
|
||||||
if not value or not isinstance(value, six.string_types):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# NOTE(aarefiev): verify provided value with introspection
|
|
||||||
# data format specifications.
|
|
||||||
# TODO(aarefiev): simple verify on import rule time.
|
|
||||||
try:
|
try:
|
||||||
act.params[formatted_param] = value.format(data=data)
|
initial = act.params[formatted_param]
|
||||||
except KeyError as e:
|
except KeyError:
|
||||||
raise utils.Error(_('Invalid formatting variable key '
|
# Ignore parameter that wasn't given.
|
||||||
'provided: %s') % e,
|
continue
|
||||||
node_info=node_info, data=data)
|
else:
|
||||||
|
act.params[formatted_param] = _format_value(initial, data)
|
||||||
|
|
||||||
LOG.debug('Running action `%(action)s %(params)s`',
|
LOG.debug('Running action `%(action)s %(params)s`',
|
||||||
{'action': act.action, 'params': act.params},
|
{'action': act.action, 'params': act.params},
|
||||||
@ -219,6 +213,38 @@ class IntrospectionRule(object):
|
|||||||
node_info=node_info, data=data)
|
node_info=node_info, data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_value(value, data):
|
||||||
|
"""Apply parameter formatting to a value.
|
||||||
|
|
||||||
|
Format strings with the values from `data`. If `value` is a dict or
|
||||||
|
list, any string members (and any nested string members) will also be
|
||||||
|
formatted recursively. This includes both keys and values for dicts.
|
||||||
|
|
||||||
|
:param value: The string to format, or container whose members to
|
||||||
|
format.
|
||||||
|
:param data: Introspection data.
|
||||||
|
:returns: `value`, formatted with the parameters from `data`.
|
||||||
|
"""
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
# NOTE(aarefiev): verify provided value with introspection
|
||||||
|
# data format specifications.
|
||||||
|
# TODO(aarefiev): simple verify on import rule time.
|
||||||
|
try:
|
||||||
|
return value.format(data=data)
|
||||||
|
except KeyError as e:
|
||||||
|
raise utils.Error(_('Invalid formatting variable key '
|
||||||
|
'provided in value %(val)s: %(e)s'),
|
||||||
|
{'val': value, 'e': e}, data=data)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
return {_format_value(k, data): _format_value(v, data)
|
||||||
|
for k, v in six.iteritems(value)}
|
||||||
|
elif isinstance(value, list):
|
||||||
|
return [_format_value(v, data) for v in value]
|
||||||
|
else:
|
||||||
|
# Assume this is a 'primitive' value.
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _parse_path(path):
|
def _parse_path(path):
|
||||||
"""Parse path, extract scheme and path.
|
"""Parse path, extract scheme and path.
|
||||||
|
|
||||||
|
@ -421,6 +421,64 @@ class TestApplyActions(BaseTest):
|
|||||||
|
|
||||||
self.assertEqual(1, self.act_mock.apply.call_count)
|
self.assertEqual(1, self.act_mock.apply.call_count)
|
||||||
|
|
||||||
|
def test_apply_data_format_value_dict(self, mock_ext_mgr):
|
||||||
|
self.data.update({'val_outer': {'val_inner': 17},
|
||||||
|
'key_outer': {'key_inner': 'baz'}})
|
||||||
|
|
||||||
|
self.rule = rules.create(actions_json=[
|
||||||
|
{'action': 'set-attribute',
|
||||||
|
'path': '/driver_info/foo',
|
||||||
|
'value': {'{data[key_outer][key_inner]}':
|
||||||
|
'{data[val_outer][val_inner]}'}}],
|
||||||
|
conditions_json=self.conditions_json
|
||||||
|
)
|
||||||
|
mock_ext_mgr.return_value.__getitem__.return_value = self.ext_mock
|
||||||
|
|
||||||
|
self.rule.apply_actions(self.node_info, data=self.data)
|
||||||
|
|
||||||
|
self.act_mock.apply.assert_called_once_with(self.node_info, {
|
||||||
|
# String-formatted values will be coerced to be strings.
|
||||||
|
'value': {'baz': '17'},
|
||||||
|
'path': '/driver_info/foo'
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_apply_data_format_value_list(self, mock_ext_mgr):
|
||||||
|
self.data.update({'outer': {'inner': 'baz'}})
|
||||||
|
|
||||||
|
self.rule = rules.create(actions_json=[
|
||||||
|
{'action': 'set-attribute',
|
||||||
|
'path': '/driver_info/foo',
|
||||||
|
'value': ['basic', ['{data[outer][inner]}']]}],
|
||||||
|
conditions_json=self.conditions_json
|
||||||
|
)
|
||||||
|
mock_ext_mgr.return_value.__getitem__.return_value = self.ext_mock
|
||||||
|
|
||||||
|
self.rule.apply_actions(self.node_info, data=self.data)
|
||||||
|
|
||||||
|
self.act_mock.apply.assert_called_once_with(self.node_info, {
|
||||||
|
'value': ['basic', ['baz']],
|
||||||
|
'path': '/driver_info/foo'
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_apply_data_format_value_primitives(self, mock_ext_mgr):
|
||||||
|
self.data.update({'outer': {'inner': False}})
|
||||||
|
|
||||||
|
self.rule = rules.create(actions_json=[
|
||||||
|
{'action': 'set-attribute',
|
||||||
|
'path': '/driver_info/foo',
|
||||||
|
'value': {42: {True: [3.14, 'foo', '{data[outer][inner]}']}}}],
|
||||||
|
conditions_json=self.conditions_json
|
||||||
|
)
|
||||||
|
mock_ext_mgr.return_value.__getitem__.return_value = self.ext_mock
|
||||||
|
|
||||||
|
self.rule.apply_actions(self.node_info, data=self.data)
|
||||||
|
|
||||||
|
self.act_mock.apply.assert_called_once_with(self.node_info, {
|
||||||
|
# String-formatted values will be coerced to be strings.
|
||||||
|
'value': {42: {True: [3.14, 'foo', 'False']}},
|
||||||
|
'path': '/driver_info/foo'
|
||||||
|
})
|
||||||
|
|
||||||
def test_apply_data_format_value_fail(self, mock_ext_mgr):
|
def test_apply_data_format_value_fail(self, mock_ext_mgr):
|
||||||
self.rule = rules.create(
|
self.rule = rules.create(
|
||||||
actions_json=[
|
actions_json=[
|
||||||
@ -434,6 +492,19 @@ class TestApplyActions(BaseTest):
|
|||||||
self.assertRaises(utils.Error, self.rule.apply_actions,
|
self.assertRaises(utils.Error, self.rule.apply_actions,
|
||||||
self.node_info, data=self.data)
|
self.node_info, data=self.data)
|
||||||
|
|
||||||
|
def test_apply_data_format_value_nested_fail(self, mock_ext_mgr):
|
||||||
|
self.data.update({'outer': {'inner': 'baz'}})
|
||||||
|
self.rule = rules.create(actions_json=[
|
||||||
|
{'action': 'set-attribute',
|
||||||
|
'path': '/driver_info/foo',
|
||||||
|
'value': ['basic', ['{data[outer][nonexistent]}']]}],
|
||||||
|
conditions_json=self.conditions_json
|
||||||
|
)
|
||||||
|
mock_ext_mgr.return_value.__getitem__.return_value = self.ext_mock
|
||||||
|
|
||||||
|
self.assertRaises(utils.Error, self.rule.apply_actions,
|
||||||
|
self.node_info, data=self.data)
|
||||||
|
|
||||||
def test_apply_data_non_format_value(self, mock_ext_mgr):
|
def test_apply_data_non_format_value(self, mock_ext_mgr):
|
||||||
self.rule = rules.create(actions_json=[
|
self.rule = rules.create(actions_json=[
|
||||||
{'action': 'set-attribute',
|
{'action': 'set-attribute',
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Modifies introspection rules to allow formatting to be applied to strings
|
||||||
|
nested in dicts and lists in the actions.
|
Loading…
Reference in New Issue
Block a user