@@ -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 \
@@ -13,6 +13,8 @@
# limitations under the License.
import colorsys
+import itertools
+import zlib
from rich.console import Console
from rich.color import Color
@@ -170,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)
@@ -13,8 +13,8 @@
# limitations under the License.
import click
-
from ovs.flowviz.main import maincli
+from ovs.flowviz.odp.tree import ConsoleTreeProcessor
from ovs.flowviz.process import (
ConsoleProcessor,
DatapathFactory,
@@ -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)
new file mode 100644
@@ -0,0 +1,291 @@
+# 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.
+ 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)