On 1 Dec 2023, at 20:14, Adrian Moreno wrote:

> process.py contains a useful base class that processes files
> odp.py and ofp.py: contain datapath and openflow subcommand definitions
> as well as the first formatting option: json.
>
> Also, this patch adds basic filtering support.
>
> Examples:
> $ ovs-ofctl dump-flows br-int | ovs-flowviz openflow json
> $ ovs-ofctl dump-flows br-int > flows.txt && ovs-flowviz -i flows.txt 
> openflow json
> $ ovs-ofctl appctl dpctl/dump-flows | ovs-flowviz -f 'ct' datapath json
> $ ovs-ofctl appctl dpctl/dump-flows > flows.txt && ovs-flowviz -i flows.txt 
> -f 'drop' datapath json

Some small comments below!

> Signed-off-by: Adrian Moreno <amore...@redhat.com>
> ---
>  python/automake.mk             |   5 +-
>  python/ovs/flowviz/__init__.py |   2 +
>  python/ovs/flowviz/main.py     | 103 +++++++++++++++++-
>  python/ovs/flowviz/odp/cli.py  |  42 ++++++++
>  python/ovs/flowviz/ofp/cli.py  |  42 ++++++++
>  python/ovs/flowviz/process.py  | 192 +++++++++++++++++++++++++++++++++
>  6 files changed, 384 insertions(+), 2 deletions(-)
>  create mode 100644 python/ovs/flowviz/odp/cli.py
>  create mode 100644 python/ovs/flowviz/ofp/cli.py
>  create mode 100644 python/ovs/flowviz/process.py
>
> diff --git a/python/automake.mk b/python/automake.mk
> index 4302f0136..4845565b8 100644
> --- a/python/automake.mk
> +++ b/python/automake.mk
> @@ -67,8 +67,11 @@ ovs_flowviz = \
>       python/ovs/flowviz/__init__.py \
>       python/ovs/flowviz/main.py \
>       python/ovs/flowviz/odp/__init__.py \
> +     python/ovs/flowviz/odp/cli.py \
>       python/ovs/flowviz/ofp/__init__.py \
> -     python/ovs/flowviz/ovs-flowviz
> +     python/ovs/flowviz/ofp/cli.py \
> +     python/ovs/flowviz/ovs-flowviz \
> +     python/ovs/flowviz/process.py
>
>
>  # These python files are used at build time but not runtime,
> diff --git a/python/ovs/flowviz/__init__.py b/python/ovs/flowviz/__init__.py
> index e69de29bb..898dba522 100644
> --- a/python/ovs/flowviz/__init__.py
> +++ b/python/ovs/flowviz/__init__.py
> @@ -0,0 +1,2 @@
> +import ovs.flowviz.ofp.cli  # noqa: F401
> +import ovs.flowviz.odp.cli  # noqa: F401
> diff --git a/python/ovs/flowviz/main.py b/python/ovs/flowviz/main.py
> index a2d5ca1fa..a45c06e48 100644
> --- a/python/ovs/flowviz/main.py
> +++ b/python/ovs/flowviz/main.py
> @@ -12,19 +12,67 @@
>  # See the License for the specific language governing permissions and
>  # limitations under the License.
>
> +import os
> +
>  import click

Do we maybe want all imports together and sorted, i.e.:

import click
import os

