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