On 1 Dec 2023, at 20:14, Adrian Moreno wrote: > Datapath flows can be arranged into a "tree"-like structure based on > recirculation ids, e.g: > > recirc(0),eth(...),ipv4(...) actions=ct,recirc(0x42) > \-> recirc(42),ct_state(0/0),eth(...),ipv4(...) actions=1 > \-> recirc(42),ct_state(1/0),eth(...),ipv4(...) actions=userspace(...) > > This patch adds support for building such logical datapath trees in a > format-agnostic way and adds support for console-based formatting > supporting: > - head-maps formatting of statistics > - hash-based pallete of recirculation ids: each recirculation id is > assigned a unique color to easily follow the sequence of related > actions. > - full-tree filtering: if a user specifies a filter, an entire subtree > is filtered out if none of its branches satisfy it. > > Signed-off-by: Adrian Moreno <amore...@redhat.com>
This patch looks good to me. One small nit on a comment not ending with a dot. Acked-by: Eelco Chaudron <echau...@redhat.com> > --- > python/automake.mk | 1 + > python/ovs/flowviz/console.py | 22 +++ > python/ovs/flowviz/odp/cli.py | 21 ++- > python/ovs/flowviz/odp/tree.py | 290 +++++++++++++++++++++++++++++++++ > 4 files changed, 332 insertions(+), 2 deletions(-) > create mode 100644 python/ovs/flowviz/odp/tree.py > > diff --git a/python/automake.mk b/python/automake.mk > index b4c1f84be..5050089e9 100644 > --- a/python/automake.mk > +++ b/python/automake.mk > @@ -71,6 +71,7 @@ ovs_flowviz = \ > python/ovs/flowviz/main.py \ > python/ovs/flowviz/odp/__init__.py \ > python/ovs/flowviz/odp/cli.py \ > + python/ovs/flowviz/odp/tree.py \ > python/ovs/flowviz/ofp/__init__.py \ > python/ovs/flowviz/ofp/cli.py \ > python/ovs/flowviz/ofp/html.py \ > diff --git a/python/ovs/flowviz/console.py b/python/ovs/flowviz/console.py > index 5b4b047c2..2d65f9bb6 100644 > --- a/python/ovs/flowviz/console.py > +++ b/python/ovs/flowviz/console.py > @@ -13,6 +13,9 @@ > # limitations under the License. > > import colorsys > +import itertools > +import zlib > + > from rich.console import Console > from rich.text import Text > from rich.style import Style > @@ -169,6 +172,25 @@ def heat_pallete(min_value, max_value): > return heat > > > +def hash_pallete(hue, saturation, value): > + """Generates a color pallete with the cartesian product > + of the hsv values provided and returns a callable that assigns a color > for > + each value hash > + """ > + HSV_tuples = itertools.product(hue, saturation, value) > + RGB_tuples = map(lambda x: colorsys.hsv_to_rgb(*x), HSV_tuples) > + styles = [ > + Style(color=Color.from_rgb(r * 255, g * 255, b * 255)) > + for r, g, b in RGB_tuples > + ] > + > + def get_style(string): > + hash_val = zlib.crc32(bytes(str(string), "utf-8")) > + return styles[hash_val % len(styles)] > + > + return get_style > + > + > def default_highlight(): > """Generates a default style for highlights.""" > return Style(underline=True) > diff --git a/python/ovs/flowviz/odp/cli.py b/python/ovs/flowviz/odp/cli.py > index 78f5cfff4..4740e753e 100644 > --- a/python/ovs/flowviz/odp/cli.py > +++ b/python/ovs/flowviz/odp/cli.py > @@ -13,12 +13,12 @@ > # limitations under the License. > > import click > - > from ovs.flowviz.main import maincli > +from ovs.flowviz.odp.tree import ConsoleTreeProcessor > from ovs.flowviz.process import ( > DatapathFactory, > - JSONProcessor, > ConsoleProcessor, > + JSONProcessor, > ) > > > @@ -65,3 +65,20 @@ def console(opts, heat_map): > ) > proc.process() > proc.print() > + > + > +@datapath.command() > +@click.option( > + "-h", > + "--heat-map", > + is_flag=True, > + default=False, > + show_default=True, > + help="Create heat-map with packet and byte counters", > +) > +@click.pass_obj > +def tree(opts, heat_map): > + """Print the flows in a tree based on the 'recirc_id'.""" > + processor = ConsoleTreeProcessor(opts) > + processor.process() > + processor.print(heat_map) > diff --git a/python/ovs/flowviz/odp/tree.py b/python/ovs/flowviz/odp/tree.py > new file mode 100644 > index 000000000..cfddb162e > --- /dev/null > +++ b/python/ovs/flowviz/odp/tree.py > @@ -0,0 +1,290 @@ > +# Copyright (c) 2023 Red Hat, Inc. > +# > +# Licensed under the Apache License, Version 2.0 (the "License"); > +# you may not use this file except in compliance with the License. > +# You may obtain a copy of the License at: > +# > +# http://www.apache.org/licenses/LICENSE-2.0 > +# > +# Unless required by applicable law or agreed to in writing, software > +# distributed under the License is distributed on an "AS IS" BASIS, > +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. > +# See the License for the specific language governing permissions and > +# limitations under the License. > + > +from rich.style import Style > +from rich.text import Text > +from rich.tree import Tree > + > +from ovs.flowviz.console import ( > + ConsoleFormatter, > + ConsoleBuffer, > + hash_pallete, > + heat_pallete, > + file_header, > +) > +from ovs.flowviz.process import ( > + DatapathFactory, > + FileProcessor, > +) > + > + > +class TreeElem: > + """Element in the tree. > + Args: > + children (list[TreeElem]): Optional, list of children > + is_root (bool): Optional; whether this is the root elemen > + """ > + > + def __init__(self, children=None, is_root=False): > + self.children = children or list() > + self.is_root = is_root > + > + def append(self, child): > + self.children.append(child) > + > + > +class FlowElem(TreeElem): > + """An element that contains a flow. > + Args: > + flow (Flow): The flow that this element contains > + children (list[TreeElem]): Optional, list of children > + is_root (bool): Optional; whether this is the root elemen > + """ > + > + def __init__(self, flow, children=None, is_root=False): > + self.flow = flow > + super(FlowElem, self).__init__(children, is_root) > + > + def evaluate_any(self, filter): > + """Evaluate the filter on the element and all its children. > + Args: > + filter(OFFilter): the filter to evaluate > + > + Returns: > + True if ANY of the flows (including self and children) evaluates > + true > + """ > + if filter.evaluate(self.flow): > + return True > + > + return any([child.evaluate_any(filter) for child in self.children]) > + > + > +class FlowTree: > + """A Flow tree is a a class that processes datapath flows into a tree > based > + on recirculation ids. > + > + Args: > + flows (list[ODPFlow]): Optional, initial list of flows > + root (TreeElem): Optional, root of the tree. > + """ > + > + def __init__(self, flows=None, root=TreeElem(is_root=True)): > + self._flows = {} > + self.root = root > + if flows: > + for flow in flows: > + self.add(flow) > + > + def add(self, flow): > + """Add a flow""" > + rid = flow.match.get("recirc_id") or 0 > + if not self._flows.get(rid): > + self._flows[rid] = list() > + self._flows[rid].append(flow) > + > + def build(self): > + """Build the flow tree.""" > + self._build(self.root, 0) > + > + def traverse(self, callback): > + """Traverses the tree calling callback on each element. > + > + callback: callable that accepts two TreeElem, the current one being > + traversed and its parent > + func callback(elem, parent): > + ... > + Note that "parent" can be None if it's the first element. > + """ > + self._traverse(self.root, None, callback) > + > + def _traverse(self, elem, parent, callback): > + callback(elem, parent) > + > + for child in elem.children: > + self._traverse(child, elem, callback) > + > + def _build(self, parent, recirc): > + """Build the subtree starting at a specific recirc_id. Recursive > function. > + > + Args: > + parent (TreeElem): parent of the (sub)tree > + recirc(int): the recirc_id subtree to build > + """ > + flows = self._flows.get(recirc) > + if not flows: > + return > + for flow in sorted( > + flows, key=lambda x: x.info.get("packets") or 0, reverse=True > + ): > + next_recircs = self._get_next_recirc(flow) > + > + elem = self._new_elem(flow, parent) > + parent.append(elem) > + > + for next_recirc in next_recircs: > + self._build(elem, next_recirc) > + > + def _get_next_recirc(self, flow): > + """Get the next recirc_ids from a Flow. > + > + The recirc_id is obtained from actions such as recirc, but also > + complex actions such as check_pkt_len and sample > + Args: > + flow (ODPFlow): flow to get the recirc_id from. > + Returns: > + set of next recirculation ids. > + """ > + > + # Helper function to find a recirc in a dictionary of actions. > + def find_in_list(actions_list): > + recircs = [] > + for item in actions_list: > + (action, value) = next(iter(item.items())) > + if action == "recirc": > + recircs.append(value) > + elif action == "check_pkt_len": > + recircs.extend(find_in_list(value.get("gt"))) > + recircs.extend(find_in_list(value.get("le"))) > + elif action == "clone": > + recircs.extend(find_in_list(value)) > + elif action == "sample": > + recircs.extend(find_in_list(value.get("actions"))) > + return recircs > + > + recircs = [] > + recircs.extend(find_in_list(flow.actions)) > + > + return set(recircs) > + > + def _new_elem(self, flow, _): > + """Creates a new TreeElem. > + > + Default implementation is to create a FlowElem. Derived classes can > + override this method to return any derived TreeElem > + """ > + return FlowElem(flow) > + > + def filter(self, filter): > + """Removes the first level subtrees if none of its sub-elements match > + the filter. > + > + Args: > + filter(OFFilter): filter to apply > + """ > + to_remove = list() > + for l0 in self.root.children: > + passes = l0.evaluate_any(filter) > + if not passes: > + to_remove.append(l0) > + for elem in to_remove: > + self.root.children.remove(elem) > + > + > +class ConsoleTreeProcessor(DatapathFactory, FileProcessor): > + def __init__(self, opts): > + super().__init__(opts) > + self.data = dict() > + self.ofconsole = ConsoleFormatter(self.opts) > + > + # Generate a color pallete for cookies Ending comment line with a dot? > + recirc_style_gen = hash_pallete( > + hue=[x / 50 for x in range(0, 50)], saturation=[0.7], value=[0.8] > + ) > + > + style = self.ofconsole.style > + style.set_default_value_style(Style(color="grey66")) > + style.set_key_style("output", Style(color="green")) > + style.set_value_style("output", Style(color="green")) > + style.set_value_style("recirc", recirc_style_gen) > + style.set_value_style("recirc_id", recirc_style_gen) > + > + def start_file(self, name, filename): > + self.tree = ConsoleTree(self.ofconsole, self.opts) > + > + def process_flow(self, flow, name): > + self.tree.add(flow) > + > + def process(self): > + super().process(False) > + > + def stop_file(self, name, filename): > + self.data[name] = self.tree > + > + def print(self, heat_map): > + for name, tree in self.data.items(): > + self.ofconsole.console.print("\n") > + self.ofconsole.console.print(file_header(name)) > + tree.build() > + if self.opts.get("filter"): > + tree.filter(self.opts.get("filter")) > + tree.print(heat_map) > + > + > +class ConsoleTree(FlowTree): > + """ConsoleTree is a FlowTree that prints the tree in the console. > + > + Args: > + console (ConsoleFormatter): console to use for printing > + opts (dict): Options dictionary > + """ > + > + class ConsoleElem(FlowElem): > + def __init__(self, flow=None, is_root=False): > + self.tree = None > + super(ConsoleTree.ConsoleElem, self).__init__( > + flow, is_root=is_root > + ) > + > + def __init__(self, console, opts): > + self.console = console > + self.opts = opts > + super(ConsoleTree, > self).__init__(root=self.ConsoleElem(is_root=True)) > + > + def _new_elem(self, flow, _): > + """Override _new_elem to provide ConsoleElems""" > + return self.ConsoleElem(flow) > + > + def _append_to_tree(self, elem, parent): > + """Callback to be used for FlowTree._build > + Appends the flow to the rich.Tree > + """ > + if elem.is_root: > + elem.tree = Tree("Datapath Flows (logical)") > + return > + > + buf = ConsoleBuffer(Text()) > + highlighted = None > + if self.opts.get("highlight"): > + result = self.opts.get("highlight").evaluate(elem.flow) > + if result: > + highlighted = result.kv > + self.console.format_flow(buf, elem.flow, highlighted) > + elem.tree = parent.tree.add(buf.text) > + > + def print(self, heat=False): > + """Print the Flow Tree. > + Args: > + heat (bool): Optional; whether heat-map style shall be applied > + """ > + if heat: > + for field in ["packets", "bytes"]: > + values = [] > + for flow_list in self._flows.values(): > + values.extend([f.info.get(field) or 0 for f in > flow_list]) > + self.console.style.set_value_style( > + field, heat_pallete(min(values), max(values)) > + ) > + self.traverse(self._append_to_tree) > + self.console.console.print(self.root.tree) > -- > 2.43.0 > > _______________________________________________ > dev mailing list > d...@openvswitch.org > https://mail.openvswitch.org/mailman/listinfo/ovs-dev < _______________________________________________ dev mailing list d...@openvswitch.org https://mail.openvswitch.org/mailman/listinfo/ovs-dev