diff mbox series

[ovs-dev,RFC,09/11] python: add flow filtering syntax

Message ID 20210901100103.111487-10-amorenoz@redhat.com
State RFC
Headers show
Series python: introduce flow parsing library | expand

Commit Message

Adrian Moreno Sept. 1, 2021, 10:01 a.m. UTC
Based on pyparsing, create a very simple filtering syntax

It supports basic logic statements (and, &, or, ||, not, !), numerical
operations (<, >), equality (=) and masking (~=). The latter is only
supported in certain fields (IntMask, EthMask, IPMask).

Signed-off-by: Adrian Moreno <amorenoz@redhat.com>
---
 python/automake.mk         |   3 +-
 python/ovs/flows/filter.py | 158 +++++++++++++++++++++++++++++++++++++
 python/setup.py            |   2 +-
 3 files changed, 161 insertions(+), 2 deletions(-)
 create mode 100644 python/ovs/flows/filter.py
diff mbox series

Patch

diff --git a/python/automake.mk b/python/automake.mk
index 112eb5565..cff41a657 100644
--- a/python/automake.mk
+++ b/python/automake.mk
@@ -48,7 +48,8 @@  ovs_pyfiles = \
 	python/ovs/flows/list.py \
 	python/ovs/flows/flow.py \
 	python/ovs/flows/ofp.py \
-	python/ovs/flows/odp.py
+	python/ovs/flows/odp.py \
+	python/ovs/flows/filter.py
 
 # These python files are used at build time but not runtime,
 # so they are not installed.
diff --git a/python/ovs/flows/filter.py b/python/ovs/flows/filter.py
new file mode 100644
index 000000000..735c7546c
--- /dev/null
+++ b/python/ovs/flows/filter.py
@@ -0,0 +1,158 @@ 
+""" Defines a Flow Filtering syntax
+"""
+import pyparsing as pp
+import netaddr
+
+from ovs.flows.decoders import decode_default, decode_int, Decoder, IPMask, EthMask
+
+
+class ClauseExpression:
+    operators = {}
+    type_decoders = {
+        int: decode_int,
+        netaddr.IPAddress: IPMask,
+        netaddr.EUI: EthMask,
+        bool: bool,
+    }
+
+    def __init__(self, tokens):
+        self.field = tokens[0]
+        self.value = ""
+        self.operator = ""
+
+        if len(tokens) > 1:
+            self.operator = tokens[1]
+            self.value = tokens[2]
+
+    def __repr__(self):
+        return "{}(field: {}, operator: {}, value: {})".format(
+            self.__class__.__name__, self.field, self.operator, self.value
+        )
+
+    def _find_data_in_kv(self, kv_list):
+        """Find a value for evaluation in a list of KeyValue
+
+        Args:
+            kv_list (list[KeyValue]): list of KeyValue to look into
+        """
+        key_parts = self.field.split(".")
+        field = key_parts[0]
+        kvs = [kv for kv in kv_list if kv.key == field]
+        if not kvs:
+            return None
+
+        for kv in kvs:
+            if kv.key == self.field:
+                # exact match
+                return kv.value
+            elif len(key_parts) > 1:
+                data = kv.value
+                for subkey in key_parts[1:]:
+                    try:
+                        data = data.get(subkey)
+                    except Exception:
+                        data = None
+                        break
+                    if not data:
+                        break
+                if data:
+                    return data
+
+    def _find_data(self, flow):
+        """Finds the key-value to use for evaluation"""
+        for section in flow.sections:
+            data = self._find_data_in_kv(section.data)
+            if data:
+                return data
+        return None
+
+    def evaluate(self, flow):
+        data = self._find_data(flow)
+        if not data:
+            return False
+
+        if not self.value and not self.operator:
+            # just asserting the existance of the key
+            return True
+
+        # Decode the value based on the type of data
+        if isinstance(data, Decoder):
+            decoder = data.__class__
+        else:
+            decoder = self.type_decoders.get(data.__class__) or decode_default
+
+        decoded_value = decoder(self.value)
+
+        if self.operator == "=":
+            return decoded_value == data
+        elif self.operator == "<":
+            return data < decoded_value
+        elif self.operator == ">":
+            return data > decoded_value
+        elif self.operator == "~=":
+            return decoded_value in data
+
+
+class BoolNot:
+    def __init__(self, t):
+        self.op, self.args = t[0]
+
+    def __repr__(self):
+        return "NOT({})".format(self.args)
+
+    def evaluate(self, flow):
+        return not self.args.evaluate(flow)
+
+
+class BoolAnd:
+    def __init__(self, pattern):
+        self.args = pattern[0][0::2]
+
+    def __repr__(self):
+        return "AND({})".format(self.args)
+
+    def evaluate(self, flow):
+        return all([arg.evaluate(flow) for arg in self.args])
+
+
+class BoolOr:
+    def __init__(self, pattern):
+        self.args = pattern[0][0::2]
+
+    def evaluate(self, flow):
+        return any([arg.evaluate(flow) for arg in self.args])
+
+    def __repr__(self):
+        return "OR({})".format(self.args)
+
+
+class OFFilter:
+    w = pp.Word(pp.alphanums + "." + ":" + "_" + "/" + "-")
+    operators = (
+        pp.Literal("=")
+        | pp.Literal("~=")
+        | pp.Literal("<")
+        | pp.Literal(">")
+        | pp.Literal("!=")
+    )
+
+    clause = (w + operators + w) | w
+    clause.setParseAction(ClauseExpression)
+
+    statement = pp.infixNotation(
+        clause,
+        [
+            ("!", 1, pp.opAssoc.RIGHT, BoolNot),
+            ("not", 1, pp.opAssoc.RIGHT, BoolNot),
+            ("&&", 2, pp.opAssoc.LEFT, BoolAnd),
+            ("and", 2, pp.opAssoc.LEFT, BoolAnd),
+            ("||", 2, pp.opAssoc.LEFT, BoolOr),
+            ("or", 2, pp.opAssoc.LEFT, BoolOr),
+        ],
+    )
+
+    def __init__(self, expr):
+        self._filter = self.statement.parseString(expr)
+
+    def evaluate(self, flow):
+        return self._filter[0].evaluate(flow)
diff --git a/python/setup.py b/python/setup.py
index b06370bd9..4e8a9761a 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -87,7 +87,7 @@  setup_args = dict(
     ext_modules=[setuptools.Extension("ovs._json", sources=["ovs/_json.c"],
                                       libraries=['openvswitch'])],
     cmdclass={'build_ext': try_build_ext},
-    install_requires=['sortedcontainers', 'netaddr'],
+    install_requires=['sortedcontainers', 'netaddr', 'pyparsing'],
     extras_require={':sys_platform == "win32"': ['pywin32 >= 1.0']},
 )