[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