Just some small comments below, and the request to fix up the comments.
I did not go too deep on the pyparser part but at a glance, it looks ok.

//Eelco

On 22 Nov 2021, at 12:22, Adrian Moreno wrote:

> 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

, and missing…

> supported in certain fields (IntMask, EthMask, IPMask).
>
> Masking operation is semantically equivalent to "includes",
> therefore:
>
> ip_src ~= 192.168.1.1
>
> means that ip_src field is either a host IP address equal to 192.168.1.1
> or an IPMask that includes it (e.g: 192.168.1.1/24)
>
> Signed-off-by: Adrian Moreno <amore...@redhat.com>
> ---
>  python/automake.mk         |   3 +-
>  python/ovs/flows/filter.py | 225 +++++++++++++++++++++++++++++++++++++
>  python/setup.py            |   2 +-
>  3 files changed, 228 insertions(+), 2 deletions(-)
>  create mode 100644 python/ovs/flows/filter.py
>
> diff --git a/python/automake.mk b/python/automake.mk
> index 8b0713cfc..21aa897f2 100644
> --- a/python/automake.mk
> +++ b/python/automake.mk
> @@ -49,7 +49,8 @@ ovs_pyfiles = \
>       python/ovs/flows/flow.py \
>       python/ovs/flows/ofp.py \
>       python/ovs/flows/ofp_act.py \
> -     python/ovs/flows/odp.py
> +     python/ovs/flows/odp.py \
> +     python/ovs/flows/filter.py

Add in alphabetical order

>
>  # 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..2d3555a60
> --- /dev/null
> +++ b/python/ovs/flows/filter.py
> @@ -0,0 +1,225 @@
> +""" Defines a Flow Filtering syntax
> +"""
> +import pyparsing as pp
> +import netaddr
> +from functools import reduce
> +from operator import and_, or_
> +
> +from ovs.flows.decoders import (
> +    decode_default,
> +    decode_int,
> +    Decoder,
> +    IPMask,
> +    EthMask,
> +)
> +
> +
> +class EvaluationResult:
> +    """An EvaluationResult is the result of an evaluation. It contains the
> +    boolean result and the list of key-values that were evaluated
> +
> +    Note that since boolean operations (and, not, or) are based only on
> +    __bool__ we use bitwise alternatives (&, ||, ~)
> +    """
> +
> +    def __init__(self, result, *kv):
> +        self.result = result
> +        self.kv = kv if kv else list()
> +
> +    def __and__(self, other):
> +        """Logical and operation"""
> +        return EvaluationResult(
> +            self.result and other.result, *self.kv, *other.kv
> +        )
> +
> +    def __or__(self, other):
> +        """Logical or operation"""
> +        return EvaluationResult(
> +            self.result or other.result, *self.kv, *other.kv
> +        )
> +
> +    def __invert__(self):
> +        """Logical not operation"""
> +        return EvaluationResult(not self.result, *self.kv)
> +
> +    def __bool__(self):
> +        """Boolean operation"""
> +        return self.result
> +
> +    def __repr__(self):
> +        return "{} [{}]".format(self.result, self.kv)
> +
> +
> +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 KeyValue for evaluation in a list of KeyValue
> +
> +        Args:
> +            kv_list (list[KeyValue]): list of KeyValue to look into
> +
> +        Returns:
> +            If found, tuple (kv, data) where kv is the KeyValue that matched
> +            and data is the data to be used for evaluation. None if not 
> found.
> +        """
> +        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, kv.value)
> +            if 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 (kv, data)
> +        return None
> +
> +    def _find_keyval_to_evaluate(self, flow):
> +        """Finds the key-value and data to use for evaluation on a flow
> +
> +        Args:
> +            flow(Flow): The flow where the lookup is performed
> +
> +        Returns:
> +            If found, tuple (kv, data) where kv is the KeyValue that matched
> +            and data is the data to be used for evaluation. None if not 
> found.
> +
> +        """
> +        for section in flow.sections:
> +            data = self._find_data_in_kv(section.data)
> +            if data:
> +                return data
> +        return None
> +
> +    def evaluate(self, flow):
> +        """
> +        Return whether the clause is satisfied by the flow
> +
> +        Args:
> +            flow (Flow): the flow to evaluate
> +        """
> +        result = self._find_keyval_to_evaluate(flow)
> +
> +        if not result:
> +            return EvaluationResult(False)
> +
> +        keyval, data = result
> +
> +        if not self.value and not self.operator:
> +            # just asserting the existance of the key
> +            return EvaluationResult(True, keyval)
> +
> +        # 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 EvaluationResult(decoded_value == data, keyval)
> +        elif self.operator == "<":
> +            return EvaluationResult(data < decoded_value, keyval)
> +        elif self.operator == ">":
> +            return EvaluationResult(data > decoded_value, keyval)
> +        elif self.operator == "~=":
> +            return EvaluationResult(decoded_value in data, keyval)
> +
> +
> +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 ~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 reduce(and_, [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 reduce(or_, [arg.evaluate(flow) for arg in self.args])
> +
> +    def __repr__(self):
> +        return "OR({})".format(self.args)
> +
> +
> +class OFFilter:

There are no test cases for OFFilter, do we need some?

> +    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']},
>  )
>
> -- 
> 2.31.1

_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to