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 <echau...@redhat.com>
> Signed-off-by: Adrian Moreno <amore...@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 <echau...@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

_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to