Skip to content

Commit

Permalink
Merge branch 'main' into test-job-template
Browse files Browse the repository at this point in the history
  • Loading branch information
bzwei authored Feb 6, 2023
2 parents f0695a6 + a1d2523 commit bfdb4ac
Show file tree
Hide file tree
Showing 13 changed files with 556 additions and 6 deletions.
80 changes: 79 additions & 1 deletion ansible_rulebook/condition_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
pyparsing_common,
)

from ansible_rulebook.exception import (
SelectattrOperatorException,
SelectOperatorException,
)

ParserElement.enable_packrat()

from ansible_rulebook.condition_types import ( # noqa: E402
Expand All @@ -44,9 +49,38 @@
NegateExpression,
OperatorExpression,
SearchType,
SelectattrType,
SelectType,
String,
)

VALID_SELECT_ATTR_OPERATORS = [
"==",
"!=",
">",
">=",
"<",
"<=",
"regex",
"search",
"match",
"in",
"not in",
"contains",
"not contains",
]

VALID_SELECT_OPERATORS = [
"==",
"!=",
">",
">=",
"<",
"<=",
"regex",
"search",
"match",
]
SUPPORTED_SEARCH_KINDS = ("match", "regex", "search")

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -95,13 +129,43 @@
)
list_values = Suppress("[") + delim_value + Suppress("]")

selectattr_t = (
Literal("selectattr")
+ Suppress("(")
+ Group(delimitedList(ident | allowed_values | list_values))
+ Suppress(")")
)

select_t = (
Literal("select")
+ Suppress("(")
+ Group(delimitedList(ident | allowed_values | list_values))
+ Suppress(")")
)


def as_list(var):
if hasattr(var.__class__, "as_list"):
return var.as_list()
return var


def SelectattrTypeFactory(tokens):
if tokens[1].value not in VALID_SELECT_ATTR_OPERATORS:
raise SelectattrOperatorException(
f"Operator {tokens[1]} is not supported"
)

return SelectattrType(tokens[0], tokens[1], as_list(tokens[2]))


def SelectTypeFactory(tokens):
if tokens[0].value not in VALID_SELECT_OPERATORS:
raise SelectOperatorException(f"Operator {tokens[0]} is not supported")

return SelectType(tokens[0], as_list(tokens[1]))


def SearchTypeFactory(kind, tokens):
options = []
if len(tokens) > 1:
Expand All @@ -123,6 +187,18 @@ def OperatorExpressionFactory(tokens):
tokens[0], tokens[1], search_type
)
tokens = tokens[4:]
elif tokens[2] == "selectattr":
select_attr_type = SelectattrTypeFactory(tokens[3])
return_value = OperatorExpression(
tokens[0], tokens[1], select_attr_type
)
tokens = tokens[4:]
elif tokens[2] == "select":
select_type = SelectTypeFactory(tokens[3])
return_value = OperatorExpression(
tokens[0], tokens[1], select_type
)
tokens = tokens[4:]
else:
return_value = OperatorExpression(
as_list(tokens[0]), tokens[1], as_list(tokens[2])
Expand All @@ -137,7 +213,9 @@ def OperatorExpressionFactory(tokens):


all_terms = (
string_search_t
selectattr_t
| select_t
| string_search_t
| list_values
| float_t
| integer
Expand Down
17 changes: 16 additions & 1 deletion ansible_rulebook/condition_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,21 @@ class SearchType(NamedTuple):
options: List[KeywordValue] = None


class SelectattrType(NamedTuple):
key: String
operator: String
value: Union[Float, Integer, String, Boolean, List]


class SelectType(NamedTuple):
operator: String
value: Union[Float, Integer, String, Boolean, List]


class OperatorExpression(NamedTuple):
left: Union[Float, Integer, String, List]
operator: str
right: Union[Float, Integer, String, List, SearchType]
right: Union[Float, Integer, String, List, SearchType, SelectType]


class NegateExpression(NamedTuple):
Expand All @@ -67,6 +78,8 @@ class Condition(NamedTuple):
NegateExpression,
KeywordValue,
SearchType,
SelectattrType,
SelectType,
]


Expand All @@ -81,4 +94,6 @@ class Condition(NamedTuple):
NegateExpression,
KeywordValue,
SearchType,
SelectType,
SelectattrType,
]
10 changes: 10 additions & 0 deletions ansible_rulebook/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,13 @@ class VarsKeyMissingException(Exception):
class InvalidAssignmentException(Exception):

pass


class SelectattrOperatorException(Exception):

pass


class SelectOperatorException(Exception):

