Message ID | 20231201191449.2386134-7-amorenoz@redhat.com |
---|---|
State | RFC |
Delegated to: | Simon Horman |
Headers | show |
Series | Add flow visualization utility | expand |
Context | Check | Description |
---|---|---|
ovsrobot/apply-robot | success | apply and check: success |
ovsrobot/intel-ovs-compilation | fail | test: fail |
ovsrobot/github-robot-_Build_and_Test | fail | github build: failed |
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 <amorenoz@redhat.com> This patch looks good to me. One small nit on a comment not ending with a dot. Acked-by: Eelco Chaudron <echaudro@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 > dev@openvswitch.org > https://mail.openvswitch.org/mailman/listinfo/ovs-dev
On 1/30/24 16:53, Eelco Chaudron wrote: > 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 <amorenoz@redhat.com> > > This patch looks good to me. One small nit on a comment not ending with a dot. > > Acked-by: Eelco Chaudron <echaudro@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? > > Good catch! Thanks. >> + 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 >> dev@openvswitch.org >> https://mail.openvswitch.org/mailman/listinfo/ovs-dev >
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 + 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)
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 <amorenoz@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