Add invert option to rule conditions
Some conditions do not have a native counterparts, so we need a way to invert them (aka NOT operation). This patch adds a new generic parameter "invert", defaulting to False. Change-Id: I50342689ba52346a5a4fbf362536b629fc688986
This commit is contained in:
parent
cc0c7bd77c
commit
b5fd510db4
@ -87,6 +87,8 @@ between values from introspection data and node. Both schemes use JSON path::
|
|||||||
if scheme (node or data) is missing, condition compares data with
|
if scheme (node or data) is missing, condition compares data with
|
||||||
introspection data.
|
introspection data.
|
||||||
|
|
||||||
|
``invert`` boolean value, whether to invert the result of the comparison.
|
||||||
|
|
||||||
``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:
|
||||||
|
|
||||||
|
@ -92,6 +92,7 @@ class RuleCondition(Base):
|
|||||||
rule = Column(String(36), ForeignKey('rules.uuid'))
|
rule = Column(String(36), ForeignKey('rules.uuid'))
|
||||||
op = Column(String(255), nullable=False)
|
op = Column(String(255), nullable=False)
|
||||||
multiple = Column(String(255), nullable=False)
|
multiple = Column(String(255), nullable=False)
|
||||||
|
invert = Column(Boolean, default=False)
|
||||||
# NOTE(dtantsur): while all operations now require a field, I can also
|
# NOTE(dtantsur): while all operations now require a field, I can also
|
||||||
# imagine user-defined operations that do not, thus it's nullable.
|
# imagine user-defined operations that do not, thus it's nullable.
|
||||||
field = Column(Text)
|
field = Column(Text)
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Add invert field to rule condition
|
||||||
|
|
||||||
|
Revision ID: e169a4a81d88
|
||||||
|
Revises: d588418040d
|
||||||
|
Create Date: 2016-02-16 11:19:29.715615
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e169a4a81d88'
|
||||||
|
down_revision = 'd588418040d'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('rule_conditions', sa.Column('invert', sa.Boolean(),
|
||||||
|
nullable=True, default=False))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column('rule_conditions', 'invert')
|
@ -59,6 +59,10 @@ def conditions_schema():
|
|||||||
"description": "how to treat multiple values",
|
"description": "how to treat multiple values",
|
||||||
"enum": ["all", "any", "first"]
|
"enum": ["all", "any", "first"]
|
||||||
},
|
},
|
||||||
|
"invert": {
|
||||||
|
"description": "whether to invert the result",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
# other properties are validated by plugins
|
# other properties are validated by plugins
|
||||||
"additionalProperties": True
|
"additionalProperties": True
|
||||||
@ -157,6 +161,9 @@ class IntrospectionRule(object):
|
|||||||
|
|
||||||
for value in field_values:
|
for value in field_values:
|
||||||
result = cond_ext.check(node_info, value, cond.params)
|
result = cond_ext.check(node_info, value, cond.params)
|
||||||
|
if cond.invert:
|
||||||
|
result = not result
|
||||||
|
|
||||||
if (cond.multiple == 'first'
|
if (cond.multiple == 'first'
|
||||||
or (cond.multiple == 'all' and not result)
|
or (cond.multiple == 'all' and not result)
|
||||||
or (cond.multiple == 'any' and result)):
|
or (cond.multiple == 'any' and result)):
|
||||||
@ -276,6 +283,7 @@ def create(conditions_json, actions_json, uuid=None,
|
|||||||
act_mgr = plugins_base.rule_actions_manager()
|
act_mgr = plugins_base.rule_actions_manager()
|
||||||
|
|
||||||
conditions = []
|
conditions = []
|
||||||
|
reserved_params = {'op', 'field', 'multiple', 'invert'}
|
||||||
for cond_json in conditions_json:
|
for cond_json in conditions_json:
|
||||||
field = cond_json['field']
|
field = cond_json['field']
|
||||||
|
|
||||||
@ -293,7 +301,7 @@ def create(conditions_json, actions_json, uuid=None,
|
|||||||
|
|
||||||
plugin = cond_mgr[cond_json['op']].obj
|
plugin = cond_mgr[cond_json['op']].obj
|
||||||
params = {k: v for k, v in cond_json.items()
|
params = {k: v for k, v in cond_json.items()
|
||||||
if k not in ('op', 'field', 'multiple')}
|
if k not in reserved_params}
|
||||||
try:
|
try:
|
||||||
plugin.validate(params)
|
plugin.validate(params)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
@ -301,8 +309,11 @@ def create(conditions_json, actions_json, uuid=None,
|
|||||||
'%(error)s') %
|
'%(error)s') %
|
||||||
{'op': cond_json['op'], 'error': exc})
|
{'op': cond_json['op'], 'error': exc})
|
||||||
|
|
||||||
conditions.append((cond_json['field'], cond_json['op'],
|
conditions.append((cond_json['field'],
|
||||||
cond_json.get('multiple', 'any'), params))
|
cond_json['op'],
|
||||||
|
cond_json.get('multiple', 'any'),
|
||||||
|
cond_json.get('invert', False),
|
||||||
|
params))
|
||||||
|
|
||||||
actions = []
|
actions = []
|
||||||
for action_json in actions_json:
|
for action_json in actions_json:
|
||||||
@ -322,9 +333,11 @@ def create(conditions_json, actions_json, uuid=None,
|
|||||||
rule = db.Rule(uuid=uuid, description=description,
|
rule = db.Rule(uuid=uuid, description=description,
|
||||||
disabled=False, created_at=timeutils.utcnow())
|
disabled=False, created_at=timeutils.utcnow())
|
||||||
|
|
||||||
for field, op, multiple, params in conditions:
|
for field, op, multiple, invert, params in conditions:
|
||||||
rule.conditions.append(db.RuleCondition(op=op, field=field,
|
rule.conditions.append(db.RuleCondition(op=op,
|
||||||
|
field=field,
|
||||||
multiple=multiple,
|
multiple=multiple,
|
||||||
|
invert=invert,
|
||||||
params=params))
|
params=params))
|
||||||
|
|
||||||
for action, params in actions:
|
for action, params in actions:
|
||||||
|
@ -309,7 +309,10 @@ class Test(Base):
|
|||||||
{'field': 'local_gb', 'op': 'lt', 'value': 1000},
|
{'field': 'local_gb', 'op': 'lt', 'value': 1000},
|
||||||
{'field': 'local_gb', 'op': 'matches', 'value': '[0-9]+'},
|
{'field': 'local_gb', 'op': 'matches', 'value': '[0-9]+'},
|
||||||
{'field': 'cpu_arch', 'op': 'contains', 'value': '[0-9]+'},
|
{'field': 'cpu_arch', 'op': 'contains', 'value': '[0-9]+'},
|
||||||
{'field': 'root_disk.wwn', 'op': 'is-empty'}
|
{'field': 'root_disk.wwn', 'op': 'is-empty'},
|
||||||
|
{'field': 'inventory.interfaces[*].ipv4_address',
|
||||||
|
'op': 'contains', 'value': r'127\.0\.0\.1',
|
||||||
|
'invert': True, 'multiple': 'all'},
|
||||||
],
|
],
|
||||||
'actions': [
|
'actions': [
|
||||||
{'action': 'set-attribute', 'path': '/extra/foo',
|
{'action': 'set-attribute', 'path': '/extra/foo',
|
||||||
|
@ -206,6 +206,23 @@ class TestCheckConditions(BaseTest):
|
|||||||
self.cond_mock.check.call_count)
|
self.cond_mock.check.call_count)
|
||||||
self.assertTrue(res)
|
self.assertTrue(res)
|
||||||
|
|
||||||
|
def test_invert(self, mock_ext_mgr):
|
||||||
|
self.conditions_json = [
|
||||||
|
{'op': 'eq', 'field': 'memory_mb', 'value': 42,
|
||||||
|
'invert': True},
|
||||||
|
]
|
||||||
|
self.rule = rules.create(conditions_json=self.conditions_json,
|
||||||
|
actions_json=self.actions_json)
|
||||||
|
|
||||||
|
mock_ext_mgr.return_value.__getitem__.return_value = self.ext_mock
|
||||||
|
self.cond_mock.check.return_value = False
|
||||||
|
|
||||||
|
res = self.rule.check_conditions(self.node_info, self.data)
|
||||||
|
|
||||||
|
self.cond_mock.check.assert_called_once_with(self.node_info, 1024,
|
||||||
|
{'value': 42})
|
||||||
|
self.assertTrue(res)
|
||||||
|
|
||||||
def test_no_field(self, mock_ext_mgr):
|
def test_no_field(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.cond_mock.check.return_value = True
|
self.cond_mock.check.return_value = True
|
||||||
|
4
releasenotes/notes/rules-invert-2585173a11db3c31.yaml
Normal file
4
releasenotes/notes/rules-invert-2585173a11db3c31.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Introspection rules conditions got a new generic "invert" parameter that
|
||||||
|
inverts the result of the condition.
|
Loading…
Reference in New Issue
Block a user