pass
29 changes: 29 additions & 0 deletions ansible_rulebook/json_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
NegateExpression,
OperatorExpression,
SearchType,
SelectattrType,
SelectType,
String,
)
from ansible_rulebook.exception import (
Expand Down Expand Up @@ -116,6 +118,17 @@ def visit_condition(parsed_condition: ConditionTypes, variables: Dict):
visit_condition(v, variables) for v in parsed_condition.options
]
return {"SearchType": data}
elif isinstance(parsed_condition, SelectattrType):
return dict(
key=visit_condition(parsed_condition.key, variables),
operator=visit_condition(parsed_condition.operator, variables),
value=visit_condition(parsed_condition.value, variables),
)
elif isinstance(parsed_condition, SelectType):
return dict(
operator=visit_condition(parsed_condition.operator, variables),
value=visit_condition(parsed_condition.value, variables),
)
elif isinstance(parsed_condition, KeywordValue):
return dict(
name=visit_condition(parsed_condition.name, variables),
Expand Down Expand Up @@ -143,6 +156,14 @@ def visit_condition(parsed_condition: ConditionTypes, variables: Dict):
return create_binary_node(
"SearchMatchesExpression", parsed_condition, variables
)
elif isinstance(parsed_condition.right, SelectattrType):
return create_binary_node(
"SelectAttrExpression", parsed_condition, variables
)
elif isinstance(parsed_condition.right, SelectType):
return create_binary_node(
"SelectExpression", parsed_condition, variables
)
elif parsed_condition.operator == "is not":
if isinstance(parsed_condition.right, Identifier):
if parsed_condition.right.value == "defined":
Expand All @@ -155,6 +176,14 @@ def visit_condition(parsed_condition: ConditionTypes, variables: Dict):
return create_binary_node(
"SearchNotMatchesExpression", parsed_condition, variables
)
elif isinstance(parsed_condition.right, SelectattrType):
return create_binary_node(
"SelectAttrNotExpression", parsed_condition, variables
)
elif isinstance(parsed_condition.right, SelectType):
return create_binary_node(
"SelectNotExpression", parsed_condition, variables
)
else:
raise Exception(f"Unhandled token {parsed_condition}")
elif isinstance(parsed_condition, NegateExpression):
Expand Down
89 changes: 87 additions & 2 deletions docs/conditions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ A condition can contain
* Multiple conditions where any one of them has to match
* Multiple conditions where not all one of them have to match

Supported datatypes
*******************
Supported data types
********************
The data type is of great importance for the rules engine. The following types are supported

* integers
Expand Down Expand Up @@ -96,6 +96,14 @@ Conditions support the following operators:
- To check if the regular expression pattern exists in the string
* - is not regex(pattern,ignorecase=true)
- To check if the regular expression pattern does not exist in the string
* - is select(operator, value)
- To check if an item exists in the list, that satisfies the test defined by operator and value
* - is not select(operator, value)
- To check if an item does not exist in the list, that does not satisfy the test defined by operator and value
* - is selectattr(key, operator, value)
- To check if an object exists in the list, that satisfies the test defined by key, operator and value
* - is not selectattr(key, operator, value)
- To check if an object does not exist in the list, that does not satisfy the test defined by key, operator and value
* - `<<`
- Assignment operator, to save the matching events or facts with events or facts prefix
* - not
Expand Down Expand Up @@ -582,6 +590,83 @@ String regular expression
| In the above example we check if the event.url does not have "example.com" in its value
| And the option controls that this is a case insensitive search.
Check if an item exists in a list based on a test
-------------------------------------------------

.. code-block:: yaml
name: check if an item exist in list
condition: event.levels is select('>=', 10)
action:
echo:
message: The list has an item with the value greater than or equal to 10
| In the above example "levels" is a list of integers e.g. [1,2,3,20], the test says
| check if any item exists in the list with a value >= 10. This test passes because
| of the presence of 20 in the list. If the value of "levels" is [1,2,3] then the
| test would yield False.
Check if an item does not exist in a list based on a test
---------------------------------------------------------

.. code-block:: yaml
name: check if an item does not exist in list
condition: event.levels is not select('>=', 10)
action:
echo:
message: The list does not have item with the value greater than or equal to 10
| In the above example "levels" is a list of integers e.g. [1,2,3], the test says
| check if *no* item exists with a value >= 10. This test passes because none of the items
| in the list is greater than or equal to 10. If the value of "levels" is [1,2,3,20] then
| the test would yield False because of the presence of 20 in the list.
| The result of the *select* condition is either True or False. It doesn't return the item or items.
| The select takes 2 arguments which are comma delimited, **operator** and **value**.
| The different operators we support are >,>=,<,<=,==,!=,match,search,regex
| The value is based on the operator used, if the operator is regex then the value is a pattern.
| If the operator is one of >,>=,<,<= then the value is either an integer or a float
Checking if an object exists in a list based on a test
------------------------------------------------------

.. code-block:: yaml
name: check if an object exist in list
condition: event.objects is selectattr('age', '>=', 20)
action:
echo:
message: An object with age greater than 20 found
| In the above example "objects" is a list of object's, with multiple properties. One of the
| properties is age, the test says check if any object exists in the list with an age >= 20.
Checking if an object does not exist in a list based on a test
---------------------------------------------------------------

.. code-block:: yaml
name: check if an object does not exist in list
condition: event.objects is not selectattr('age', '>=', 20)
action:
echo:
message: No object with age greater than 20 found
| In the above example "objects" is a list of object's, with multiple properties. One of the
| properties is age, the test says check if *no* object exists in the list with an age >= 20.
| The result of the *selectattr* condition is either True or False. It doesn't return the
| matching object or objects.
| The *selectattr* takes 3 arguments which are comma delimited, **key**, **operator** and **value**.
| The key is a valid key name in the object.
| The different operators we support are >, >=, <, <=, ==, !=, match, search, regex, in, not in,
| contains, not contains.
| The value is based on the operator used, if the operator is regex then the value is a pattern.
| If the operator is one of >, >=, <, <= then the value is either an integer or a float.
| If the operator is in or not in then the value is list of integer, float or string.

FAQ
***

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ install_requires =
janus
ansible-runner
websockets
drools_jpy == 0.1.9
drools_jpy == 0.2.0

[options.packages.find]
include =
Expand Down
30 changes: 30 additions & 0 deletions tests/examples/61_select_1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
- name: 61 select 1
hosts: all
sources:
- generic:
payload:
- name: Fred
age: 54
levels:
- 10
- 20
- 30
- name: Barney
age: 53
levels:
- 11
- 15
- 16
- name: Wilma
age: 53
levels:
- 1
- 5
- 6
rules:
- name: r1
condition: event.levels is select('>', 25)
action:
echo:
message: Found a player with level greater than 25
Loading

0 comments on commit bfdb4ac

Please sign in to comment.