> +from ovs.flow.filter import OFFilter
> +
>
>  class Options(dict):
>      """Options dictionary"""
>
>
> +def validate_input(ctx, param, value):
> +    """Validate the "-i" option"""
> +    result = list()
> +    for input_str in value:
> +        parts = input_str.strip().split(",")
> +        if len(parts) == 2:
> +            file_parts = tuple(parts)
> +        elif len(parts) == 1:
> +            file_parts = tuple(["Filename: " + parts[0], parts[0]])
> +        else:
> +            raise click.BadParameter(
> +                "input filename should have the following format: "
> +                "[alias,]FILENAME"
> +            )
> +
> +        if not os.path.isfile(file_parts[1]):
> +            raise click.BadParameter(
> +                "input filename %s does not exist" % file_parts[1]
> +            )
> +        result.append(file_parts)
> +    return result
> +
> +
>  @click.group(
>      subcommand_metavar="TYPE",
>      context_settings=dict(help_option_names=["-h", "--help"]),
>  )
> +@click.option(
> +    "-i",
> +    "--input",
> +    "filename",
> +    help="Read flows from specified filepath. If not provided, flows will be"
> +    " read from stdin. This option can be specified multiple times."
> +    " Format [alias,]FILENAME. Where alias is a name that shall be used to"
> +    " refer to this FILENAME",
> +    multiple=True,
> +    type=click.Path(),
> +    callback=validate_input,
> +)
> +@click.option(
> +    "-f",
> +    "--filter",
> +    help="Filter flows that match the filter expression."
> +    "Run 'ovs-flowviz filter' for a detailed description of the filtering "
> +    "syntax",
> +    type=str,
> +    show_default=False,
> +)
>  @click.pass_context
> -def maincli(ctx):
> +def maincli(ctx, filename, filter):
>      """
>      OpenvSwitch flow visualization utility.
>
> @@ -32,6 +80,59 @@ def maincli(ctx):
>      (such as the output of ovs-ofctl dump-flows or ovs-appctl 
> dpctl/dump-flows)
>      and prints them in different formats.
>      """
> +    ctx.obj = Options()
> +    ctx.obj["filename"] = filename or None
> +    if filter:
> +        try:
> +            ctx.obj["filter"] = OFFilter(filter)
> +        except Exception as e:
> +            raise click.BadParameter("Wrong filter syntax: {}".format(e))
> +
> +
> +@maincli.command(hidden=True)
> +@click.pass_context
> +def filter(ctx):
> +    """
> +    \b
> +    Filter Syntax
> +    *************
> +
> +     [! | not ] {key}[[.subkey]...] [OPERATOR] {value})] [LOGICAL OPERATOR] 
> ...
> +
> +    \b
> +    Comparison operators are:
> +        =   equality
> +        <   less than
> +        >   more than
> +        ~=  masking (valid for IP and Ethernet fields)
> +
> +    \b
> +    Logical operators are:
> +        !{expr}:  NOT
> +        {expr} && {expr}: AND
> +        {expr} || {expr}: OR
> +
> +    \b
> +    Matches and flow metadata:
> +        To compare against a match or info field, use the field directly, 
> e.g:
> +            priority=100
> +            n_bytes>10
> +        Use simple keywords for flags:
> +            tcp and ip_src=192.168.1.1
> +    \b
> +    Actions:
> +        Actions values might be dictionaries, use subkeys to access 
> individual
> +        values, e.g:
> +            output.port=3
> +        Use simple keywords for flags
> +            drop
> +
> +    \b
> +    Examples of valid filters.
> +        nw_addr~=192.168.1.1 && (tcp_dst=80 || tcp_dst=443)
> +        arp=true && !arp_tsa=192.168.1.1
> +        n_bytes>0 && drop=true"""
> +    click.echo(ctx.command.get_help(ctx))
>
>
>  def main():
> diff --git a/python/ovs/flowviz/odp/cli.py b/python/ovs/flowviz/odp/cli.py
> new file mode 100644
> index 000000000..ed2f82065
> --- /dev/null
> +++ b/python/ovs/flowviz/odp/cli.py
> @@ -0,0 +1,42 @@
> +# 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 click
> +
> +from ovs.flowviz.main import maincli
> +from ovs.flowviz.process import (
> +    DatapathFactory,
> +    JSONProcessor,
> +)
> +
> +
> +@maincli.group(subcommand_metavar="FORMAT")
> +@click.pass_obj
> +def datapath(opts):
> +    """Process Datapath Flows."""
> +    pass
> +
> +
> +class JSONPrint(DatapathFactory, JSONProcessor):
> +    def __init__(self, opts):
> +        super().__init__(opts)
> +
> +
> +@datapath.command()
> +@click.pass_obj
> +def json(opts):
> +    """Print the flows in JSON format."""
> +    proc = JSONPrint(opts)
> +    proc.process()
> +    print(proc.json_string())
> diff --git a/python/ovs/flowviz/ofp/cli.py b/python/ovs/flowviz/ofp/cli.py
> new file mode 100644
> index 000000000..b9a2a8aad
> --- /dev/null
> +++ b/python/ovs/flowviz/ofp/cli.py
> @@ -0,0 +1,42 @@
> +# 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 click
> +
> +from ovs.flowviz.main import maincli
> +from ovs.flowviz.process import (
> +    OpenFlowFactory,
> +    JSONProcessor,
> +)
> +
> +
> +@maincli.group(subcommand_metavar="FORMAT")
> +@click.pass_obj
> +def openflow(opts):
> +    """Process OpenFlow Flows."""
> +    pass
> +
> +
> +class JSONPrint(OpenFlowFactory, JSONProcessor):
> +    def __init__(self, opts):
> +        super().__init__(opts)
> +
> +
> +@openflow.command()
> +@click.pass_obj
> +def json(opts):
> +    """Print the flows in JSON format."""
> +    proc = JSONPrint(opts)
> +    proc.process()
> +    print(proc.json_string())
> diff --git a/python/ovs/flowviz/process.py b/python/ovs/flowviz/process.py
> new file mode 100644
> index 000000000..413506bf2
> --- /dev/null
> +++ b/python/ovs/flowviz/process.py
> @@ -0,0 +1,192 @@
> +# 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 json
> +import click
> +
> +from ovs.flow.decoders import FlowEncoder
> +from ovs.flow.odp import ODPFlow
> +from ovs.flow.ofp import OFPFlow
> +
> +
> +class FileProcessor(object):
> +    """Base class for file-based Flow processing. It is able to create flows
> +    from strings found in a file (or stdin).
> +
> +    The process of parsing the flows is extendable in many ways by deriving
> +    this class.
> +
> +    When process() is called, the base class will:
> +        - call self.start_file() for each new file that get's processed
> +        - call self.create_flow() for each flow line
> +        - apply the filter defined in opts if provided (can be optionally
> +            disabled)
> +        - call self.process_flow() for after the flow has been filtered
> +        - call self.stop_file() after the file has been processed entirely
> +
> +    In the case of stdin, the filename and file alias is 'stdin'.
> +
> +    Child classes must at least implement create_flow() and process_flow()
> +    functions.
> +
> +    Args:
> +        opts (dict): Options dictionary
> +    """
> +
> +    def __init__(self, opts):
> +        self.opts = opts
> +
> +    # Methods that must be implemented by derived classes

