[ovs-dev] [RFC PATCH 05/10] python/ovs-ofparse: Add rich console formatting
Adrian Moreno
amorenoz at redhat.com
Mon Nov 22 15:01:52 UTC 2021
Using 'rich' library, add a console formatter that supports:
- rich style definition based on a configuration file where any
key-value can be asssigned a specific color. The style to use is
selected using the "--style" general option
- paging based on rich's paging support. If colors are printed (i.e: a
style has been specified, the use of a pager that supports colors is
required, e.g: PAGER="less -r")
- supports highlighting. Using the --highlight global options the user
can select which keys will be highlighted and the highlighted style
can be defined using the configuration file
Auxiliary functions
- heat_map: allow calculating the relative value of a field and
selecting its color based on a heat-map pallete
- hash-pallete: calculate the color based on a pseudo-hash of a value.
That way, we can print same values with the same color
Signed-off-by: Adrian Moreno <amorenoz at redhat.com>
---
python/automake.mk | 6 +-
python/ovs/ovs_ofparse/console.py | 206 ++++++++++++++++++++
python/ovs/ovs_ofparse/datapath.py | 20 ++
python/ovs/ovs_ofparse/etc/ovs-ofparse.conf | 76 ++++++++
python/ovs/ovs_ofparse/format.py | 6 -
python/ovs/ovs_ofparse/main.py | 13 +-
python/ovs/ovs_ofparse/openflow.py | 20 ++
python/ovs/ovs_ofparse/process.py | 46 +++++
python/setup.py | 7 +-
9 files changed, 390 insertions(+), 10 deletions(-)
create mode 100644 python/ovs/ovs_ofparse/console.py
create mode 100644 python/ovs/ovs_ofparse/etc/ovs-ofparse.conf
diff --git a/python/automake.mk b/python/automake.mk
index d01278cc2..a951c0fca 100644
--- a/python/automake.mk
+++ b/python/automake.mk
@@ -58,7 +58,9 @@ ovs_pyfiles = \
python/ovs/ovs_ofparse/openflow.py \
python/ovs/ovs_ofparse/ovs-ofparse \
python/ovs/ovs_ofparse/process.py \
- python/ovs/ovs_ofparse/format.py
+ python/ovs/ovs_ofparse/format.py \
+ python/ovs/ovs_ofparse/console.py \
+ python/ovs/ovs_ofparse/etc/ovs-ofparse.conf
@@ -94,7 +96,7 @@ EXTRA_DIST += $(PYFILES)
PYCOV_CLEAN_FILES += $(PYFILES:.py=.py,cover)
FLAKE8_PYFILES += \
- $(filter-out python/ovs/compat/% python/ovs/dirs.py,$(PYFILES)) \
+ $(filter-out %.conf python/ovs/compat/% python/ovs/dirs.py,$(PYFILES)) \
python/setup.py \
python/build/__init__.py \
python/build/nroff.py \
diff --git a/python/ovs/ovs_ofparse/console.py b/python/ovs/ovs_ofparse/console.py
new file mode 100644
index 000000000..3916544a2
--- /dev/null
+++ b/python/ovs/ovs_ofparse/console.py
@@ -0,0 +1,206 @@
+""" This module defines console formatting
+"""
+
+import colorsys
+import contextlib
+import itertools
+import sys
+import zlib
+from rich.console import Console
+from rich.text import Text
+from rich.style import Style
+from rich.color import Color
+from rich.panel import Panel
+from rich.emoji import Emoji
+
+from ovs.ovs_ofparse.format import FlowFormatter, FlowBuffer, FlowStyle
+
+
+def file_header(name):
+ return Panel(
+ Text(
+ Emoji.replace(":scroll:")
+ + " "
+ + name
+ + " "
+ + Emoji.replace(":scroll:"),
+ style="bold",
+ justify="center",
+ )
+ )
+
+
+class ConsoleBuffer(FlowBuffer):
+ """ConsoleBuffer implements FlowBuffer to provide console-based text
+ formatting based on rich.Text
+
+ Append functions accept a rich.Style
+
+ Args:
+ rtext(rich.Text): Optional; text instance to reuse
+ """
+
+ def __init__(self, rtext):
+ self._text = rtext or Text()
+
+ @property
+ def text(self):
+ return self._text
+
+ def _append(self, string, style):
+ """Append to internal text"""
+ return self._text.append(string, style)
+
+ def append_key(self, kv, style):
+ """Append a key
+ Args:
+ kv (KeyValue): the KeyValue instance to append
+ style (rich.Style): the style to use
+ """
+ return self._append(kv.meta.kstring, style)
+
+ def append_delim(self, kv, style):
+ """Append a delimiter
+ Args:
+ kv (KeyValue): the KeyValue instance to append
+ style (rich.Style): the style to use
+ """
+ return self._append(kv.meta.delim, style)
+
+ def append_end_delim(self, kv, style):
+ """Append an end delimiter
+ Args:
+ kv (KeyValue): the KeyValue instance to append
+ style (rich.Style): the style to use
+ """
+ return self._append(kv.meta.end_delim, style)
+
+ def append_value(self, kv, style):
+ """Append a value
+ Args:
+ kv (KeyValue): the KeyValue instance to append
+ style (rich.Style): the style to use
+ """
+ return self._append(kv.meta.vstring, style)
+
+ def append_extra(self, extra, style):
+ """Append extra string
+ Args:
+ kv (KeyValue): the KeyValue instance to append
+ style (rich.Style): the style to use
+ """
+ return self._append(extra, style)
+
+
+class ConsoleFormatter(FlowFormatter):
+ """
+ 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()
+ """
+
+ def __init__(self, opts=None, console=None, **kwargs):
+ super(ConsoleFormatter, self).__init__()
+ style = self.style_from_opts(opts)
+ self.console = console or Console(no_color=(style is None), **kwargs)
+ self.style = style or FlowStyle()
+
+ def style_from_opts(self, opts):
+ return self._style_from_opts(opts, "console", Style)
+
+ def print_flow(self, flow, highlighted=None):
+ """
+ Prints a flow to the console
+
+ Args:
+ flow (ovs_dbg.OFPFlow): the flow to print
+ style (dict): Optional; style dictionary to use
+ highlighted (list): Optional; list of KeyValues to highlight
+ """
+
+ buf = ConsoleBuffer(Text())
+ self.format_flow(buf, flow, highlighted)
+ self.console.print(buf.text)
+
+ def format_flow(self, buf, flow, highlighted=None):
+ """
+ Formats the flow into the provided buffer as a rich.Text
+
+ Args:
+ buf (FlowBuffer): the flow buffer to append to
+ flow (ovs_dbg.OFPFlow): the flow to format
+ style (FlowStyle): Optional; style object to use
+ highlighted (list): Optional; list of KeyValues to highlight
+ """
+ return super(ConsoleFormatter, self).format_flow(
+ buf, flow, self.style, highlighted
+ )
+
+
+def hash_pallete(hue, saturation, value):
+ """Generates a color pallete with the cartesian product
+ of the hsv values provided and returns a callable that assigns a color for
+ each value hash
+ """
+ HSV_tuples = itertools.product(hue, saturation, value)
+ RGB_tuples = map(lambda x: colorsys.hsv_to_rgb(*x), HSV_tuples)
+ styles = [
+ Style(color=Color.from_rgb(r * 255, g * 255, b * 255))
+ for r, g, b in RGB_tuples
+ ]
+
+ def get_style(string):
+ hash_val = zlib.crc32(bytes(str(string), "utf-8"))
+ return styles[hash_val % len(styles)]
+
+ return get_style
+
+
+def heat_pallete(min_value, max_value):
+ """Generates a color pallete based on the 5-color heat pallete so that
+ for each value between min and max a color is returned that represents it's
+ relative size
+ Args:
+ min_value (int): minimum value
+ max_value (int) maximum value
+ """
+ h_min = 0 # red
+ h_max = 220 / 360 # blue
+
+ def heat(value):
+ if max_value == min_value:
+ r, g, b = colorsys.hsv_to_rgb(h_max / 2, 1.0, 1.0)
+ else:
+ normalized = (int(value) - min_value) / (max_value - min_value)
+ hue = ((1 - normalized) + h_min) * (h_max - h_min)
+ r, g, b = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
+ return Style(color=Color.from_rgb(r * 255, g * 255, b * 255))
+
+ return heat
+
+
+def print_context(console, opts):
+ """
+ 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 opts.get("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)
+
+ with_style = opts.get("style") is not None
+
+ return console.pager(styles=with_style)
+
+ return contextlib.nullcontext()
diff --git a/python/ovs/ovs_ofparse/datapath.py b/python/ovs/ovs_ofparse/datapath.py
index 35c278c50..fd64345ce 100644
--- a/python/ovs/ovs_ofparse/datapath.py
+++ b/python/ovs/ovs_ofparse/datapath.py
@@ -4,6 +4,7 @@ from ovs.ovs_ofparse.main import maincli
from ovs.flows.odp import ODPFlowFactory
from ovs.ovs_ofparse.process import (
JSONProcessor,
+ ConsoleProcessor,
)
factory = ODPFlowFactory()
@@ -23,3 +24,22 @@ def json(opts):
proc = JSONProcessor(opts, factory)
proc.process()
print(proc.json_string())
+
+
+ at datapath.command()
+ at click.option(
+ "-h",
+ "--heat-map",
+ is_flag=True,
+ default=False,
+ show_default=True,
+ help="Create heat-map with packet and byte counters",
+)
+ at click.pass_obj
+def console(opts, heat_map):
+ """Print the flows with some style"""
+ proc = ConsoleProcessor(
+ opts, factory, heat_map=["packets", "bytes"] if heat_map else []
+ )
+ proc.process()
+ proc.print()
diff --git a/python/ovs/ovs_ofparse/etc/ovs-ofparse.conf b/python/ovs/ovs_ofparse/etc/ovs-ofparse.conf
new file mode 100644
index 000000000..19bf157df
--- /dev/null
+++ b/python/ovs/ovs_ofparse/etc/ovs-ofparse.conf
@@ -0,0 +1,76 @@
+
+# Create any number of styles.{style_name} sections with a defined style
+# "console" and "html" styles can be defined
+#
+# console styles:
+# Both the color and the underline can be defined
+#
+# [console|html].[key|value|delim||default].OPTIONS.[color|underline]=VALUE
+#
+# Where OPTIONS can be:
+# highlighted: to apply when the key is highlighted
+# type: to apply when the value matches a type (only aplicable to 'value')
+# Special types such as IPAddress or EthMask can be used)
+# (only aplicable to 'value')
+# {key_name}: to apply when the key matches the key_name
+
+[styles.dark]
+
+# defaults for key-values
+console.key.color = #5D86BA
+console.value.color= #B0C4DE
+console.delim.color= #B0C4DE
+console.default.color= #FFFFFF
+
+# defaults for special types
+console.value.type.IPAddress.color = #008700
+console.value.type.IPMask.color = #008700
+console.value.type.EthMask.color = #008700
+
+# dim some long arguments
+console.value.ct.color = bright_black
+console.value.ufid.color = #870000
+console.value.clone.color = bright_black
+console.value.controller.color = bright_black
+
+# show drop and recirculations
+console.key.drop.color = red
+console.key.resubmit.color = #00d700
+console.key.output.color = #00d700
+console.value.output.color = #00d700
+
+# highlights
+console.key.highlighted.color = #f20905
+console.key.highlighted.underline = true
+console.value.highlighted.underline = true
+console.delim.highlighted.underline = true
+
+
+[styles.light]
+# If a color is omitted, the default terminal color will be used
+# highlight keys
+console.key.color = blue
+
+# special types
+console.value.type.IPAddress.color = #008700
+console.value.type.IPMask.color = #008700
+console.value.type.EthMask.color = #008700
+
+# dim long arguments
+console.value.ct.color = bright_black
+console.value.ufid.color = #870000
+console.value.clone.color = bright_black
+console.value.controller.color = bright_black
+
+# show drop and recirculations
+console.key.drop.color = red
+console.key.resubmit.color = #00d700
+console.key.output.color = #005f00
+console.value.output.color = #00d700
+
+# highlights
+console.key.highlighted.color = #f20905
+console.value.highlighted.color = #f20905
+console.key.highlighted.underline = true
+console.value.highlighted.underline = true
+console.delim.highlighted.underline = true
diff --git a/python/ovs/ovs_ofparse/format.py b/python/ovs/ovs_ofparse/format.py
index 110ba39c2..cb455bcf8 100644
--- a/python/ovs/ovs_ofparse/format.py
+++ b/python/ovs/ovs_ofparse/format.py
@@ -34,9 +34,6 @@ class FlowStyle:
def __init__(self, initial=None):
self._styles = initial if initial is not None else dict()
- def set_flag_style(self, kvstyle):
- self._styles["flag"] = kvstyle
-
def set_delim_style(self, kvstyle, highlighted=False):
if highlighted:
self._styles["delim.highlighted"] = kvstyle
@@ -82,9 +79,6 @@ class FlowStyle:
None,
)
- def get_flag_style(self):
- return self._styles.get("flag") or self._styles.get("default")
-
def get_key_style(self, kv, highlighted=False):
key = kv.meta.kstring
diff --git a/python/ovs/ovs_ofparse/main.py b/python/ovs/ovs_ofparse/main.py
index 917de5972..7520d0190 100644
--- a/python/ovs/ovs_ofparse/main.py
+++ b/python/ovs/ovs_ofparse/main.py
@@ -70,6 +70,15 @@ def validate_input(ctx, param, value):
type=click.Path(),
callback=validate_input,
)
+ at click.option(
+ "-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,
+)
@click.option(
"-f",
"--filter",
@@ -87,7 +96,7 @@ def validate_input(ctx, param, value):
show_default=False,
)
@click.pass_context
-def maincli(ctx, config, style, filename, filter, highlight):
+def maincli(ctx, config, style, filename, paged, filter, highlight):
"""
OpenFlow Parse utility.
@@ -98,6 +107,7 @@ def maincli(ctx, config, style, filename, filter, highlight):
"""
ctx.obj = Options()
ctx.obj["filename"] = filename or None
+ ctx.obj["paged"] = paged
if filter:
try:
ctx.obj["filter"] = OFFilter(filter)
@@ -115,6 +125,7 @@ def maincli(ctx, config, style, filename, filter, highlight):
parser.read(config_file)
ctx.obj["config"] = parser
+ ctx.obj["style"] = style
@maincli.command(hidden=True)
diff --git a/python/ovs/ovs_ofparse/openflow.py b/python/ovs/ovs_ofparse/openflow.py
index 178c76a63..190f92bdb 100644
--- a/python/ovs/ovs_ofparse/openflow.py
+++ b/python/ovs/ovs_ofparse/openflow.py
@@ -5,6 +5,7 @@ from ovs.flows.ofp import OFPFlowFactory
from ovs.ovs_ofparse.main import maincli
from ovs.ovs_ofparse.process import (
JSONProcessor,
+ ConsoleProcessor,
)
@@ -25,3 +26,22 @@ def json(opts):
proc = JSONProcessor(opts, factory)
proc.process()
print(proc.json_string())
+
+
+ at openflow.command()
+ at click.option(
+ "-h",
+ "--heat-map",
+ is_flag=True,
+ default=False,
+ show_default=True,
+ help="Create heat-map with packet and byte counters",
+)
+ at click.pass_obj
+def console(opts, heat_map):
+ """Print the flows in the console"""
+ proc = ConsoleProcessor(
+ opts, factory, heat_map=["n_packets", "n_bytes"] if heat_map else []
+ )
+ proc.process()
+ proc.print()
diff --git a/python/ovs/ovs_ofparse/process.py b/python/ovs/ovs_ofparse/process.py
index c01c4b510..c817d0769 100644
--- a/python/ovs/ovs_ofparse/process.py
+++ b/python/ovs/ovs_ofparse/process.py
@@ -5,6 +5,12 @@ import json
import click
from ovs.flows.decoders import FlowEncoder
+from ovs.ovs_ofparse.console import (
+ ConsoleFormatter,
+ print_context,
+ heat_pallete,
+ file_header,
+)
class FlowProcessor(object):
@@ -147,3 +153,43 @@ class JSONProcessor(FlowProcessor):
indent=4,
cls=FlowEncoder,
)
+
+
+class ConsoleProcessor(FlowProcessor):
+ """A generic Console Processor that prints flows into the console"""
+
+ def __init__(self, opts, factory, heat_map=[]):
+ super().__init__(opts, factory)
+ self.heat_map = heat_map
+ self.console = ConsoleFormatter(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 print(self):
+ with print_context(self.console.console, self.opts):
+ for name, flows in self.flows.items():
+ self.console.console.print("\n")
+ self.console.console.print(file_header(name))
+
+ if len(self.heat_map) > 0 and len(self.flows) > 0:
+ for field in self.heat_map:
+ values = [f.info.get(field) or 0 for f in flows]
+ self.console.style.set_value_style(
+ field, heat_pallete(min(values), max(values))
+ )
+
+ for flow in flows:
+ high = None
+ if self.opts.get("highlight"):
+ result = self.opts.get("highlight").evaluate(flow)
+ if result:
+ high = result.kv
+ self.console.print_flow(flow, high)
diff --git a/python/setup.py b/python/setup.py
index 6643f59cd..e347edbc6 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -70,6 +70,7 @@ setup_args = dict(
url='http://www.openvswitch.org/',
author='Open vSwitch',
author_email='dev at openvswitch.org',
+ include_package_data=True,
packages=['ovs', 'ovs.compat', 'ovs.compat.sortedcontainers',
'ovs.db', 'ovs.unixctl', 'ovs.flows', 'ovs.ovs_ofparse'],
keywords=['openvswitch', 'ovs', 'OVSDB'],
@@ -90,8 +91,12 @@ setup_args = dict(
install_requires=['sortedcontainers',
'netaddr',
'pyparsing',
- 'click'],
+ 'click',
+ 'rich'],
scripts=['ovs/ovs_ofparse/ovs-ofparse'],
+ data_files=[
+ ("etc", ["ovs/ovs_ofparse/etc/ovs-ofparse.conf"])
+ ],
extras_require={':sys_platform == "win32"': ['pywin32 >= 1.0']},
)
--
2.31.1
More information about the dev
mailing list