---
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