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

Reply via email to