Extend conditions and actions
Conditions: ``field`` supports new format, it allow to comparison data both from inspection and node info: {'field': 'node://ironic/style/path', 'op': 'eq', 'value': 'val'} {'field': 'data://introspection/path', 'op': 'eq', 'value': 'val'} Actions: ``value`` supports fetching data from introspection, it's using python string formatting notation: {'action': 'set-attribute', 'path': '/driver_info/ipmi_address', 'value': '{data[inventory][bmc_address]}'} Related-Bug: #1524753 Change-Id: Ie05f82e7a29fba2f743217f0893c085fd843cd5b
This commit is contained in:
parent
61d0b85ed6
commit
bf86545ca4
@ -74,6 +74,16 @@ A condition is represented by an object with fields:
|
|||||||
``field`` a `JSON path <http://goessner.net/articles/JsonPath/>`_ to the field
|
``field`` a `JSON path <http://goessner.net/articles/JsonPath/>`_ to the field
|
||||||
in the introspection data to use in comparison.
|
in the introspection data to use in comparison.
|
||||||
|
|
||||||
|
Starting with the Mitaka release, you can also apply conditions to ironic node
|
||||||
|
field. Prefix field with schema (``data://`` or ``node://``) to distinguish
|
||||||
|
between values from introspection data and node. Both schemes use JSON path::
|
||||||
|
|
||||||
|
{'field': 'node://property.path', 'op': 'eq', 'value': 'val'}
|
||||||
|
{'field': 'data://introspection.path', 'op': 'eq', 'value': 'val'}
|
||||||
|
|
||||||
|
if scheme (node or data) is missing, condition compares data with
|
||||||
|
introspection data.
|
||||||
|
|
||||||
``multiple`` how to treat situations where the ``field`` query returns multiple
|
``multiple`` how to treat situations where the ``field`` query returns multiple
|
||||||
results (e.g. the field contains a list), available options are:
|
results (e.g. the field contains a list), available options are:
|
||||||
|
|
||||||
@ -110,6 +120,13 @@ Default available actions include:
|
|||||||
value as a list and appends value to it. If optional ``unique`` parameter is
|
value as a list and appends value to it. If optional ``unique`` parameter is
|
||||||
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
|
||||||
|
from introspection, it's using `python string formatting notation
|
||||||
|
<https://docs.python.org/2/library/string.html#formatspec>`_ ::
|
||||||
|
|
||||||
|
{'action': 'set-attribute', 'path': '/driver_info/ipmi_address',
|
||||||
|
'value': '{data[inventory][bmc_address]}'}
|
||||||
|
|
||||||
.. _setting-ipmi-creds:
|
.. _setting-ipmi-creds:
|
||||||
|
|
||||||
Setting IPMI Credentials
|
Setting IPMI Credentials
|
||||||
|
@ -116,6 +116,9 @@ class RuleConditionPlugin(WithValidation): # pragma: no cover
|
|||||||
class RuleActionPlugin(WithValidation): # pragma: no cover
|
class RuleActionPlugin(WithValidation): # pragma: no cover
|
||||||
"""Abstract base class for rule action plugins."""
|
"""Abstract base class for rule action plugins."""
|
||||||
|
|
||||||
|
FORMATTED_PARAMS = []
|
||||||
|
"""List of params will be formatted with python format."""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def apply(self, node_info, params, **kwargs):
|
def apply(self, node_info, params, **kwargs):
|
||||||
"""Run action on successful rule match.
|
"""Run action on successful rule match.
|
||||||
|
@ -113,6 +113,8 @@ class SetAttributeAction(base.RuleActionPlugin):
|
|||||||
REQUIRED_PARAMS = {'path', 'value'}
|
REQUIRED_PARAMS = {'path', 'value'}
|
||||||
# TODO(dtantsur): proper validation of path
|
# TODO(dtantsur): proper validation of path
|
||||||
|
|
||||||
|
FORMATTED_PARAMS = ['value']
|
||||||
|
|
||||||
def apply(self, node_info, params, **kwargs):
|
def apply(self, node_info, params, **kwargs):
|
||||||
node_info.patch([{'op': 'add', 'path': params['path'],
|
node_info.patch([{'op': 'add', 'path': params['path'],
|
||||||
'value': params['value']}])
|
'value': params['value']}])
|
||||||
@ -132,6 +134,8 @@ class SetCapabilityAction(base.RuleActionPlugin):
|
|||||||
REQUIRED_PARAMS = {'name'}
|
REQUIRED_PARAMS = {'name'}
|
||||||
OPTIONAL_PARAMS = {'value'}
|
OPTIONAL_PARAMS = {'value'}
|
||||||
|
|
||||||
|
FORMATTED_PARAMS = ['value']
|
||||||
|
|
||||||
def apply(self, node_info, params, **kwargs):
|
def apply(self, node_info, params, **kwargs):
|
||||||
node_info.update_capabilities(
|
node_info.update_capabilities(
|
||||||
**{params['name']: params.get('value')})
|
**{params['name']: params.get('value')})
|
||||||
@ -145,6 +149,8 @@ class ExtendAttributeAction(base.RuleActionPlugin):
|
|||||||
OPTIONAL_PARAMS = {'unique'}
|
OPTIONAL_PARAMS = {'unique'}
|
||||||
# TODO(dtantsur): proper validation of path
|
# TODO(dtantsur): proper validation of path
|
||||||
|
|
||||||
|
FORMATTED_PARAMS = ['value']
|
||||||
|
|
||||||
def apply(self, node_info, params, **kwargs):
|
def apply(self, node_info, params, **kwargs):
|
||||||
def _replace(values):
|
def _replace(values):
|
||||||
value = params['value']
|
value = params['value']
|
||||||
|
@ -131,7 +131,14 @@ class IntrospectionRule(object):
|
|||||||
node_info=node_info, data=data)
|
node_info=node_info, data=data)
|
||||||
ext_mgr = plugins_base.rule_conditions_manager()
|
ext_mgr = plugins_base.rule_conditions_manager()
|
||||||
for cond in self._conditions:
|
for cond in self._conditions:
|
||||||
field_values = jsonpath.parse(cond.field).find(data)
|
scheme, path = _parse_path(cond.field)
|
||||||
|
|
||||||
|
if scheme == 'node':
|
||||||
|
source_data = node_info.node().dict()
|
||||||
|
elif scheme == 'data':
|
||||||
|
source_data = data
|
||||||
|
|
||||||
|
field_values = jsonpath.parse(path).find(source_data)
|
||||||
field_values = [x.value for x in field_values]
|
field_values = [x.value for x in field_values]
|
||||||
cond_ext = ext_mgr[cond.op].obj
|
cond_ext = ext_mgr[cond.op].obj
|
||||||
|
|
||||||
@ -172,7 +179,7 @@ class IntrospectionRule(object):
|
|||||||
|
|
||||||
:param node_info: NodeInfo instance
|
:param node_info: NodeInfo instance
|
||||||
:param rollback: if True, rollback actions are executed
|
:param rollback: if True, rollback actions are executed
|
||||||
:param data: introspection data (only used for logging)
|
:param data: introspection data
|
||||||
"""
|
"""
|
||||||
if rollback:
|
if rollback:
|
||||||
method = 'rollback'
|
method = 'rollback'
|
||||||
@ -185,11 +192,26 @@ class IntrospectionRule(object):
|
|||||||
|
|
||||||
ext_mgr = plugins_base.rule_actions_manager()
|
ext_mgr = plugins_base.rule_actions_manager()
|
||||||
for act in self._actions:
|
for act in self._actions:
|
||||||
|
ext = ext_mgr[act.action].obj
|
||||||
|
for formatted_param in ext.FORMATTED_PARAMS:
|
||||||
|
value = act.params.get(formatted_param)
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# NOTE(aarefiev): verify provided value with introspection
|
||||||
|
# data format specifications.
|
||||||
|
# TODO(aarefiev): simple verify on import rule time.
|
||||||
|
try:
|
||||||
|
act.params[formatted_param] = value.format(data=data)
|
||||||
|
except KeyError as e:
|
||||||
|
raise utils.Error(_('Invalid formatting variable key '
|
||||||
|
'provided: %s') % e,
|
||||||
|
node_info=node_info, data=data)
|
||||||
|
|
||||||
LOG.debug('Running %(what)s action `%(action)s %(params)s`',
|
LOG.debug('Running %(what)s action `%(action)s %(params)s`',
|
||||||
{'action': act.action, 'params': act.params,
|
{'action': act.action, 'params': act.params,
|
||||||
'what': method},
|
'what': method},
|
||||||
node_info=node_info, data=data)
|
node_info=node_info, data=data)
|
||||||
ext = ext_mgr[act.action].obj
|
|
||||||
getattr(ext, method)(node_info, act.params)
|
getattr(ext, method)(node_info, act.params)
|
||||||
|
|
||||||
LOG.debug('Successfully applied %s',
|
LOG.debug('Successfully applied %s',
|
||||||
@ -197,6 +219,27 @@ class IntrospectionRule(object):
|
|||||||
node_info=node_info, data=data)
|
node_info=node_info, data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_path(path):
|
||||||
|
"""Parse path, extract scheme and path.
|
||||||
|
|
||||||
|
Parse path with 'node' and 'data' scheme, which links on
|
||||||
|
introspection data and node info respectively. If scheme is
|
||||||
|
missing in path, default is 'data'.
|
||||||
|
|
||||||
|
:param path: data or node path
|
||||||
|
:return: tuple (scheme, path)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
index = path.index('://')
|
||||||
|
except ValueError:
|
||||||
|
scheme = 'data'
|
||||||
|
path = path
|
||||||
|
else:
|
||||||
|
scheme = path[:index]
|
||||||
|
path = path[index + 3:]
|
||||||
|
return scheme, path
|
||||||
|
|
||||||
|
|
||||||
def create(conditions_json, actions_json, uuid=None,
|
def create(conditions_json, actions_json, uuid=None,
|
||||||
description=None):
|
description=None):
|
||||||
"""Create a new rule in database.
|
"""Create a new rule in database.
|
||||||
@ -235,8 +278,15 @@ def create(conditions_json, actions_json, uuid=None,
|
|||||||
conditions = []
|
conditions = []
|
||||||
for cond_json in conditions_json:
|
for cond_json in conditions_json:
|
||||||
field = cond_json['field']
|
field = cond_json['field']
|
||||||
|
|
||||||
|
scheme, path = _parse_path(field)
|
||||||
|
|
||||||
|
if scheme not in ('node', 'data'):
|
||||||
|
raise utils.Error(_('Unsupported scheme for field: %s, valid '
|
||||||
|
'values are node:// or data://') % scheme)
|
||||||
|
# verify field as JSON path
|
||||||
try:
|
try:
|
||||||
jsonpath.parse(field)
|
jsonpath.parse(path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise utils.Error(_('Unable to parse field JSON path %(field)s: '
|
raise utils.Error(_('Unable to parse field JSON path %(field)s: '
|
||||||
'%(error)s') % {'field': field, 'error': exc})
|
'%(error)s') % {'field': field, 'error': exc})
|
||||||
@ -364,7 +414,7 @@ def apply(node_info, data):
|
|||||||
if to_rollback:
|
if to_rollback:
|
||||||
LOG.debug('Running rollback actions', node_info=node_info, data=data)
|
LOG.debug('Running rollback actions', node_info=node_info, data=data)
|
||||||
for rule in to_rollback:
|
for rule in to_rollback:
|
||||||
rule.apply_actions(node_info, rollback=True)
|
rule.apply_actions(node_info, rollback=True, data=data)
|
||||||
else:
|
else:
|
||||||
LOG.debug('No rollback actions to apply',
|
LOG.debug('No rollback actions to apply',
|
||||||
node_info=node_info, data=data)
|
node_info=node_info, data=data)
|
||||||
@ -372,7 +422,7 @@ def apply(node_info, data):
|
|||||||
if to_apply:
|
if to_apply:
|
||||||
LOG.debug('Running actions', node_info=node_info, data=data)
|
LOG.debug('Running actions', node_info=node_info, data=data)
|
||||||
for rule in to_apply:
|
for rule in to_apply:
|
||||||
rule.apply_actions(node_info, rollback=False)
|
rule.apply_actions(node_info, rollback=False, data=data)
|
||||||
else:
|
else:
|
||||||
LOG.debug('No actions to apply', node_info=node_info, data=data)
|
LOG.debug('No actions to apply', node_info=node_info, data=data)
|
||||||
|
|
||||||
|
@ -88,15 +88,23 @@ class NodeTest(BaseTest):
|
|||||||
self.uuid = uuidutils.generate_uuid()
|
self.uuid = uuidutils.generate_uuid()
|
||||||
self.bmc_address = '1.2.3.4'
|
self.bmc_address = '1.2.3.4'
|
||||||
self.macs = ['11:22:33:44:55:66', '66:55:44:33:22:11']
|
self.macs = ['11:22:33:44:55:66', '66:55:44:33:22:11']
|
||||||
self.node = mock.Mock(driver='pxe_ipmitool',
|
fake_node = {
|
||||||
driver_info={'ipmi_address': self.bmc_address},
|
'driver': 'pxe_ipmitool',
|
||||||
properties={'cpu_arch': 'i386', 'local_gb': 40},
|
'driver_info': {'ipmi_address': self.bmc_address},
|
||||||
uuid=self.uuid,
|
'properties': {'cpu_arch': 'i386', 'local_gb': 40},
|
||||||
power_state='power on',
|
'uuid': self.uuid,
|
||||||
provision_state='inspecting',
|
'power_state': 'power on',
|
||||||
extra={},
|
'provision_state': 'inspecting',
|
||||||
instance_uuid=None,
|
'extra': {},
|
||||||
maintenance=False)
|
'instance_uuid': None,
|
||||||
|
'maintenance': False
|
||||||
|
}
|
||||||
|
mock_to_dict = mock.Mock(return_value=fake_node)
|
||||||
|
|
||||||
|
self.node = mock.Mock(**fake_node)
|
||||||
|
self.node.dict = mock_to_dict
|
||||||
|
|
||||||
self.ports = []
|
self.ports = []
|
||||||
self.node_info = node_cache.NodeInfo(uuid=self.uuid, started_at=0,
|
self.node_info = node_cache.NodeInfo(uuid=self.uuid, started_at=0,
|
||||||
node=self.node, ports=self.ports)
|
node=self.node, ports=self.ports)
|
||||||
|
self.node_info.node = mock.Mock(return_value=self.node)
|
||||||
|
@ -342,6 +342,49 @@ class Test(Base):
|
|||||||
self.uuid,
|
self.uuid,
|
||||||
[{'op': 'add', 'path': '/extra/foo', 'value': 'bar'}])
|
[{'op': 'add', 'path': '/extra/foo', 'value': 'bar'}])
|
||||||
|
|
||||||
|
def test_conditions_scheme_actions_path(self):
|
||||||
|
rules = [
|
||||||
|
{
|
||||||
|
'conditions': [
|
||||||
|
{'field': 'node://properties.local_gb', 'op': 'eq',
|
||||||
|
'value': 40},
|
||||||
|
{'field': 'node://driver_info.ipmi_address', 'op': 'eq',
|
||||||
|
'value': self.bmc_address},
|
||||||
|
],
|
||||||
|
'actions': [
|
||||||
|
{'action': 'set-attribute', 'path': '/extra/foo',
|
||||||
|
'value': 'bar'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'conditions': [
|
||||||
|
{'field': 'data://inventory.cpu.count', 'op': 'eq',
|
||||||
|
'value': self.data['inventory']['cpu']['count']},
|
||||||
|
],
|
||||||
|
'actions': [
|
||||||
|
{'action': 'set-attribute',
|
||||||
|
'path': '/driver_info/ipmi_address',
|
||||||
|
'value': '{data[inventory][bmc_address]}'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for rule in rules:
|
||||||
|
self.call_add_rule(rule)
|
||||||
|
|
||||||
|
self.call_introspect(self.uuid)
|
||||||
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||||
|
self.call_continue(self.data)
|
||||||
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||||
|
|
||||||
|
self.cli.node.update.assert_any_call(
|
||||||
|
self.uuid,
|
||||||
|
[{'op': 'add', 'path': '/extra/foo', 'value': 'bar'}])
|
||||||
|
|
||||||
|
self.cli.node.update.assert_any_call(
|
||||||
|
self.uuid,
|
||||||
|
[{'op': 'add', 'path': '/driver_info/ipmi_address',
|
||||||
|
'value': self.data['inventory']['bmc_address']}])
|
||||||
|
|
||||||
def test_root_device_hints(self):
|
def test_root_device_hints(self):
|
||||||
self.node.properties['root_device'] = {'size': 20}
|
self.node.properties['root_device'] = {'size': 20}
|
||||||
|
|
||||||
|
@ -18,14 +18,13 @@ import mock
|
|||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from ironic_inspector import db
|
from ironic_inspector import db
|
||||||
from ironic_inspector import node_cache
|
|
||||||
from ironic_inspector.plugins import base as plugins_base
|
from ironic_inspector.plugins import base as plugins_base
|
||||||
from ironic_inspector import rules
|
from ironic_inspector import rules
|
||||||
from ironic_inspector.test import base as test_base
|
from ironic_inspector.test import base as test_base
|
||||||
from ironic_inspector import utils
|
from ironic_inspector import utils
|
||||||
|
|
||||||
|
|
||||||
class BaseTest(test_base.BaseTest):
|
class BaseTest(test_base.NodeTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(BaseTest, self).setUp()
|
super(BaseTest, self).setUp()
|
||||||
self.uuid = uuidutils.generate_uuid()
|
self.uuid = uuidutils.generate_uuid()
|
||||||
@ -41,7 +40,6 @@ class BaseTest(test_base.BaseTest):
|
|||||||
'memory_mb': 1024,
|
'memory_mb': 1024,
|
||||||
'local_gb': 42,
|
'local_gb': 42,
|
||||||
}
|
}
|
||||||
self.node_info = node_cache.NodeInfo(uuid=self.uuid, started_at=42)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCreateRule(BaseTest):
|
class TestCreateRule(BaseTest):
|
||||||
@ -321,6 +319,38 @@ class TestCheckConditionsMultiple(BaseTest):
|
|||||||
data)
|
data)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckConditionsSchemePath(BaseTest):
|
||||||
|
def test_conditions_data_path(self):
|
||||||
|
self.data_set = [
|
||||||
|
([{'op': 'eq', 'field': 'data://memory_mb', 'value': 1024}],
|
||||||
|
True),
|
||||||
|
([{'op': 'gt', 'field': 'data://local_gb', 'value': 42}],
|
||||||
|
False)
|
||||||
|
]
|
||||||
|
|
||||||
|
for condition, res in self.data_set:
|
||||||
|
rule = rules.create(conditions_json=condition,
|
||||||
|
actions_json=self.actions_json)
|
||||||
|
self.assertIs(res,
|
||||||
|
rule.check_conditions(self.node_info, self.data),
|
||||||
|
self.data)
|
||||||
|
|
||||||
|
def test_conditions_node_path(self):
|
||||||
|
self.node_set = [
|
||||||
|
([{'op': 'eq', 'field': 'node://driver_info.ipmi_address',
|
||||||
|
'value': self.bmc_address}],
|
||||||
|
True),
|
||||||
|
([{'op': 'eq', 'field': 'node://driver', 'value': 'fake'}],
|
||||||
|
False)
|
||||||
|
]
|
||||||
|
|
||||||
|
for condition, res in self.node_set:
|
||||||
|
rule = rules.create(conditions_json=condition,
|
||||||
|
actions_json=self.actions_json)
|
||||||
|
self.assertIs(res,
|
||||||
|
rule.check_conditions(self.node_info, self.data))
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(plugins_base, 'rule_actions_manager', autospec=True)
|
@mock.patch.object(plugins_base, 'rule_actions_manager', autospec=True)
|
||||||
class TestApplyActions(BaseTest):
|
class TestApplyActions(BaseTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -330,12 +360,13 @@ class TestApplyActions(BaseTest):
|
|||||||
self.rule = rules.create(conditions_json=self.conditions_json,
|
self.rule = rules.create(conditions_json=self.conditions_json,
|
||||||
actions_json=self.actions_json)
|
actions_json=self.actions_json)
|
||||||
self.act_mock = mock.Mock(spec=plugins_base.RuleActionPlugin)
|
self.act_mock = mock.Mock(spec=plugins_base.RuleActionPlugin)
|
||||||
|
self.act_mock.FORMATTED_PARAMS = ['value']
|
||||||
self.ext_mock = mock.Mock(spec=['obj'], obj=self.act_mock)
|
self.ext_mock = mock.Mock(spec=['obj'], obj=self.act_mock)
|
||||||
|
|
||||||
def test_apply(self, mock_ext_mgr):
|
def test_apply(self, mock_ext_mgr):
|
||||||
mock_ext_mgr.return_value.__getitem__.return_value = self.ext_mock
|
mock_ext_mgr.return_value.__getitem__.return_value = self.ext_mock
|
||||||
|
|
||||||
self.rule.apply_actions(self.node_info)
|
self.rule.apply_actions(self.node_info, data=self.data)
|
||||||
|
|
||||||
self.act_mock.apply.assert_any_call(self.node_info,
|
self.act_mock.apply.assert_any_call(self.node_info,
|
||||||
{'message': 'boom!'})
|
{'message': 'boom!'})
|
||||||
@ -344,6 +375,33 @@ class TestApplyActions(BaseTest):
|
|||||||
self.act_mock.apply.call_count)
|
self.act_mock.apply.call_count)
|
||||||
self.assertFalse(self.act_mock.rollback.called)
|
self.assertFalse(self.act_mock.rollback.called)
|
||||||
|
|
||||||
|
def test_apply_data_format_value(self, mock_ext_mgr):
|
||||||
|
self.rule = rules.create(actions_json=[
|
||||||
|
{'action': 'set-attribute',
|
||||||
|
'path': '/driver_info/ipmi_address',
|
||||||
|
'value': '{data[memory_mb]}'}],
|
||||||
|
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.assertEqual(1, self.act_mock.apply.call_count)
|
||||||
|
self.assertFalse(self.act_mock.rollback.called)
|
||||||
|
|
||||||
|
def test_apply_data_format_value_fail(self, mock_ext_mgr):
|
||||||
|
self.rule = rules.create(
|
||||||
|
actions_json=[
|
||||||
|
{'action': 'set-attribute',
|
||||||
|
'path': '/driver_info/ipmi_address',
|
||||||
|
'value': '{data[inventory][bmc_address]}'}],
|
||||||
|
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_rollback(self, mock_ext_mgr):
|
def test_rollback(self, mock_ext_mgr):
|
||||||
mock_ext_mgr.return_value.__getitem__.return_value = self.ext_mock
|
mock_ext_mgr.return_value.__getitem__.return_value = self.ext_mock
|
||||||
|
|
||||||
@ -380,7 +438,7 @@ class TestApply(BaseTest):
|
|||||||
rule.check_conditions.assert_called_once_with(self.node_info,
|
rule.check_conditions.assert_called_once_with(self.node_info,
|
||||||
self.data)
|
self.data)
|
||||||
rule.apply_actions.assert_called_once_with(
|
rule.apply_actions.assert_called_once_with(
|
||||||
self.node_info, rollback=bool(idx))
|
self.node_info, rollback=bool(idx), data=self.data)
|
||||||
|
|
||||||
def test_actions(self, mock_get_all):
|
def test_actions(self, mock_get_all):
|
||||||
mock_get_all.return_value = self.rules
|
mock_get_all.return_value = self.rules
|
||||||
@ -393,7 +451,7 @@ class TestApply(BaseTest):
|
|||||||
rule.check_conditions.assert_called_once_with(self.node_info,
|
rule.check_conditions.assert_called_once_with(self.node_info,
|
||||||
self.data)
|
self.data)
|
||||||
rule.apply_actions.assert_called_once_with(
|
rule.apply_actions.assert_called_once_with(
|
||||||
self.node_info, rollback=bool(idx))
|
self.node_info, rollback=bool(idx), data=self.data)
|
||||||
|
|
||||||
def test_no_rollback(self, mock_get_all):
|
def test_no_rollback(self, mock_get_all):
|
||||||
mock_get_all.return_value = self.rules
|
mock_get_all.return_value = self.rules
|
||||||
@ -406,7 +464,7 @@ class TestApply(BaseTest):
|
|||||||
rule.check_conditions.assert_called_once_with(self.node_info,
|
rule.check_conditions.assert_called_once_with(self.node_info,
|
||||||
self.data)
|
self.data)
|
||||||
rule.apply_actions.assert_called_once_with(
|
rule.apply_actions.assert_called_once_with(
|
||||||
self.node_info, rollback=False)
|
self.node_info, rollback=False, data=self.data)
|
||||||
|
|
||||||
def test_only_rollback(self, mock_get_all):
|
def test_only_rollback(self, mock_get_all):
|
||||||
mock_get_all.return_value = self.rules
|
mock_get_all.return_value = self.rules
|
||||||
@ -419,4 +477,4 @@ class TestApply(BaseTest):
|
|||||||
rule.check_conditions.assert_called_once_with(self.node_info,
|
rule.check_conditions.assert_called_once_with(self.node_info,
|
||||||
self.data)
|
self.data)
|
||||||
rule.apply_actions.assert_called_once_with(
|
rule.apply_actions.assert_called_once_with(
|
||||||
self.node_info, rollback=True)
|
self.node_info, rollback=True, data=self.data)
|
||||||
|
5
releasenotes/notes/extend-rules-9a9d38701e970611.yaml
Normal file
5
releasenotes/notes/extend-rules-9a9d38701e970611.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Conditions now support comparing fields from node info;
|
||||||
|
- Actions support formatting to fetch values from introspection data.
|
||||||
|
See http://docs.openstack.org/developer/ironic-inspector/usage.html#introspection-rules
|
Loading…
Reference in New Issue
Block a user