Message ID | 20240710170504.2162803-10-amorenoz@redhat.com |
---|---|
State | Changes Requested |
Headers | show |
Series | Add flow visualization utility. | expand |
Context | Check | Description |
---|---|---|
ovsrobot/apply-robot | success | apply and check: success |
ovsrobot/github-robot-_Build_and_Test | success | github build: passed |
ovsrobot/intel-ovs-compilation | fail | test: fail |
On 10 Jul 2024, at 19:04, Adrian Moreno wrote: > This view is interesting for debugging the logical pipeline. It arranges > the flows in "logical" groups (not to be confused with OVN's > Logical_Flows). A logical group of flows is a set of flows that: > - Have the same table number and priority > - Match on the same fields (regardless of the value they match against) > - Have the same actions, regardless of the arguments for those actions, > except for output and recirc, for which arguments do care. > > Optionally, the cookie can also be force to be unique for the logical force -> forced > group. By doing so, we can extend the information we show by querying an > external OVN database and running "ovn-detrace" on each cookie. The > result is a compact list of flow groups with interlieved OVN interlieved -> interleaved. > information. > > Furthermore, if connected to an OVN database, we can apply an OVN > regexp filter. > > Examples: > $ ovs-ofctl dump-flows br-int | ovs-flowviz openflow logic > $ ovs-ofctl dump-flows br-int | ovs-flowviz openflow logic -s -h > $ export OVN_NB_DB=... > $ export OVN_SB_DB=... > $ ovs-ofctl dump-flows br-int | ovs-flowviz openflow logic -d > $ ovs-ofctl dump-flows br-int | ovs-flowviz openflow logic -d > --ovn-filter="acl.*icmp4" > > Acked-by: Eelco Chaudron <echaudro@redhat.com> > Signed-off-by: Adrian Moreno <amorenoz@redhat.com> > --- Thanks for sending the v5, the changes look good to me with one small spelling error above/below. You can add my ack on the next rebase version if fixed. //Eelco Acked-by: Eelco Chaudron <echaudro@redhat.com> > python/automake.mk | 1 + > python/ovs/flowviz/ofp/cli.py | 113 ++++++++++++ > python/ovs/flowviz/ofp/logic.py | 303 ++++++++++++++++++++++++++++++++ > 3 files changed, 417 insertions(+) > create mode 100644 python/ovs/flowviz/ofp/logic.py > > diff --git a/python/automake.mk b/python/automake.mk > index b3fef9bed..9640b5886 100644 > --- a/python/automake.mk > +++ b/python/automake.mk > @@ -74,6 +74,7 @@ ovs_flowviz = \ > python/ovs/flowviz/odp/tree.py \ > python/ovs/flowviz/ofp/__init__.py \ > python/ovs/flowviz/ofp/cli.py \ > + python/ovs/flowviz/ofp/logic.py \ > python/ovs/flowviz/ofp/html.py \ > python/ovs/flowviz/ovs-flowviz \ > python/ovs/flowviz/process.py > diff --git a/python/ovs/flowviz/ofp/cli.py b/python/ovs/flowviz/ofp/cli.py > index 28f3873b7..a0a94bd3b 100644 > --- a/python/ovs/flowviz/ofp/cli.py > +++ b/python/ovs/flowviz/ofp/cli.py > @@ -12,10 +12,13 @@ > # See the License for the specific language governing permissions and > # limitations under the License. > > +import os > + > import click > > from ovs.flowviz.main import maincli > from ovs.flowviz.ofp.html import HTMLProcessor > +from ovs.flowviz.ofp.logic import LogicFlowProcessor > from ovs.flowviz.process import ( > ConsoleProcessor, > JSONOpenFlowProcessor, > @@ -59,6 +62,116 @@ def console(opts, heat_map): > proc.print() > > > +def ovn_detrace_callback(ctx, param, value): > + """click callback to add detrace information to config object and > + set general ovn-detrace flag to True > + """ > + ctx.obj[param.name] = value > + if value != param.default: > + ctx.obj["ovn_detrace_flag"] = True > + return value > + > + > +@openflow.command() > +@click.option( > + "-d", > + "--ovn-detrace", > + "ovn_detrace_flag", > + is_flag=True, > + show_default=True, > + help="Use ovn-detrace to extract cookie information (implies '-c')", > +) > +@click.option( > + "--ovn-detrace-path", > + default="/usr/bin", > + type=click.Path(), > + help="Use an alternative path to where ovn_detrace.py is located. " > + "Instead of using this option you can just set PYTHONPATH accordingly.", > + show_default=True, > + callback=ovn_detrace_callback, > +) > +@click.option( > + "--ovnnb-db", > + default=os.getenv("OVN_NB_DB") or "unix:/var/run/ovn/ovnnb_db.sock", > + help="Specify the OVN NB database string (implies -d). " > + "If the OVN_NB_DB environment variable is set, it's used as default. " > + "Otherwise, the default is unix:/var/run/ovn/ovnnb_db.sock", > + callback=ovn_detrace_callback, > +) > +@click.option( > + "--ovnsb-db", > + default=os.getenv("OVN_SB_DB") or "unix:/var/run/ovn/ovnsb_db.sock", > + help="Specify the OVN NB database string (implies -d). " > + "If the OVN_NB_DB environment variable is set, it's used as default. " > + "Otherwise, the default is unix:/var/run/ovn/ovnnb_db.sock", > + callback=ovn_detrace_callback, > +) > +@click.option( > + "-o", > + "--ovn-filter", > + help="Specify a filter to be run on ovn-detrace information (implied -d). " > + "Format: python regular expression " > + "(see https://docs.python.org/3/library/re.html)", > + callback=ovn_detrace_callback, > +) > +@click.option( > + "-s", > + "--show-flows", > + is_flag=True, > + default=False, > + show_default=True, > + help="Show the full flows under each logical flow", > +) > +@click.option( > + "-c", > + "--cookie", > + "cookie_flag", > + is_flag=True, > + default=False, > + show_default=True, > + help="Consider the cookie in the logical flow", > +) > +@click.option( > + "-h", > + "--heat-map", > + is_flag=True, > + default=False, > + show_default=True, > + help="Create heat-map with packet and byte counters (when -s is used)", > +) > +@click.pass_obj > +def logic( > + opts, > + ovn_detrace_flag, > + ovn_detrace_path, > + ovnnb_db, > + ovnsb_db, > + ovn_filter, > + show_flows, > + cookie_flag, > + heat_map, > +): > + """ > + Print the logical structure of the flows. > + > + First, sorts the flows based on tables and priorities. > + Then, deduplicates logically equivalent flows: these a flows that match > + on the same set of fields (regardless of the values they match against), > + have the same priority, and actions (regardless of action arguments, > + except in the case of output and recirculate). > + Optionally, the cookie can also be considered to be part of the logical > + flow. > + """ > + if ovn_detrace_flag: > + opts["ovn_detrace_flag"] = True > + if opts.get("ovn_detrace_flag"): > + cookie_flag = True > + > + processor = LogicFlowProcessor(opts, cookie_flag, heat_map) > + processor.process() > + processor.print(show_flows) > + > + > @openflow.command() > @click.pass_obj > def html(opts): > diff --git a/python/ovs/flowviz/ofp/logic.py b/python/ovs/flowviz/ofp/logic.py > new file mode 100644 > index 000000000..dd5c29c5a > --- /dev/null > +++ b/python/ovs/flowviz/ofp/logic.py > @@ -0,0 +1,303 @@ > +# 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. > + > +import sys > +import io > +import re > + > +from rich.tree import Tree > +from rich.text import Text > + > +from ovs.flowviz.process import FileProcessor > +from ovs.flowviz.console import ( > + ConsoleFormatter, > + ConsoleBuffer, > + hash_pallete, > + file_header, > + heat_pallete, > +) > + > + > +class LFlow: > + """A Logical Flow represents the scheleton of a flow. scheleton -> skeleton > + > + Two logical flows have the same logical representation if they match > + against same fields (regardless of the matching value) and have the same against same fields -> against the same fields > + set of actions (regardless of the actions' arguments, except for those > + in the exact_actions list). > + > + Attributes: > + flow (OFPFlow): The flow > + exact_actions(list): Optional; list of action keys that are > + considered unique if the value is also the same. > + match_cookie (bool): Optional; if cookies are part of the logical > + flow > + """ > + > + def __init__(self, flow, exact_actions=[], match_cookie=False): > + self.cookie = flow.info.get("cookie") or 0 if match_cookie else None > + self.priority = flow.match.get("priority") or 0 > + self.match_keys = tuple([kv.key for kv in flow.match_kv]) > + > + self.action_keys = tuple( > + [ > + kv.key > + for kv in flow.actions_kv > + if kv.key not in exact_actions > + ] > + ) > + self.match_action_kvs = [ > + kv for kv in flow.actions_kv if kv.key in exact_actions > + ] > + > + def __eq__(self, other): > + return ( > + (self.cookie == other.cookie if self.cookie else True) > + and self.priority == other.priority > + and self.action_keys == other.action_keys > + and self.equal_match_action_kvs(other) > + and self.match_keys == other.match_keys > + ) > + > + def equal_match_action_kvs(self, other): > + """ Compares the logical flow's match action key-values with the > + others. > + > + Args: > + other (LFlow): The other LFlow to compare against > + > + Returns true if both LFlow have the same action k-v. > + """ > + if len(other.match_action_kvs) != len(self.match_action_kvs): > + return False > + > + for kv in self.match_action_kvs: > + found = False > + for other_kv in other.match_action_kvs: > + if self.match_kv(kv, other_kv): > + found = True > + break > + if not found: > + return False > + return True > + > + def match_kv(self, one, other): > + """Compares a KeyValue. > + Args: > + one, other (KeyValue): The objects to compare > + > + Returns true if both KeyValue objects have the same key and value > + """ > + return one.key == other.key and one.value == other.value > + > + def __hash__(self): > + hash_data = [ > + self.cookie, > + self.priority, > + self.action_keys, > + tuple((kv.key, str(kv.value)) for kv in self.match_action_kvs), > + self.match_keys, > + ] > + if self.cookie: > + hash_data.append(self.cookie) > + return tuple(hash_data).__hash__() > + > + def format(self, buf, formatter): > + """Format the Logical Flow into a Buffer.""" > + if self.cookie: > + buf.append_extra( > + "cookie={} ".format(hex(self.cookie)).ljust(18), > + style=cookie_style_gen(str(self.cookie)), > + ) > + > + buf.append_extra( > + "priority={} ".format(self.priority), style="steel_blue" > + ) > + buf.append_extra(",".join(self.match_keys), style="steel_blue") > + buf.append_extra(" ---> ", style="bold magenta") > + buf.append_extra(",".join(self.action_keys), style="steel_blue") > + > + if len(self.match_action_kvs) > 0: > + buf.append_extra(" ", style=None) > + > + for kv in self.match_action_kvs: > + formatter.format_kv(buf, kv, formatter.style) > + buf.append_extra(",", style=None) > + > + > +class LogicFlowProcessor(FileProcessor): > + def __init__(self, opts, match_cookie, heat_map): > + super().__init__(opts, "ofp") > + self.data = dict() > + self.min_max = dict() > + self.match_cookie = match_cookie > + self.heat_map = ["n_packets", "n_bytes"] if heat_map else [] > + self.ovn_detrace = ( > + OVNDetrace(opts) if opts.get("ovn_detrace_flag") else None > + ) > + > + def start_file(self, name, filename): > + if len(self.heat_map) > 0: > + self.min = [-1] * len(self.heat_map) > + self.max = [0] * len(self.heat_map) > + self.tables = dict() > + > + def stop_file(self, name, filename): > + if len(self.heat_map) > 0: > + self.min_max[name] = (self.min, self.max) > + self.data[name] = self.tables > + > + def process_flow(self, flow, name): > + """Sort the flows by table and logical flow.""" > + # Running calculation of min and max values for all the fields that > + # take place in the heatmap. > + for i, field in enumerate(self.heat_map): > + val = flow.info.get(field) > + if self.min[i] == -1 or val < self.min[i]: > + self.min[i] = val > + if val > self.max[i]: > + self.max[i] = val > + > + table = flow.info.get("table") or 0 > + if not self.tables.get(table): > + self.tables[table] = dict() > + > + # Group flows by logical hash > + lflow = LFlow( > + flow, > + exact_actions=["output", "resubmit", "drop"], > + match_cookie=self.match_cookie, > + ) > + > + if not self.tables[table].get(lflow): > + self.tables[table][lflow] = list() > + > + self.tables[table][lflow].append(flow) > + > + def print(self, show_flows): > + formatter = ConsoleFormatter(opts=self.opts) > + console = formatter.console > + for name, tables in self.data.items(): > + console.print("\n") > + console.print(file_header(name)) > + tree = Tree("Ofproto Flows (logical)") > + > + for table_num in sorted(tables.keys()): > + table = tables[table_num] > + table_tree = tree.add("** TABLE {} **".format(table_num)) > + > + if len(self.heat_map) > 0 and len(table.values()) > 0: > + for i, field in enumerate(self.heat_map): > + (min_val, max_val) = self.min_max[name][i] > + formatter.style.set_value_style( > + field, heat_pallete(min_val, max_val) > + ) > + > + for lflow in sorted( > + table.keys(), > + key=(lambda x: x.priority), > + reverse=True, > + ): > + flows = table[lflow] > + ovn_info = None > + if self.ovn_detrace: > + ovn_info = self.ovn_detrace.get_ovn_info(lflow.cookie) > + if self.opts.get("ovn_filter"): > + ovn_regexp = re.compile( > + self.opts.get("ovn_filter") > + ) > + if not ovn_regexp.search(ovn_info): > + continue > + > + buf = ConsoleBuffer(Text()) > + > + lflow.format(buf, formatter) > + buf.append_extra( > + " ( x {} )".format(len(flows)), > + style="dark_olive_green3", > + ) > + lflow_tree = table_tree.add(buf.text) > + > + if ovn_info: > + ovn = lflow_tree.add("OVN Info") > + for part in ovn_info.split("\n"): > + if part.strip(): > + ovn.add(part.strip()) > + > + if show_flows: > + for flow in flows: > + buf = ConsoleBuffer(Text()) > + highlighted = None > + if self.opts.get("highlight"): > + result = self.opts.get("highlight").evaluate( > + flow > + ) > + if result: > + highlighted = result.kv > + formatter.format_flow(buf, flow, highlighted) > + lflow_tree.add(buf.text) > + > + console.print(tree) > + > + > +class OVNDetrace(object): > + def __init__(self, opts): > + if not opts.get("ovn_detrace_flag"): > + raise Exception("Cannot initialize OVN Detrace connection") > + > + if opts.get("ovn_detrace_path"): > + sys.path.append(opts.get("ovn_detrace_path")) > + > + import ovn_detrace > + > + class FakePrinter(ovn_detrace.Printer): > + def __init__(self): > + self.buff = io.StringIO() > + > + def print_p(self, msg): > + print(" * ", msg, file=self.buff) > + > + def print_h(self, msg): > + print(" * ", msg, file=self.buff) > + > + def clear(self): > + self.buff = io.StringIO() > + > + self.ovn_detrace = ovn_detrace > + self.ovnnb_conn = ovn_detrace.OVSDB( > + opts.get("ovnnb_db"), "OVN_Northbound" > + ) > + self.ovnsb_conn = ovn_detrace.OVSDB( > + opts.get("ovnsb_db"), "OVN_Southbound" > + ) > + self.ovn_printer = FakePrinter() > + self.cookie_handlers = ovn_detrace.get_cookie_handlers( > + self.ovnnb_conn, self.ovnsb_conn, self.ovn_printer > + ) > + > + def get_ovn_info(self, cookie): > + self.ovn_printer.clear() > + self.ovn_detrace.print_record_from_cookie( > + self.ovnsb_conn, self.cookie_handlers, "{:x}".format(cookie) > + ) > + return self.ovn_printer.buff.getvalue() > + > + > +# Try to make it easy to spot same cookies by printing them in different > +# colors > +cookie_style_gen = hash_pallete( > + hue=[x / 10 for x in range(0, 10)], > + saturation=[0.5], > + value=[0.5 + x / 10 * (0.85 - 0.5) for x in range(0, 10)], > +) > -- > 2.45.2
diff --git a/python/automake.mk b/python/automake.mk index b3fef9bed..9640b5886 100644 --- a/python/automake.mk +++ b/python/automake.mk @@ -74,6 +74,7 @@ ovs_flowviz = \ python/ovs/flowviz/odp/tree.py \ python/ovs/flowviz/ofp/__init__.py \ python/ovs/flowviz/ofp/cli.py \ + python/ovs/flowviz/ofp/logic.py \ python/ovs/flowviz/ofp/html.py \ python/ovs/flowviz/ovs-flowviz \ python/ovs/flowviz/process.py diff --git a/python/ovs/flowviz/ofp/cli.py b/python/ovs/flowviz/ofp/cli.py index 28f3873b7..a0a94bd3b 100644 --- a/python/ovs/flowviz/ofp/cli.py +++ b/python/ovs/flowviz/ofp/cli.py @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + import click from ovs.flowviz.main import maincli from ovs.flowviz.ofp.html import HTMLProcessor +from ovs.flowviz.ofp.logic import LogicFlowProcessor from ovs.flowviz.process import ( ConsoleProcessor, JSONOpenFlowProcessor, @@ -59,6 +62,116 @@ def console(opts, heat_map): proc.print() +def ovn_detrace_callback(ctx, param, value): + """click callback to add detrace information to config object and + set general ovn-detrace flag to True + """ + ctx.obj[param.name] = value + if value != param.default: + ctx.obj["ovn_detrace_flag"] = True + return value + + +@openflow.command() +@click.option( + "-d", + "--ovn-detrace", + "ovn_detrace_flag", + is_flag=True, + show_default=True, + help="Use ovn-detrace to extract cookie information (implies '-c')", +) +@click.option( + "--ovn-detrace-path", + default="/usr/bin", + type=click.Path(), + help="Use an alternative path to where ovn_detrace.py is located. " + "Instead of using this option you can just set PYTHONPATH accordingly.", + show_default=True, + callback=ovn_detrace_callback, +) +@click.option( + "--ovnnb-db", + default=os.getenv("OVN_NB_DB") or "unix:/var/run/ovn/ovnnb_db.sock", + help="Specify the OVN NB database string (implies -d). " + "If the OVN_NB_DB environment variable is set, it's used as default. " + "Otherwise, the default is unix:/var/run/ovn/ovnnb_db.sock", + callback=ovn_detrace_callback, +) +@click.option( + "--ovnsb-db", + default=os.getenv("OVN_SB_DB") or "unix:/var/run/ovn/ovnsb_db.sock", + help="Specify the OVN NB database string (implies -d). " + "If the OVN_NB_DB environment variable is set, it's used as default. " + "Otherwise, the default is unix:/var/run/ovn/ovnnb_db.sock", + callback=ovn_detrace_callback, +) +@click.option( + "-o", + "--ovn-filter", + help="Specify a filter to be run on ovn-detrace information (implied -d). " + "Format: python regular expression " + "(see https://docs.python.org/3/library/re.html)", + callback=ovn_detrace_callback, +) +@click.option( + "-s", + "--show-flows", + is_flag=True, + default=False, + show_default=True, + help="Show the full flows under each logical flow", +) +@click.option( + "-c", + "--cookie", + "cookie_flag", + is_flag=True, + default=False, + show_default=True, + help="Consider the cookie in the logical flow", +) +@click.option( + "-h", + "--heat-map", + is_flag=True, + default=False, + show_default=True, + help="Create heat-map with packet and byte counters (when -s is used)", +) +@click.pass_obj +def logic( + opts, + ovn_detrace_flag, + ovn_detrace_path, + ovnnb_db, + ovnsb_db, + ovn_filter, + show_flows, + cookie_flag, + heat_map, +): + """ + Print the logical structure of the flows. + + First, sorts the flows based on tables and priorities. + Then, deduplicates logically equivalent flows: these a flows that match + on the same set of fields (regardless of the values they match against), + have the same priority, and actions (regardless of action arguments, + except in the case of output and recirculate). + Optionally, the cookie can also be considered to be part of the logical + flow. + """ + if ovn_detrace_flag: + opts["ovn_detrace_flag"] = True + if opts.get("ovn_detrace_flag"): + cookie_flag = True + + processor = LogicFlowProcessor(opts, cookie_flag, heat_map) + processor.process() + processor.print(show_flows) + + @openflow.command() @click.pass_obj def html(opts): diff --git a/python/ovs/flowviz/ofp/logic.py b/python/ovs/flowviz/ofp/logic.py new file mode 100644 index 000000000..dd5c29c5a --- /dev/null +++ b/python/ovs/flowviz/ofp/logic.py @@ -0,0 +1,303 @@ +# 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. + +import sys +import io +import re + +from rich.tree import Tree +from rich.text import Text + +from ovs.flowviz.process import FileProcessor +from ovs.flowviz.console import ( + ConsoleFormatter, + ConsoleBuffer, + hash_pallete, + file_header, + heat_pallete, +) + + +class LFlow: + """A Logical Flow represents the scheleton of a flow. + + Two logical flows have the same logical representation if they match + against same fields (regardless of the matching value) and have the same + set of actions (regardless of the actions' arguments, except for those + in the exact_actions list). + + Attributes: + flow (OFPFlow): The flow + exact_actions(list): Optional; list of action keys that are + considered unique if the value is also the same. + match_cookie (bool): Optional; if cookies are part of the logical + flow + """ + + def __init__(self, flow, exact_actions=[], match_cookie=False): + self.cookie = flow.info.get("cookie") or 0 if match_cookie else None + self.priority = flow.match.get("priority") or 0 + self.match_keys = tuple([kv.key for kv in flow.match_kv]) + + self.action_keys = tuple( + [ + kv.key + for kv in flow.actions_kv + if kv.key not in exact_actions + ] + ) + self.match_action_kvs = [ + kv for kv in flow.actions_kv if kv.key in exact_actions + ] + + def __eq__(self, other): + return ( + (self.cookie == other.cookie if self.cookie else True) + and self.priority == other.priority + and self.action_keys == other.action_keys + and self.equal_match_action_kvs(other) + and self.match_keys == other.match_keys + ) + + def equal_match_action_kvs(self, other): + """ Compares the logical flow's match action key-values with the + others. + + Args: + other (LFlow): The other LFlow to compare against + + Returns true if both LFlow have the same action k-v. + """ + if len(other.match_action_kvs) != len(self.match_action_kvs): + return False + + for kv in self.match_action_kvs: + found = False + for other_kv in other.match_action_kvs: + if self.match_kv(kv, other_kv): + found = True + break + if not found: + return False + return True + + def match_kv(self, one, other): + """Compares a KeyValue. + Args: + one, other (KeyValue): The objects to compare + + Returns true if both KeyValue objects have the same key and value + """ + return one.key == other.key and one.value == other.value + + def __hash__(self): + hash_data = [ + self.cookie, + self.priority, + self.action_keys, + tuple((kv.key, str(kv.value)) for kv in self.match_action_kvs), + self.match_keys, + ] + if self.cookie: + hash_data.append(self.cookie) + return tuple(hash_data).__hash__() + + def format(self, buf, formatter): + """Format the Logical Flow into a Buffer.""" + if self.cookie: + buf.append_extra( + "cookie={} ".format(hex(self.cookie)).ljust(18), + style=cookie_style_gen(str(self.cookie)), + ) + + buf.append_extra( + "priority={} ".format(self.priority), style="steel_blue" + ) + buf.append_extra(",".join(self.match_keys), style="steel_blue") + buf.append_extra(" ---> ", style="bold magenta") + buf.append_extra(",".join(self.action_keys), style="steel_blue") + + if len(self.match_action_kvs) > 0: + buf.append_extra(" ", style=None) + + for kv in self.match_action_kvs: + formatter.format_kv(buf, kv, formatter.style) + buf.append_extra(",", style=None) + + +class LogicFlowProcessor(FileProcessor): + def __init__(self, opts, match_cookie, heat_map): + super().__init__(opts, "ofp") + self.data = dict() + self.min_max = dict() + self.match_cookie = match_cookie + self.heat_map = ["n_packets", "n_bytes"] if heat_map else [] + self.ovn_detrace = ( + OVNDetrace(opts) if opts.get("ovn_detrace_flag") else None + ) + + def start_file(self, name, filename): + if len(self.heat_map) > 0: + self.min = [-1] * len(self.heat_map) + self.max = [0] * len(self.heat_map) + self.tables = dict() + + def stop_file(self, name, filename): + if len(self.heat_map) > 0: + self.min_max[name] = (self.min, self.max) + self.data[name] = self.tables + + def process_flow(self, flow, name): + """Sort the flows by table and logical flow.""" + # Running calculation of min and max values for all the fields that + # take place in the heatmap. + for i, field in enumerate(self.heat_map): + val = flow.info.get(field) + if self.min[i] == -1 or val < self.min[i]: + self.min[i] = val + if val > self.max[i]: + self.max[i] = val + + table = flow.info.get("table") or 0 + if not self.tables.get(table): + self.tables[table] = dict() + + # Group flows by logical hash + lflow = LFlow( + flow, + exact_actions=["output", "resubmit", "drop"], + match_cookie=self.match_cookie, + ) + + if not self.tables[table].get(lflow): + self.tables[table][lflow] = list() + + self.tables[table][lflow].append(flow) + + def print(self, show_flows): + formatter = ConsoleFormatter(opts=self.opts) + console = formatter.console + for name, tables in self.data.items(): + console.print("\n") + console.print(file_header(name)) + tree = Tree("Ofproto Flows (logical)") + + for table_num in sorted(tables.keys()): + table = tables[table_num] + table_tree = tree.add("** TABLE {} **".format(table_num)) + + if len(self.heat_map) > 0 and len(table.values()) > 0: + for i, field in enumerate(self.heat_map): + (min_val, max_val) = self.min_max[name][i] + formatter.style.set_value_style( + field, heat_pallete(min_val, max_val) + ) + + for lflow in sorted( + table.keys(), + key=(lambda x: x.priority), + reverse=True, + ): + flows = table[lflow] + ovn_info = None + if self.ovn_detrace: + ovn_info = self.ovn_detrace.get_ovn_info(lflow.cookie) + if self.opts.get("ovn_filter"): + ovn_regexp = re.compile( + self.opts.get("ovn_filter") + ) + if not ovn_regexp.search(ovn_info): + continue + + buf = ConsoleBuffer(Text()) + + lflow.format(buf, formatter) + buf.append_extra( + " ( x {} )".format(len(flows)), + style="dark_olive_green3", + ) + lflow_tree = table_tree.add(buf.text) + + if ovn_info: + ovn = lflow_tree.add("OVN Info") + for part in ovn_info.split("\n"): + if part.strip(): + ovn.add(part.strip()) + + if show_flows: + for flow in flows: + buf = ConsoleBuffer(Text()) + highlighted = None + if self.opts.get("highlight"): + result = self.opts.get("highlight").evaluate( + flow + ) + if result: + highlighted = result.kv + formatter.format_flow(buf, flow, highlighted) + lflow_tree.add(buf.text) + + console.print(tree) + + +class OVNDetrace(object): + def __init__(self, opts): + if not opts.get("ovn_detrace_flag"): + raise Exception("Cannot initialize OVN Detrace connection") + + if opts.get("ovn_detrace_path"): + sys.path.append(opts.get("ovn_detrace_path")) + + import ovn_detrace + + class FakePrinter(ovn_detrace.Printer): + def __init__(self): + self.buff = io.StringIO() + + def print_p(self, msg): + print(" * ", msg, file=self.buff) + + def print_h(self, msg): + print(" * ", msg, file=self.buff) + + def clear(self): + self.buff = io.StringIO() + + self.ovn_detrace = ovn_detrace + self.ovnnb_conn = ovn_detrace.OVSDB( + opts.get("ovnnb_db"), "OVN_Northbound" + ) + self.ovnsb_conn = ovn_detrace.OVSDB( + opts.get("ovnsb_db"), "OVN_Southbound" + ) + self.ovn_printer = FakePrinter() + self.cookie_handlers = ovn_detrace.get_cookie_handlers( + self.ovnnb_conn, self.ovnsb_conn, self.ovn_printer + ) + + def get_ovn_info(self, cookie): + self.ovn_printer.clear() + self.ovn_detrace.print_record_from_cookie( + self.ovnsb_conn, self.cookie_handlers, "{:x}".format(cookie) + ) + return self.ovn_printer.buff.getvalue() + + +# Try to make it easy to spot same cookies by printing them in different +# colors +cookie_style_gen = hash_pallete( + hue=[x / 10 for x in range(0, 10)], + saturation=[0.5], + value=[0.5 + x / 10 * (0.85 - 0.5) for x in range(0, 10)], +)