diff --git a/ovs_dbg/filter.py b/ovs_dbg/filter.py index 691ea1a..364699b 100644 --- a/ovs_dbg/filter.py +++ b/ovs_dbg/filter.py @@ -2,12 +2,46 @@ """ import pyparsing as pp import netaddr +from functools import reduce +from operator import and_, or_ from ovs_dbg.decoders import decode_default, decode_int, Decoder, IPMask, EthMask from ovs_dbg.fields import field_decoders +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 = { @@ -32,7 +66,7 @@ def __repr__(self): ) def _find_data_in_kv(self, kv_list): - """Find a value for evaluation in a list of KeyValue + """Find a KeyValue for evaluation in a list of KeyValue Args: kv_list (list[KeyValue]): list of KeyValue to look into @@ -46,8 +80,8 @@ def _find_data_in_kv(self, kv_list): for kv in kvs: if kv.key == self.field: # exact match - return kv.value - elif len(key_parts) > 1: + return kv + if len(key_parts) > 1: data = kv.value for subkey in key_parts[1:]: try: @@ -58,9 +92,10 @@ def _find_data_in_kv(self, kv_list): if not data: break if data: - return data + return kv + return None - def _find_data(self, flow): + def _find_keyval_to_evaluate(self, flow): """Finds the key-value to use for evaluation""" for section in flow.sections: data = self._find_data_in_kv(section.data) @@ -69,13 +104,22 @@ def _find_data(self, flow): return None def evaluate(self, flow): - data = self._find_data(flow) - if not data: - return False + """ + Return whether the clause is satisfied by the flow + + Args: + flow (Flow): the flow to evaluate + """ + keyval = self._find_keyval_to_evaluate(flow) + + if not keyval: + return EvaluationResult(False) + + data = keyval.value if not self.value and not self.operator: # just asserting the existance of the key - return True + return EvaluationResult(True, keyval) # Decode the value based on the type of data if isinstance(data, Decoder): @@ -86,13 +130,13 @@ def evaluate(self, flow): decoded_value = decoder(self.value) if self.operator == "=": - return decoded_value == data + return EvaluationResult(decoded_value == data, keyval) elif self.operator == "<": - return data < decoded_value + return EvaluationResult(data < decoded_value, keyval) elif self.operator == ">": - return data > decoded_value + return EvaluationResult(data > decoded_value, keyval) elif self.operator == "~=": - return decoded_value in data + return EvaluationResult(decoded_value in data, keyval) class BoolNot: @@ -103,7 +147,7 @@ def __repr__(self): return "NOT({})".format(self.args) def evaluate(self, flow): - return not self.args.evaluate(flow) + return ~self.args.evaluate(flow) class BoolAnd: @@ -114,7 +158,7 @@ def __repr__(self): return "AND({})".format(self.args) def evaluate(self, flow): - return all([arg.evaluate(flow) for arg in self.args]) + return reduce(and_, [arg.evaluate(flow) for arg in self.args]) class BoolOr: @@ -122,7 +166,7 @@ def __init__(self, pattern): self.args = pattern[0][0::2] def evaluate(self, flow): - return any([arg.evaluate(flow) for arg in self.args]) + return reduce(or_, [arg.evaluate(flow) for arg in self.args]) def __repr__(self): return "OR({})".format(self.args) diff --git a/ovs_dbg/ofparse/console.py b/ovs_dbg/ofparse/console.py index 06de0b9..d72c6e7 100644 --- a/ovs_dbg/ofparse/console.py +++ b/ovs_dbg/ofparse/console.py @@ -94,20 +94,21 @@ def __init__(self, opts=None, console=None, **kwargs): def style_from_opts(self, opts): return self._style_from_opts(opts, "console", Style) - def print_flow(self, flow): + def print_flow(self, flow, highlighted=None): """ Prints a flow to the console Args: flow (ovs_dbg.OFPFlow): the flow to print style (dict): Optional; style dictionary to use + highlighted (list): Optional; list of KeyValues to highlight """ buf = ConsoleBuffer(Text()) - self.format_flow(buf, flow) + self.format_flow(buf, flow, highlighted) self.console.print(buf.text) - def format_flow(self, buf, flow): + def format_flow(self, buf, flow, highlighted=None): """ Formats the flow into the provided buffer as a rich.Text @@ -115,8 +116,11 @@ def format_flow(self, buf, flow): buf (FlowBuffer): the flow buffer to append to flow (ovs_dbg.OFPFlow): the flow to format style (FlowStyle): Optional; style object to use + highlighted (list): Optional; list of KeyValues to highlight """ - return super(ConsoleFormatter, self).format_flow(buf, flow, self.style) + return super(ConsoleFormatter, self).format_flow( + buf, flow, self.style, highlighted + ) def hash_pallete(hue, saturation, value): diff --git a/ovs_dbg/ofparse/dp.py b/ovs_dbg/ofparse/dp.py index 402374a..f21ad6d 100644 --- a/ovs_dbg/ofparse/dp.py +++ b/ovs_dbg/ofparse/dp.py @@ -80,7 +80,12 @@ def callback(flow): def append_to_tree(parent, flow): buf = ConsoleBuffer(Text()) - ofconsole.format_flow(buf, flow) + highlighted = None + if opts.get("highlight"): + result = opts.get("highlight").evaluate(flow) + if result: + highlighted = result.kv + ofconsole.format_flow(buf, flow, highlighted) tree_elem = parent.add(buf.text) return tree_elem @@ -107,7 +112,7 @@ def callback(flow): filter=opts.get("filter"), ) - html_obj = get_html_obj(flow_list) + html_obj = get_html_obj(flow_list, opts) print(html_obj) @@ -220,7 +225,7 @@ def callback(flow): html_obj += svg.decode("utf-8") html_obj += "" - html_obj += get_html_obj(list(itertools.chain(*recirc_flows.values()))) + html_obj += get_html_obj(list(itertools.chain(*recirc_flows.values())), opts) print(html_obj) @@ -229,6 +234,7 @@ def __init__(self, flow=None, opts=None): self._flow = flow self._formatter = HTMLFormatter(opts) self._subflows = list() + self._opts = opts def append(self, flow): self._subflows.append(flow) @@ -246,7 +252,12 @@ def render(self, item=0): id=self._flow.id ) buf = HTMLBuffer() - self._formatter.format_flow(buf, self._flow, self._style) + highlighted = None + if self._opts.get("highlight"): + result = self._opts.get("highlight").evaluate(self._flow) + if result: + highlighted = result.kv + self._formatter.format_flow(buf, self._flow, highlighted) html_obj += buf.text html_obj += "" if self._subflows: @@ -265,13 +276,13 @@ def render(self, item=0): return html_obj, item -def get_html_obj(flow_list, flow_attrs=None): +def get_html_obj(flow_list, opts=None): def append_to_html(parent, flow): - html_flow = HTMLFlowTree(flow) + html_flow = HTMLFlowTree(flow, opts) parent.append(html_flow) return html_flow - root = HTMLFlowTree() + root = HTMLFlowTree(flow=None, opts=opts) process_flow_tree(flow_list, root, 0, append_to_html) html_obj = """ diff --git a/ovs_dbg/ofparse/format.py b/ovs_dbg/ofparse/format.py index af53da6..71a0a09 100644 --- a/ovs_dbg/ofparse/format.py +++ b/ovs_dbg/ofparse/format.py @@ -133,13 +133,6 @@ class FlowFormatter: def __init__(self): self._highlighted = list() - def highlight(self, keys): - """Set the highlighted keys - Args: - keys (list[str]): list of keys to highlight - """ - self._highlighted = keys - def _style_from_opts(self, opts, opts_key, style_constructor): """Create style object from options @@ -172,7 +165,7 @@ def _style_from_opts(self, opts, opts_key, style_constructor): return FlowStyle({k: style_constructor(**v) for k, v in style.items()}) - def format_flow(self, buf, flow, style_obj=None): + def format_flow(self, buf, flow, style_obj=None, highlighted=None): """ Formats the flow into the provided buffer @@ -180,6 +173,7 @@ def format_flow(self, buf, flow, style_obj=None): buf (FlowBuffer): the flow buffer to append to flow (ovs_dbg.OFPFlow): the flow to format style_obj (FlowStyle): Optional; style to use + highlighted (list): Optional; list of KeyValues to highlight """ last_printed_pos = 0 style_obj = style_obj or FlowStyle() @@ -189,10 +183,12 @@ def format_flow(self, buf, flow, style_obj=None): flow.orig[last_printed_pos : section.pos], style=style_obj.get("default"), ) - self.format_kv_list(buf, section.data, section.string, style_obj) + self.format_kv_list( + buf, section.data, section.string, style_obj, highlighted + ) last_printed_pos = section.pos + len(section.string) - def format_kv_list(self, buf, kv_list, full_str, style_obj): + def format_kv_list(self, buf, kv_list, full_str, style_obj, highlighted): """ Format a KeyValue List @@ -201,10 +197,13 @@ def format_kv_list(self, buf, kv_list, full_str, style_obj): kv_list (list[KeyValue]: the KeyValue list to format full_str (str): the full string containing all k-v style_obj (FlowStyle): a FlowStyle object to use + highlighted (list): Optional; list of KeyValues to highlight """ for i in range(len(kv_list)): kv = kv_list[i] - written = self.format_kv(buf, kv, style_obj=style_obj) + written = self.format_kv( + buf, kv, style_obj=style_obj, highlighted=highlighted + ) end = kv_list[i + 1].meta.kpos if i < (len(kv_list) - 1) else len(full_str) @@ -213,7 +212,7 @@ def format_kv_list(self, buf, kv_list, full_str, style_obj): style=style_obj.get("default"), ) - def format_kv(self, buf, kv, style_obj): + def format_kv(self, buf, kv, style_obj, highlighted=None): """Format a KeyValue A formatted keyvalue has the following parts: @@ -223,14 +222,15 @@ def format_kv(self, buf, kv, style_obj): buf (FlowBuffer): buffer to append the KeyValue to kv (KeyValue): The KeyValue to print style_obj (FlowStyle): The style object to use + highlighted (list): Optional; list of KeyValues to highlight Returns the number of printed characters """ ret = 0 key = kv.meta.kstring - highlighted = key in self._highlighted + is_highlighted = key in [k.key for k in highlighted] if highlighted else False - key_style = style_obj.get_key_style(kv, highlighted) + key_style = style_obj.get_key_style(kv, is_highlighted) buf.append_key(kv, key_style) # format value ret += len(key) @@ -238,16 +238,16 @@ def format_kv(self, buf, kv, style_obj): return ret if kv.meta.delim not in ("\n", "\t", "\r", ""): - buf.append_delim(kv, style_obj.get_delim_style(highlighted)) + buf.append_delim(kv, style_obj.get_delim_style(is_highlighted)) ret += len(kv.meta.delim) - value_style = style_obj.get_value_style(kv, highlighted) + value_style = style_obj.get_value_style(kv, is_highlighted) buf.append_value(kv, value_style) # format value ret += len(kv.meta.vstring) if kv.meta.end_delim: - buf.append_end_delim(kv, style_obj.get_delim_style(highlighted)) + buf.append_end_delim(kv, style_obj.get_delim_style(is_highlighted)) ret += len(kv.meta.end_delim) return ret diff --git a/ovs_dbg/ofparse/html.py b/ovs_dbg/ofparse/html.py index aac4591..cf05042 100644 --- a/ovs_dbg/ofparse/html.py +++ b/ovs_dbg/ofparse/html.py @@ -110,5 +110,15 @@ def __init__(self, opts=None): ), ) - def format_flow(self, buf, flow, style=None): - return super(HTMLFormatter, self).format_flow(buf, flow, self.style) + def format_flow(self, buf, flow, highlighted=None): + """ + Formats the flow into the provided buffer as a html object + + Args: + buf (FlowBuffer): the flow buffer to append to + flow (ovs_dbg.OFPFlow): the flow to format + highlighted (list): Optional; list of KeyValues to highlight + """ + return super(HTMLFormatter, self).format_flow( + buf, flow, self.style, highlighted + ) diff --git a/ovs_dbg/ofparse/main.py b/ovs_dbg/ofparse/main.py index 2b71a7a..6f4264b 100644 --- a/ovs_dbg/ofparse/main.py +++ b/ovs_dbg/ofparse/main.py @@ -55,8 +55,16 @@ class Options(dict): type=str, show_default=False, ) +@click.option( + "-l", + "--highlight", + help="Highlight flows that match the filter expression. Run 'ofparse filter'" + "for a detailed description of the filtering syntax", + type=str, + show_default=False, +) @click.pass_context -def maincli(ctx, config, style, filename, paged, filter): +def maincli(ctx, config, style, filename, paged, filter, highlight): """ OpenFlow Parse utility. @@ -73,6 +81,12 @@ def maincli(ctx, config, style, filename, paged, filter): except Exception as e: raise click.BadParameter("Wrong filter syntax: {}".format(e)) + if highlight: + try: + ctx.obj["highlight"] = OFFilter(highlight) + except Exception as e: + raise click.BadParameter("Wrong filter syntax: {}".format(e)) + config_file = config or _default_config_path parser = configparser.ConfigParser() parser.read(config_file) diff --git a/ovs_dbg/ofparse/ofp.py b/ovs_dbg/ofparse/ofp.py index 0cc1dff..3885669 100644 --- a/ovs_dbg/ofparse/ofp.py +++ b/ovs_dbg/ofparse/ofp.py @@ -152,7 +152,12 @@ def callback(flow): if show_flows: for flow in flows: buf = ConsoleBuffer(Text()) - ConsoleFormatter(console, opts).format_flow(buf, flow) + highlighted = None + if opts.get("highlight"): + result = opts.get("highlight").evaluate(flow) + if result: + highlighted = result.kv + ConsoleFormatter(console, opts).format_flow(buf, flow, highlighted) lflow_tree.add(buf.text) with print_context(console, opts): @@ -185,8 +190,13 @@ def callback(flow): html_obj += "" diff --git a/ovs_dbg/ofparse/process.py b/ovs_dbg/ofparse/process.py index 773de4d..75423bd 100644 --- a/ovs_dbg/ofparse/process.py +++ b/ovs_dbg/ofparse/process.py @@ -79,7 +79,12 @@ def pprint(flow_factory, opts): console = ConsoleFormatter(opts) def callback(flow): - console.print_flow(flow) + high = None + if opts.get("highlight"): + result = opts.get("highlight").evaluate(flow) + if result: + high = result.kv + console.print_flow(flow, high) with print_context(console.console, opts): process_flows(flow_factory, callback, opts.get("filename"), opts.get("filter")) diff --git a/tests/test_filter.py b/tests/test_filter.py index 6a23e1c..d425880 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -1,56 +1,66 @@ import pytest +from ovs_dbg.kv import KeyValue from ovs_dbg.filter import OFFilter from ovs_dbg.ofp import OFPFlow @pytest.mark.parametrize( - "expr,flow,expected", + "expr,flow,expected,match", [ ( "nw_src=192.168.1.1 && tcp_dst=80", OFPFlow.from_string("nw_src=192.168.1.1,tcp_dst=80 actions=drop"), True, + ["nw_src", "tcp_dst"], ), ( "nw_src=192.168.1.2 || tcp_dst=80", OFPFlow.from_string("nw_src=192.168.1.1,tcp_dst=80 actions=drop"), True, + ["nw_src", "tcp_dst"], ), ( "nw_src=192.168.1.1 || tcp_dst=90", OFPFlow.from_string("nw_src=192.168.1.1,tcp_dst=80 actions=drop"), True, + ["nw_src", "tcp_dst"], ), ( "nw_src=192.168.1.2 && tcp_dst=90", OFPFlow.from_string("nw_src=192.168.1.1,tcp_dst=80 actions=drop"), False, + ["nw_src", "tcp_dst"], ), ( "nw_src=192.168.1.1", OFPFlow.from_string("nw_src=192.168.1.0/24,tcp_dst=80 actions=drop"), False, + ["nw_src"], ), ( "nw_src~=192.168.1.1", OFPFlow.from_string("nw_src=192.168.1.0/24,tcp_dst=80 actions=drop"), True, + ["nw_src"], ), ( "nw_src~=192.168.1.1/30", OFPFlow.from_string("nw_src=192.168.1.0/24,tcp_dst=80 actions=drop"), True, + ["nw_src"], ), ( "nw_src~=192.168.1.0/16", OFPFlow.from_string("nw_src=192.168.1.0/24,tcp_dst=80 actions=drop"), False, + ["nw_src"], ), ( "nw_src~=192.168.1.0/16", OFPFlow.from_string("nw_src=192.168.1.0/24,tcp_dst=80 actions=drop"), False, + ["nw_src"], ), ( "n_bytes=100", @@ -58,6 +68,7 @@ "n_bytes=100 priority=100,nw_src=192.168.1.0/24,tcp_dst=80 actions=drop" ), True, + ["n_bytes"], ), ( "n_bytes>10", @@ -65,6 +76,7 @@ "n_bytes=100 priority=100,nw_src=192.168.1.0/24,tcp_dst=80 actions=drop" ), True, + ["n_bytes"], ), ( "n_bytes>100", @@ -72,6 +84,7 @@ "n_bytes=100 priority=100,nw_src=192.168.1.0/24,tcp_dst=80 actions=drop" ), False, + ["n_bytes"], ), ( "n_bytes<100", @@ -79,6 +92,7 @@ "n_bytes=100 priority=100,nw_src=192.168.1.0/24,tcp_dst=80 actions=drop" ), False, + ["n_bytes"], ), ( "n_bytes<1000", @@ -86,6 +100,7 @@ "n_bytes=100 priority=100,nw_src=192.168.1.0/24,tcp_dst=80 actions=drop" ), True, + ["n_bytes"], ), ( "n_bytes>0 && drop=true", @@ -93,6 +108,7 @@ "n_bytes=100 priority=100,nw_src=192.168.1.0/24,tcp_dst=80 actions=drop" ), True, + ["n_bytes", "drop"], ), ( "n_bytes>0 && drop=true", @@ -100,6 +116,7 @@ "n_bytes=100 priority=100,nw_src=192.168.1.0/24,tcp_dst=80 actions=2" ), False, + ["n_bytes"], ), ( "n_bytes>10 && !output.port=3", @@ -107,6 +124,7 @@ "n_bytes=100 priority=100,nw_src=192.168.1.0/24,tcp_dst=80 actions=2" ), True, + ["n_bytes", "output"], ), ( "dl_src=00:11:22:33:44:55", @@ -114,6 +132,7 @@ "n_bytes=100 priority=100,dl_src=00:11:22:33:44:55,nw_src=192.168.1.0/24,tcp_dst=80 actions=2" ), True, + ["dl_src"], ), ( "dl_src~=00:11:22:33:44:55", @@ -121,6 +140,7 @@ "n_bytes=100 priority=100,dl_src=00:11:22:33:44:55/ff:ff:ff:ff:ff:00,nw_src=192.168.1.0/24,tcp_dst=80 actions=2" ), True, + ["dl_src"], ), ( "dl_src~=00:11:22:33:44:66", @@ -128,6 +148,7 @@ "n_bytes=100 priority=100,dl_src=00:11:22:33:44:55/ff:ff:ff:ff:ff:00,nw_src=192.168.1.0/24,tcp_dst=80 actions=2" ), True, + ["dl_src"], ), ( "dl_src~=00:11:22:33:44:66 && tp_dst=1000", @@ -135,6 +156,7 @@ "n_bytes=100 priority=100,dl_src=00:11:22:33:44:55/ff:ff:ff:ff:ff:00,nw_src=192.168.1.0/24,tp_dst=0x03e8/0xfff8 actions=2" ), False, + ["dl_src", "tp_dst"], ), ( "dl_src~=00:11:22:33:44:66 && tp_dst~=1000", @@ -142,10 +164,16 @@ "n_bytes=100 priority=100,dl_src=00:11:22:33:44:55/ff:ff:ff:ff:ff:00,nw_src=192.168.1.0/24,tp_dst=0x03e8/0xfff8 actions=2" ), True, + ["dl_src", "tp_dst"], ), ], ) -def test_filter(expr, flow, expected): +def test_filter(expr, flow, expected, match): ffilter = OFFilter(expr) result = ffilter.evaluate(flow) - assert expected == result + if expected: + assert result + else: + assert not result + + assert [kv.key for kv in result.kv] == match