Ending comment line with a dot?

> +    def init(self):
> +        """Called before the flow processing begins."""
> +        pass
> +
> +    def start_file(self, alias, filename):
> +        """Called before the processing of a file begins.
> +        Args:
> +            alias(str): The alias name of the filename
> +            filename(str): The filename string
> +        """
> +        pass
> +
> +    def create_flow(self, line, idx):
> +        """Called for each line in the file.
> +        Args:
> +            line(str): The flow line
> +            idx(int): The line index
> +
> +        Returns a Flow.
> +        Must be implemented by child classes.
> +        """
> +        raise NotImplementedError
> +
> +    def process_flow(self, flow, name):
> +        """Called for built flow (after filtering).
> +        Args:
> +            flow(Flow): The flow created by create_flow
> +            name(str): The name of the file from which the flow comes
> +        """
> +        raise NotImplementedError
> +
> +    def stop_file(self, alias, filename):
> +        """Called after the processing of a file ends.
> +        Args:
> +            alias(str): The alias name of the filename
> +            filename(str): The filename string
> +        """
> +        pass
> +
> +    def end(self):
> +        """Called after the processing ends."""
> +        pass
> +
> +    def process(self, do_filter=True):
> +        idx = 0
> +        filenames = self.opts.get("filename")
> +        filt = self.opts.get("filter") if do_filter else None
> +        self.init()
> +        if filenames:
> +            for alias, filename in filenames:
> +                try:
> +                    with open(filename) as f:
> +                        self.start_file(alias, filename)
> +                        for line in f:
> +                            flow = self.create_flow(line, idx)
> +                            idx += 1
> +                            if not flow or (filt and not 
> filt.evaluate(flow)):
> +                                continue
> +                            self.process_flow(flow, alias)
> +                        self.stop_file(alias, filename)
> +                except IOError as e:
> +                    raise click.BadParameter(
> +                        "Failed to read from file {} ({}): {}".format(
> +                            filename, e.errno, e.strerror
> +                        )
> +                    )
> +        else:
> +            data = sys.stdin.read()
> +            self.start_file("stdin", "stdin")
> +            for line in data.split("\n"):
> +                line = line.strip()
> +                if line:
> +                    flow = self.create_flow(line, idx)
> +                    idx += 1
> +                    if (
> +                        not flow
> +                        or not getattr(flow, "_sections", None)
> +                        or (filt and not filt.evaluate(flow))
> +                    ):
> +                        continue
> +                    self.process_flow(flow, "stdin")
> +            self.stop_file("stdin", "stdin")
> +        self.end()
> +
> +
> +class DatapathFactory():
> +    """A mixin class that creates OpenFlow flows."""

Datapath flows

> +
> +    def create_flow(self, line, idx):
> +        # Skip strings commonly found in Datapath flow dumps.
> +        if any(s in line for s in [
> +            "flow-dump from the main thread",
> +            "flow-dump from pmd on core",
> +        ]):
> +            return None
> +
> +        return ODPFlow(line, idx)
> +
> +
> +class OpenFlowFactory():
> +    """A mixin class that creates Datapath flows."""

OpenFlow flows

> +
> +    def create_flow(self, line, idx):
> +        # Skip strings commonly found in OpenFlow flow dumps.
> +        if " reply " in line:
> +            return None
> +
> +        return OFPFlow(line, idx)
> +
> +
> +class JSONProcessor(FileProcessor):
> +    """A FileProcessor that prints flows in JSON format."""
> +
> +    def __init__(self, opts):
> +        super().__init__(opts)
> +        self.flows = dict()
> +
> +    def start_file(self, name, filename):
> +        self.flows_list = list()
> +
> +    def stop_file(self, name, filename):
> +        self.flows[name] = self.flows_list
> +
> +    def process_flow(self, flow, name):
> +        self.flows_list.append(flow)
> +
> +    def json_string(self):
> +        if len(self.flows.keys()) > 1:
> +            return json.dumps(
> +                [
> +                    {"name": name, "flows": [flow.dict() for flow in flows]}
> +                    for name, flows in self.flows.items()
> +                ],
> +                indent=4,
> +                cls=FlowEncoder,
> +            )
> +        return json.dumps(
> +            [flow.dict() for flow in self.flows_list],
> +            indent=4,
> +            cls=FlowEncoder,
> +        )
> -- 
> 2.43.0
>
> _______________________________________________
> dev mailing list
> d...@openvswitch.org
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev

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

Reply via email to