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 <[email protected]> --- 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()) + + [email protected]() [email protected]( + "-h", + "--heat-map", + is_flag=True, + default=False, + show_default=True, + help="Create heat-map with packet and byte counters", +) [email protected]_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, ) [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, +) @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()) + + [email protected]() [email protected]( + "-h", + "--heat-map", + is_flag=True, + default=False, + show_default=True, + help="Create heat-map with packet and byte counters", +) [email protected]_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='[email protected]', + 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 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
