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