From d6ff54faf763ccf11d5799d134b176f9b4eedfcc Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 26 Jan 2016 17:01:35 +0100 Subject: [PATCH] Add new conditions: matches and contains Both check a value against a regular expression. The former requires full match, while the latter requires matching anywhere in the string representaion of value. Change-Id: Ia59d17d6f8383aed97696d678fc1e7e329242692 --- doc/source/usage.rst | 4 ++- ironic_inspector/plugins/rules.py | 23 +++++++++++++++ ironic_inspector/test/functional.py | 2 ++ ironic_inspector/test/test_plugins_rules.py | 28 +++++++++++++++++++ .../contains-matches-ee28958b08995494.yaml | 4 +++ setup.cfg | 2 ++ 6 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/contains-matches-ee28958b08995494.yaml diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 4f617b883..4e811c1bb 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -63,7 +63,9 @@ A condition is represented by an object with fields: ``op`` the type of comparison operation, default available operators include : ``eq``, ``le``, ``ge``, ``ne``, ``lt``, ``gt`` (basic comparison operators), -``in-net`` (checks that IP address is in a given network). +``in-net`` (checks that IP address is in a given network), ``contains``, +``matches`` (check field against a regular expression, matches require a full +match). ``field`` a `JSON path `_ to the field in the introspection data to use in comparison. diff --git a/ironic_inspector/plugins/rules.py b/ironic_inspector/plugins/rules.py index 8cc93aed7..ae4ab781d 100644 --- a/ironic_inspector/plugins/rules.py +++ b/ironic_inspector/plugins/rules.py @@ -14,9 +14,11 @@ """Standard plugins for rules API.""" import operator +import re import netaddr +from ironic_inspector.common.i18n import _ from ironic_inspector.plugins import base from ironic_inspector import utils @@ -79,6 +81,27 @@ class NetCondition(base.RuleConditionPlugin): return netaddr.IPAddress(field) in network +class ReCondition(base.RuleConditionPlugin): + def validate(self, params, **kwargs): + try: + re.compile(params['value']) + except re.error as exc: + raise ValueError(_('invalid regular expression: %s') % exc) + + +class MatchesCondition(ReCondition): + def check(self, node_info, field, params, **kwargs): + regexp = params['value'] + if regexp[-1] != '$': + regexp += '$' + return re.match(regexp, str(field)) is not None + + +class ContainsCondition(ReCondition): + def check(self, node_info, field, params, **kwargs): + return re.search(params['value'], str(field)) is not None + + class FailAction(base.RuleActionPlugin): REQUIRED_PARAMS = {'message'} diff --git a/ironic_inspector/test/functional.py b/ironic_inspector/test/functional.py index 749326865..7bc7597ca 100644 --- a/ironic_inspector/test/functional.py +++ b/ironic_inspector/test/functional.py @@ -303,6 +303,8 @@ class Test(Base): {'field': 'memory_mb', 'op': 'eq', 'value': 12288}, {'field': 'local_gb', 'op': 'gt', 'value': 998}, {'field': 'local_gb', 'op': 'lt', 'value': 1000}, + {'field': 'local_gb', 'op': 'matches', 'value': '[0-9]+'}, + {'field': 'cpu_arch', 'op': 'contains', 'value': '[0-9]+'}, ], 'actions': [ {'action': 'set-attribute', 'path': '/extra/foo', diff --git a/ironic_inspector/test/test_plugins_rules.py b/ironic_inspector/test/test_plugins_rules.py index f2d11dbde..16d650b35 100644 --- a/ironic_inspector/test/test_plugins_rules.py +++ b/ironic_inspector/test/test_plugins_rules.py @@ -73,6 +73,34 @@ class TestSimpleConditions(test_base.BaseTest): self._test(cond, expected, *values) +class TestReConditions(test_base.BaseTest): + def test_validate(self): + for cond in (rules_plugins.MatchesCondition(), + rules_plugins.ContainsCondition()): + cond.validate({'value': r'[a-z]?(foo|b.r).+'}) + self.assertRaises(ValueError, cond.validate, + {'value': '**'}) + + def test_matches(self): + cond = rules_plugins.MatchesCondition() + for reg, field, res in [(r'.*', 'foo', True), + (r'fo{1,2}', 'foo', True), + (r'o{1,2}', 'foo', False), + (r'[1-9]*', 42, True), + (r'^(foo|bar)$', 'foo', True), + (r'fo', 'foo', False)]: + self.assertEqual(res, cond.check(None, field, {'value': reg})) + + def test_contains(self): + cond = rules_plugins.ContainsCondition() + for reg, field, res in [(r'.*', 'foo', True), + (r'fo{1,2}', 'foo', True), + (r'o{1,2}', 'foo', True), + (r'[1-9]*', 42, True), + (r'bar', 'foo', False)]: + self.assertEqual(res, cond.check(None, field, {'value': reg})) + + class TestNetCondition(test_base.BaseTest): cond = rules_plugins.NetCondition() diff --git a/releasenotes/notes/contains-matches-ee28958b08995494.yaml b/releasenotes/notes/contains-matches-ee28958b08995494.yaml new file mode 100644 index 000000000..2f721ea53 --- /dev/null +++ b/releasenotes/notes/contains-matches-ee28958b08995494.yaml @@ -0,0 +1,4 @@ +--- +features: + - New condition plugins "contains" and "matches" allow to match value against + regular expressions. diff --git a/setup.cfg b/setup.cfg index 26cf1ad7d..33c1ffba9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,8 @@ ironic_inspector.rules.conditions = ge = ironic_inspector.plugins.rules:GeCondition ne = ironic_inspector.plugins.rules:NeCondition in-net = ironic_inspector.plugins.rules:NetCondition + matches = ironic_inspector.plugins.rules:MatchesCondition + contains = ironic_inspector.plugins.rules:ContainsCondition ironic_inspector.rules.actions = example = ironic_inspector.plugins.example:ExampleRuleAction fail = ironic_inspector.plugins.rules:FailAction