ofparse is a command line utility capable of parsing openflow flows
(e.g: the output of 'ovs-ofctl dump flows {br}') as well as dpif flows
(e.g: the output of 'ovs-dpctl dump-flows or 'ovs-appctl
dpctl/dump-flows') and print it back in different formats.

It supports:
- Filtering based on a simple yet flexible filtering syntax that
  supports matching on any field or action keyword or argument and
  perform arithmetic comparisons and bitwise masking
- Using the system's PAGER to page the results for easier inspection
- Openflow Format:
  - "Pretty" printing the flows using styles based on value "types"
  - printing the flows in json format
  - "Logic" representation of the flows. A "logical" flow groups all the
    flows that match on the same fields and execute the same actions
    (regardless of their arguments or field contents).
- DPIF Format:
  - "Pretty" printing the flows using styles based on value "types"
  - printing the flows in json format
  - "Logic" representation of the flows by gruping them based on
    "recirc_id" and printing them in a tree structure

Note: This patch adds the entire utitily as a way to demonstrate the use
of the ovs.flows library
Original Idea for the "logic" representation of datapath and openflow
flows comes from Flavio Leitner <[email protected]>.

Signed-off-by: Adrian Moreno <[email protected]>
---
 python/automake.mk             |  12 +-
 python/ovs/ofparse/__init__.py |   1 +
 python/ovs/ofparse/console.py  | 248 +++++++++++++++++++++++++++++++++
 python/ovs/ofparse/dp.py       | 102 ++++++++++++++
 python/ovs/ofparse/main.py     | 118 ++++++++++++++++
 python/ovs/ofparse/ofp.py      | 167 ++++++++++++++++++++++
 python/ovs/ofparse/ofparse     |   6 +
 python/ovs/ofparse/process.py  |  82 +++++++++++
 python/setup.py                |   5 +-
 9 files changed, 737 insertions(+), 4 deletions(-)
 create mode 100644 python/ovs/ofparse/__init__.py
 create mode 100644 python/ovs/ofparse/console.py
 create mode 100644 python/ovs/ofparse/dp.py
 create mode 100644 python/ovs/ofparse/main.py
 create mode 100644 python/ovs/ofparse/ofp.py
 create mode 100755 python/ovs/ofparse/ofparse
 create mode 100644 python/ovs/ofparse/process.py

diff --git a/python/automake.mk b/python/automake.mk
index cff41a657..57389093b 100644
--- a/python/automake.mk
+++ b/python/automake.mk
@@ -49,7 +49,15 @@ ovs_pyfiles = \
        python/ovs/flows/flow.py \
        python/ovs/flows/ofp.py \
        python/ovs/flows/odp.py \
-       python/ovs/flows/filter.py
+       python/ovs/flows/filter.py \
+       python/ovs/ofparse/console.py \
+       python/ovs/ofparse/dp.py \
+       python/ovs/ofparse/ofp.py \
+       python/ovs/ofparse/process.py \
+       python/ovs/ofparse/main.py \
+       python/ovs/ofparse/__init__.py
+
+
 
 # These python files are used at build time but not runtime,
 # so they are not installed.
@@ -69,7 +77,7 @@ EXTRA_DIST += \
 EXTRA_DIST += python/ovs/_json.c
 
 PYFILES = $(ovs_pyfiles) python/ovs/dirs.py $(ovstest_pyfiles)
-EXTRA_DIST += $(PYFILES)
+EXTRA_DIST += $(PYFILES) python/ovs/ofparse/ofparse
 PYCOV_CLEAN_FILES += $(PYFILES:.py=.py,cover)
 
 FLAKE8_PYFILES += \
diff --git a/python/ovs/ofparse/__init__.py b/python/ovs/ofparse/__init__.py
new file mode 100644
index 000000000..bc6b4772e
--- /dev/null
+++ b/python/ovs/ofparse/__init__.py
@@ -0,0 +1 @@
+from . import ofp, dp
diff --git a/python/ovs/ofparse/console.py b/python/ovs/ofparse/console.py
new file mode 100644
index 000000000..76a33cc72
--- /dev/null
+++ b/python/ovs/ofparse/console.py
@@ -0,0 +1,248 @@
+""" This module defines OFConsole class
+"""
+
+import sys
+import contextlib
+from rich.console import Console
+from rich.text import Text
+from rich.style import Style
+
+
+class OFConsole:
+    """OFConsole is a class capable of printing flows in a rich console format
+
+    Args:
+        console (rich.Console): Optional, an existing console to use
+        max_value_len (int): Optional; max length of the printed values
+        kwargs (dict): Optional; Extra arguments to be passed down to
+            rich.console.Console()
+    """
+
+    default_style = {
+        "key": Style(color="steel_blue"),
+        "delim": Style(color="steel_blue"),
+        "value": Style(color="medium_orchid"),
+        "value.type.IPAddress": Style(color="green4"),
+        "value.type.IPMask": Style(color="green4"),
+        "value.type.EthMask": Style(color="green4"),
+        "value.ct": Style(color="bright_black"),
+        "value.ufid": Style(color="dark_red"),
+        "value.clone": Style(color="bright_black"),
+        "value.controller": Style(color="bright_black"),
+        "flag": Style(color="slate_blue1"),
+        "key.drop": Style(color="red"),
+        "key.resubmit": Style(color="green3"),
+        "key.output": Style(color="green3"),
+    }
+
+    def __init__(self, console=None, max_value_length=-1, **kwargs):
+        self.console = console or Console(**kwargs)
+        self.max_value_length = max_value_length
+
+    def print_flow(self, flow, style=None):
+        """
+        Prints a flow to the console
+
+        Args:
+            flow (ovs_dbg.OFPFlow): the flow to print
+            style (dict): Optional; style dictionary to use
+        """
+
+        text = Text()
+        self.format_flow(flow, style, text)
+        self.console.print(text)
+
+    def format_flow(self, flow, style=None, text=None):
+        """
+        Formats the flow into the rich.Text
+
+        Args:
+            flow (ovs_dbg.OFPFlow): the flow to format
+            style (dict): Optional; style dictionary to use
+            text (rich.Text): Optional; the Text object to append to
+        """
+        text = text if text is not None else Text()
+
+        last_printed_pos = 0
+        for section in sorted(flow.sections, key=lambda x: x.pos):
+            text.append(
+                flow.orig[last_printed_pos : section.pos],
+                Style(color="white"),
+            )
+            self.format_kv_list(section.data, section.string, style, text)
+            last_printed_pos = section.pos + len(section.string)
+
+    def format_info(self, flow, style=None, text=None):
+        """
+        Formats the flow information into the rich.Text
+
+        Args:
+            flow (ovs_dbg.OFPFlow): the flow to format
+            style (dict): Optional; style dictionary to use
+            text (rich.Text): Optional; the Text object to append to
+        """
+        self.format_kv_list(flow.info_kv, flow.meta.istring, style, text)
+
+    def format_matches(self, flow, style=None, text=None):
+        """
+        Formats the flow information into the rich.Text
+
+        Args:
+            flow (ovs_dbg.OFPFlow): the flow to format
+            style (dict): Optional; style dictionary to use
+            text (rich.Text): Optional; the Text object to append to
+        """
+        self.format_kv_list(flow.match_kv, flow.meta.mstring, style, text)
+
+    def format_actions(self, flow, style=None, text=None):
+        """
+        Formats the action into the rich.Text
+
+        Args:
+            flow (ovs_dbg.OFPFlow): the flow to format
+            style (dict): Optional; style dictionary to use
+            text (rich.Text): Optional; the Text object to append to
+        """
+        self.format_kv_list(flow.actions_kv, flow.meta.astring, style, text)
+
+    def format_kv_list(self, kv_list, full_str, style=None, text=None):
+        """
+        Formats the list of KeyValues into the rich.Text
+
+        Args:
+            kv_list (list[KeyValue]): the flow to format
+            full_str (str): the full string containing all k-v
+            style (dict): Optional; style dictionary to use
+            text (rich.Text): Optional; the Text object to append to
+        """
+        text = text if text is not None else Text()
+        for i in range(len(kv_list)):
+            kv = kv_list[i]
+            written = self.format_kv(kv, style=style, text=text)
+
+            # print kv separators
+            end = kv_list[i + 1].meta.kpos if i < (len(kv_list) - 1) else 
len(full_str)
+            text.append(
+                full_str[(kv.meta.kpos + written) : end].rstrip("\n\r"),
+                style=Style(color="white"),
+            )
+
+    def format_kv(self, kv, style=None, text=None, highlighted=[]):
+        """Format a KeyValue
+
+        A formatted keyvalue has the following parts:
+            {key}{delim}{value}[{delim}]
+
+        The following keys are fetched in style dictionary to determine the
+        style to use for the key section:
+            - key.highlighted.{key} (if key is found in hightlighted)
+            - key.highlighted (if key is found in hightlighted)
+            - key.{key}
+            - key
+
+        The following keys are fetched in style dictionary to determine the
+        style to use for the value section of a specific key:
+            - value.highlighted.{key} (if key is found in hightlighted)
+            - value.highlighted.type{value.__class__.__name__}
+            - value.highlighted
+                (if key is found in hightlighted)
+            - value.{key}
+            - value.type.{value.__class__.__name__}
+            - value
+
+        The following keys are fetched in style dictionary to determine the
+        style to use for the delim section
+            - delim
+
+        Args:
+            kv (KeyValue): The KeyValue to print
+            text (rich.Text): Optional; Text instance to append the text to
+            style (dict): The style dictionary
+            highlighted(list): A list of keys that shall be highlighted
+
+        Returns the number of printed characters
+        """
+        ret = 0
+        text = text if text is not None else Text()
+        styles = style or self.default_style
+        meta = kv.meta
+        key = meta.kstring
+
+        if kv.value is True and not kv.meta.vstring:
+            text.append(key, styles.get("flag"))
+            return len(key)
+
+        key_style_lookup = (
+            ["key.highlighted.%s" % key, "key.highlighted"]
+            if key in highlighted
+            else []
+        )
+        key_style_lookup.extend(["key.%s" % key, "key"])
+        key_style = next(styles.get(s) for s in key_style_lookup if 
styles.get(s))
+
+        text.append(key, key_style)
+        ret += len(key)
+
+        if kv.meta.vstring:
+            if kv.meta.delim not in ("\n", "\t", "\r", ""):
+                text.append(kv.meta.delim, styles.get("delim"))
+                ret += len(kv.meta.delim)
+
+            value_style_lookup = (
+                [
+                    "value.highlighted.%s" % key,
+                    "value.highlighted.type.%s" % kv.value.__class__.__name__,
+                    "value.highlighted",
+                ]
+                if key in highlighted
+                else []
+            )
+            value_style_lookup.extend(
+                [
+                    "value.%s" % key,
+                    "value.type.%s" % kv.value.__class__.__name__,
+                    "value",
+                ]
+            )
+            value_style = next(
+                styles.get(s) for s in value_style_lookup if styles.get(s)
+            )
+
+            if (
+                self.max_value_length >= 0
+                and len(kv.meta.vstring) > self.max_value_length
+            ):
+                value_str = kv.meta.vstring[0 : self.max_value_length] + "..."
+            else:
+                value_str = kv.meta.vstring
+
+            text.append(value_str, style=value_style)
+            ret += len(kv.meta.vstring)
+        if meta.end_delim:
+            text.append(meta.end_delim, styles.get("delim"))
+            ret += len(kv.meta.end_delim)
+
+        return ret
+
+
+def print_context(console, paged=False, styles=True):
+    """
+    Returns a printing context
+
+    Args:
+        console: The console to print
+        paged (bool): Wheter to page the output
+        style (bool): Whether to force the use of styled pager
+    """
+    if paged:
+        # Internally pydoc's pager library is used which returns a
+        # plain pager if both stdin and stdout are not tty devices
+        #
+        # Workaround that limitation if only stdin is not a tty (e.g
+        # data is piped to us through stdin)
+        if not sys.stdin.isatty() and sys.stdout.isatty():
+            setattr(sys.stdin, "isatty", lambda: True)
+
+        return console.pager(styles=styles)
+
+    return contextlib.nullcontext()
diff --git a/python/ovs/ofparse/dp.py b/python/ovs/ofparse/dp.py
new file mode 100644
index 000000000..284e0cb1f
--- /dev/null
+++ b/python/ovs/ofparse/dp.py
@@ -0,0 +1,102 @@
+import sys
+import click
+import colorsys
+from rich.tree import Tree
+from rich.text import Text
+from rich.console import Console
+from rich.style import Style
+from rich.color import Color
+
+from ovs.ofparse.main import maincli
+from ovs.ofparse.process import process_flows, tojson, pprint
+from .console import OFConsole, print_context
+from ovs.flows.odp import ODPFlow
+
+
[email protected](subcommand_metavar="FORMAT")
[email protected]_obj
+def datapath(opts):
+    """Process DPIF Flows"""
+    pass
+
+
[email protected]()
[email protected]_obj
+def json(opts):
+    """Print the flows in JSON format"""
+    return tojson(flow_factory=ODPFlow.from_string, opts=opts)
+
+
[email protected]()
[email protected]_obj
+def pretty(opts):
+    """Print the flows with some style"""
+    return pprint(flow_factory=ODPFlow.from_string, opts=opts)
+
+
[email protected]()
[email protected]_obj
+def logic(opts):
+    """Print the flows in a tree based on the 'recirc_id'"""
+
+    flow_list = []
+
+    def callback(flow):
+        flow_list.append(flow)
+
+    process_flows(
+        flow_factory=ODPFlow.from_string,
+        callback=callback,
+        filename=opts.get("filename"),
+        filter=opts.get("filter"),
+    )
+
+    tree = Tree("Datapath Flows (logical)")
+    console = Console(color_system=None if opts["no_color"] else "256")
+    ofconsole = OFConsole(console)
+
+    recirc_styles = [
+        Style(color=Color.from_rgb(r * 255, g * 255, b * 255))
+        for r, g, b in create_color_pallete(50)
+    ]
+
+    def process_flow_tree(parent, recirc_id):
+        sorted_flows = sorted(
+            filter(lambda f: f.match.get("recirc_id") == recirc_id, flow_list),
+            key=lambda x: x.info.get("packets") or 0,
+            reverse=True,
+        )
+
+        style = OFConsole.default_style
+        style["value"] = Style(color="bright_black")
+        style["key.output"] = Style(color="green")
+        style["value.output"] = Style(color="green")
+        for flow in sorted_flows:
+            next_recirc = next(
+                (kv.value for kv in flow.actions_kv if kv.key == "recirc"), 
None
+            )
+            if next_recirc:
+                style["value.recirc"] = recirc_styles[next_recirc % 
len(recirc_styles)]
+
+            text = Text()
+            style["value.recirc_id"] = recirc_styles[
+                (flow.match.get("recirc_id")) % len(recirc_styles)
+            ]
+            ofconsole.format_flow(flow=flow, style=style, text=text)
+            tree_elem = parent.add(text)
+
+            if next_recirc:
+                process_flow_tree(tree_elem, next_recirc)
+
+    process_flow_tree(tree, 0)
+
+    with print_context(console, opts["paged"], not opts["no_color"]):
+        console.print(tree)
+
+
+def create_color_pallete(size):
+    """Create a color pallete of size colors by modifying the Hue in the HSV
+    color space
+    """
+    HSV_tuples = [(x / size, 0.7, 0.8) for x in range(size)]
+    return map(lambda x: colorsys.hsv_to_rgb(*x), HSV_tuples)
diff --git a/python/ovs/ofparse/main.py b/python/ovs/ofparse/main.py
new file mode 100644
index 000000000..f961ce554
--- /dev/null
+++ b/python/ovs/ofparse/main.py
@@ -0,0 +1,118 @@
+import click
+import sys
+
+from ovs.flows.filter import OFFilter
+
+
+class Options(dict):
+    """Options dictionary"""
+
+    pass
+
+
[email protected](
+    subcommand_metavar="TYPE", context_settings=dict(help_option_names=["-h", 
"--help"])
+)
[email protected](
+    "-i",
+    "-input",
+    "filename",
+    help="Read flows from specified filepath. If not provided, flows will be"
+    " read from stdin",
+    type=click.Path(),
+)
[email protected](
+    "-p",
+    "--paged",
+    help="Page the result (uses $PAGER). If colors are not disabled you might "
+    'need to enable colors on your PAGER, eg: export PAGER="less -r".',
+    is_flag=True,
+    default=False,
+    show_default=True,
+)
[email protected](
+    "--no-color",
+    help="Do not use colors. Alternatively, set the environment variable 
NO_COLOR",
+    is_flag=True,
+    default=False,
+    show_default=True,
+)
[email protected](
+    "-f",
+    "--filter",
+    help="Filter flows that match the filter expression. Run 'ofparse filter'"
+    "for a detailed description of the filtering syntax",
+    type=str,
+    show_default=False,
+)
[email protected]_context
+def maincli(ctx, filename, paged, no_color, filter):
+    """
+    OpenFlow Parse utility.
+
+    It parses openflow flows (such as the output of ovs-ofctl 'dump-flows') and
+    prints them in different formats.
+
+    """
+    ctx.obj = Options()
+    ctx.obj["filename"] = filename or ""
+    ctx.obj["paged"] = paged
+    ctx.obj["no_color"] = no_color
+    if filter:
+        try:
+            ctx.obj["filter"] = OFFilter(filter)
+        except Exception as e:
+            raise click.BadParameter("Wrong filter syntax: {}".format(e))
+
+
[email protected](hidden=True)
[email protected]_context
+def filter(ctx):
+    """
+    \b
+    Filter Syntax
+    *************
+
+        [! | not ] {key}[[.subkey[.subkey]..] [= | > | < | ~=] {value})] [&& | 
|| | or | and | not ] ...
+
+    \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():
+    """
+    Main Function
+    """
+    maincli()
diff --git a/python/ovs/ofparse/ofp.py b/python/ovs/ofparse/ofp.py
new file mode 100644
index 000000000..291ac9609
--- /dev/null
+++ b/python/ovs/ofparse/ofp.py
@@ -0,0 +1,167 @@
+import sys
+import click
+import colorsys
+from rich.tree import Tree
+from rich.text import Text
+from rich.console import Console
+from rich.style import Style
+from rich.color import Color
+
+from ovs.ofparse.main import maincli
+from ovs.ofparse.process import process_flows, tojson, pprint
+from .console import OFConsole, print_context
+from ovs.flows.ofp import OFPFlow
+
+
[email protected](subcommand_metavar="FORMAT")
[email protected]_obj
+def openflow(opts):
+    """Process OpenFlow Flows"""
+    pass
+
+
[email protected]()
[email protected]_obj
+def json(opts):
+    """Print the flows in JSON format"""
+    return tojson(flow_factory=create_ofp_flow, opts=opts)
+
+
[email protected]()
[email protected]_obj
+def pretty(opts):
+    """Print the flows with some style"""
+    return pprint(flow_factory=create_ofp_flow, opts=opts)
+
+
[email protected]()
[email protected](
+    "-s",
+    "--show-flows",
+    is_flag=True,
+    default=False,
+    show_default=True,
+    help="Show the full flows under each logical flow",
+)
[email protected]_obj
+def logic(opts, show_flows):
+    """
+    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, cookie and actions (regardless of action arguments)
+
+    Frisorting the flows based on tables and priority, deduplicates flows
+    based on
+    """
+    tables = dict()
+
+    class LFlow:
+        """A Logical Flow represents the scheleton of a flow
+
+        Attributes:
+            cookie (int): The flow cookie
+            priority (int): The flow priority
+            action_keys (tuple): The action keys
+            match_keys (tuple): The match keys
+        """
+
+        def __init__(self, flow):
+            self.priority = flow.match.get("priority") or 0
+            self.cookie = flow.info.get("cookie") or 0
+            self.action_keys = tuple([kv.key for kv in flow.actions_kv])
+            self.match_keys = tuple([kv.key for kv in flow.match_kv])
+
+        def __eq__(self, other):
+            return (
+                self.cookie == other.cookie
+                and self.priority == other.priority
+                and self.action_keys == other.action_keys
+                and self.match_keys == other.match_keys
+            )
+
+        def __hash__(self):
+            return tuple(
+                [self.cookie, self.priority, self.action_keys, self.match_keys]
+            ).__hash__()
+
+    def callback(flow):
+        """Parse the flows and sort them by table and logical flow"""
+        table = flow.info.get("table") or 0
+        if not tables.get(table):
+            tables[table] = dict()
+
+        # Group flows by logical hash
+        lflow = LFlow(flow)
+
+        if not tables[table].get(lflow):
+            tables[table][lflow] = list()
+
+        tables[table][lflow].append(flow)
+
+    process_flows(
+        flow_factory=create_ofp_flow,
+        callback=callback,
+        filename=opts.get("filename"),
+        filter=opts.get("filter"),
+    )
+
+    # Try to make it easy to spot same cookies by printing them in different
+    # colors
+    cookie_styles = [
+        Style(color=Color.from_rgb(r * 255, g * 255, b * 255))
+        for r, g, b in create_color_pallete(200)
+    ]
+
+    tree = Tree("Ofproto Flows (logical)")
+    console = Console(color_system=None if opts["no_color"] else "256")
+
+    for table_num in sorted(tables.keys()):
+        table = tables[table_num]
+        table_tree = tree.add("** TABLE {} **".format(table_num))
+
+        for lflow in sorted(
+            table.keys(),
+            key=(lambda x: x.priority),
+            reverse=True,
+        ):
+            flows = table[lflow]
+
+            text = Text()
+
+            text.append(
+                "cookie={} ".format(hex(lflow.cookie)).ljust(18),
+                style=cookie_styles[(lflow.cookie * 0x27D4EB2D) % 
len(cookie_styles)],
+            )
+            text.append("priority={} ".format(lflow.priority), 
style="steel_blue")
+            text.append(",".join(lflow.match_keys), style="steel_blue")
+            text.append("  --->  ", style="bold magenta")
+            text.append(",".join(lflow.action_keys), style="steel_blue")
+            text.append(" ( x {} )".format(len(flows)), 
style="dark_olive_green3")
+            lflow_tree = table_tree.add(text)
+
+            if show_flows:
+                for flow in flows:
+                    text = Text()
+                    OFConsole(console).format_flow(flow, text=text)
+                    lflow_tree.add(text)
+
+    with print_context(console, opts["paged"], not opts["no_color"]):
+        console.print(tree)
+
+
+def create_color_pallete(size):
+    """Create a color pallete of size colors by modifying the Hue in the HSV
+    color space
+    """
+    HSV_tuples = [(x / size, 0.5, 0.5) for x in range(size)]
+    return map(lambda x: colorsys.hsv_to_rgb(*x), HSV_tuples)
+
+
+def create_ofp_flow(string):
+    """Create a OFPFlow"""
+    if " reply " in string:
+        return None
+    return OFPFlow.from_string(string)
diff --git a/python/ovs/ofparse/ofparse b/python/ovs/ofparse/ofparse
new file mode 100755
index 000000000..093c714ca
--- /dev/null
+++ b/python/ovs/ofparse/ofparse
@@ -0,0 +1,6 @@
+#!/usr/bin/env python3
+
+from ovs.ofparse import main
+
+if __name__ == '__main__':
+    main.main()
diff --git a/python/ovs/ofparse/process.py b/python/ovs/ofparse/process.py
new file mode 100644
index 000000000..ca761a65f
--- /dev/null
+++ b/python/ovs/ofparse/process.py
@@ -0,0 +1,82 @@
+""" Defines common flow processing functionality
+"""
+import sys
+import json
+import rich
+
+from ovs.flows.decoders import FlowEncoder
+from ovs.ofparse.console import OFConsole, print_context
+
+
+def process_flows(flow_factory, callback, filename="", filter=None):
+    """Process flows from file or stdin
+
+    Args:
+        flow_factory(Callable): function to call to create a flow
+        callback (Callable): function to call with each processed flow
+        filename (str): Optional; filename to read frows from
+        filter (OFFilter): Optional; filter to use to filter flows
+    """
+    if filename:
+        with open(filename) as f:
+            for line in f:
+                flow = flow_factory(line)
+                if not flow or (filter and not filter.evaluate(flow)):
+                    continue
+                callback(flow)
+    else:
+        data = sys.stdin.read()
+        for line in data.split("\n"):
+            line = line.strip()
+            if line:
+                flow = flow_factory(line)
+                if not flow or (filter and not filter.evaluate(flow)):
+                    continue
+                callback(flow)
+
+
+def tojson(flow_factory, opts):
+    """
+    Print the json representation of the flow list
+
+    Args:
+        flow_factory (Callable): Function to call to create the flows
+        opts (dict): Options
+    """
+    flows = []
+
+    def callback(flow):
+        flows.append(flow)
+
+    process_flows(flow_factory, callback, opts.get("filename"), 
opts.get("filter"))
+
+    flow_json = json.dumps(
+        [flow.dict() for flow in flows],
+        indent=4,
+        cls=FlowEncoder,
+    )
+
+    if opts["paged"]:
+        console = rich.Console()
+        with print_context(console, opts["paged"], not opts["no_color"]):
+            console.print(flow_json)
+    else:
+        print(flow_json)
+
+
+def pprint(flow_factory, opts, style=None):
+    """
+    Pretty print the flows
+
+    Args:
+        flow_factory (Callable): Function to call to create the flows
+        opts (dict): Options
+        style (dict): Optional, Style dictionary
+    """
+    console = OFConsole(no_color=opts["no_color"])
+
+    def callback(flow):
+        console.print_flow(flow, style=style)
+
+    with print_context(console.console, opts["paged"], not opts["no_color"]):
+        process_flows(flow_factory, callback, opts.get("filename"), 
opts.get("filter"))
diff --git a/python/setup.py b/python/setup.py
index 4e8a9761a..d6808f87f 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -71,7 +71,8 @@ setup_args = dict(
     author='Open vSwitch',
     author_email='[email protected]',
     packages=['ovs', 'ovs.compat', 'ovs.compat.sortedcontainers',
-              'ovs.db', 'ovs.unixctl', 'ovs.flows'],
+              'ovs.db', 'ovs.unixctl', 'ovs.flows', 'ovs.ofparse'],
+    scripts=['ovs/ofparse/ofparse'],
     keywords=['openvswitch', 'ovs', 'OVSDB'],
     license='Apache 2.0',
     classifiers=[
@@ -87,7 +88,7 @@ setup_args = dict(
     ext_modules=[setuptools.Extension("ovs._json", sources=["ovs/_json.c"],
                                       libraries=['openvswitch'])],
     cmdclass={'build_ext': try_build_ext},
-    install_requires=['sortedcontainers', 'netaddr', 'pyparsing'],
+    install_requires=['sortedcontainers', 'netaddr', 'pyparsing', 'rich', 
'click'],
     extras_require={':sys_platform == "win32"': ['pywin32 >= 1.0']},
 )
 
-- 
2.31.1

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to