mirror of
https://github.com/openvswitch/ovs
synced 2025-08-31 14:25:26 +00:00
python: ovs: flowviz: Add console formatting.
Add a flow formatting framework and one implementation for console printing using rich. The flow formatting framework is a simple set of classes that can be used to write different flow formatting implementations. It supports styles to be described by any class, highlighting and config-file based style definition. The first flow formatting implementation is also introduced: the ConsoleFormatter. It uses the an advanced rich-text printing library [1]. The console printing supports: - Heatmap: printing the packet/byte statistics of each flow in a color that represents its relative size: blue (low) -> red (high). - Printing a banner with the file name and alias. - Extensive style definition via config file. This console format is added to both OpenFlow and Datapath flows. Examples: - Highlight drops in datapath flows: $ ovs-flowviz -i flows.txt --highlight "drop" datapath console - Quickly detect where most packets are going using heatmap and paginated output: $ ovs-ofctl dump-flows br-int | ovs-flowviz openflow console -h [1] https://rich.readthedocs.io/en/stable/introduction.html Acked-by: Eelco Chaudron <echaudro@redhat.com> Signed-off-by: Adrian Moreno <amorenoz@redhat.com> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
This commit is contained in:
committed by
Ilya Maximets
parent
e3149d4808
commit
ec2646dd43
@@ -65,6 +65,8 @@ ovs_pytests = \
|
||||
|
||||
ovs_flowviz = \
|
||||
python/ovs/flowviz/__init__.py \
|
||||
python/ovs/flowviz/console.py \
|
||||
python/ovs/flowviz/format.py \
|
||||
python/ovs/flowviz/main.py \
|
||||
python/ovs/flowviz/odp/__init__.py \
|
||||
python/ovs/flowviz/odp/cli.py \
|
||||
|
162
python/ovs/flowviz/console.py
Normal file
162
python/ovs/flowviz/console.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# Copyright (c) 2023 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at:
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import colorsys
|
||||
|
||||
from rich.console import Console
|
||||
from rich.color import Color
|
||||
from rich.text import Text
|
||||
from rich.style import Style
|
||||
|
||||
from ovs.flowviz.format import FlowFormatter, FlowBuffer
|
||||
|
||||
|
||||
def file_header(name):
|
||||
return Text(f"### {name} ###")
|
||||
|
||||
|
||||
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):
|
||||
"""ConsoleFormatter is a FlowFormatter that formats flows into the console
|
||||
using rich.Console.
|
||||
|
||||
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__()
|
||||
self.style = self.style_from_opts(opts)
|
||||
self.console = console or Console(color_system="256", **kwargs)
|
||||
|
||||
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 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 default_highlight():
|
||||
"""Generates a default style for highlights."""
|
||||
return Style(underline=True)
|
371
python/ovs/flowviz/format.py
Normal file
371
python/ovs/flowviz/format.py
Normal file
@@ -0,0 +1,371 @@
|
||||
# Copyright (c) 2023 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at:
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Flow formatting framework.
|
||||
|
||||
This file defines a simple flow formatting framework. It's comprised of 3
|
||||
classes: FlowStyle, FlowFormatter and FlowBuffer.
|
||||
|
||||
The FlowStyle arranges opaque style objects in a dictionary that can be queried
|
||||
to determine what style a particular key-value should be formatted with.
|
||||
That way, a particular implementation can represent its style using their own
|
||||
object.
|
||||
|
||||
The FlowBuffer is an abstract class and must be derived by particular
|
||||
implementations. It should know how to append parts of a flow using a style.
|
||||
Only here the type of the style is relevant.
|
||||
|
||||
When asked to format a flow, the FlowFormatter will determine which style
|
||||
the flow must be formatted with and call FlowBuffer functions with each part
|
||||
of the flow and their corresponding style.
|
||||
"""
|
||||
|
||||
|
||||
class FlowStyle:
|
||||
"""A FlowStyle determines the KVStyle to use for each key value in a flow.
|
||||
|
||||
Styles are internally represented by a dictionary.
|
||||
In order to determine the style for a "key", the following items in the
|
||||
dictionary are fetched:
|
||||
- key.highlighted.{key} (if key is found in hightlighted)
|
||||
- key.highlighted (if key is found in hightlighted)
|
||||
- key.{key}
|
||||
- key
|
||||
- default
|
||||
|
||||
In order to determine the style for a "value", the following items in the
|
||||
dictionary are fetched:
|
||||
- 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
|
||||
- default
|
||||
|
||||
The actual type of the style object stored for each item above is opaque
|
||||
to this class and it depends on the particular FlowFormatter child class
|
||||
that will handle them. Even callables can be stored, if so they will be
|
||||
called with the value of the field that is to be formatted and the return
|
||||
object will be used as style.
|
||||
|
||||
Additionally, the following style items can be defined:
|
||||
- delim: for delimiters
|
||||
- delim.highlighted: for delimiters of highlighted key-values
|
||||
"""
|
||||
|
||||
def __init__(self, initial=None):
|
||||
self._styles = initial if initial is not None else dict()
|
||||
|
||||
def __len__(self):
|
||||
return len(self._styles)
|
||||
|
||||
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
|
||||
else:
|
||||
self._styles["delim"] = kvstyle
|
||||
|
||||
def set_default_key_style(self, kvstyle, highlighted=False):
|
||||
if highlighted:
|
||||
self._styles["key.highlighted"] = kvstyle
|
||||
else:
|
||||
self._styles["key"] = kvstyle
|
||||
|
||||
def set_default_value_style(self, kvstyle, highlighted=False):
|
||||
if highlighted:
|
||||
self._styles["value.highlighted"] = kvstyle
|
||||
else:
|
||||
self._styles["value"] = kvstyle
|
||||
|
||||
def set_key_style(self, key, kvstyle, highlighted=False):
|
||||
if highlighted:
|
||||
self._styles["key.highlighted.{}".format(key)] = kvstyle
|
||||
else:
|
||||
self._styles["key.{}".format(key)] = kvstyle
|
||||
|
||||
def set_value_style(self, key, kvstyle, highlighted=None):
|
||||
if highlighted:
|
||||
self._styles["value.highlighted.{}".format(key)] = kvstyle
|
||||
else:
|
||||
self._styles["value.{}".format(key)] = kvstyle
|
||||
|
||||
def set_value_type_style(self, name, kvstyle, highlighted=None):
|
||||
if highlighted:
|
||||
self._styles["value.highlighted.type.{}".format(name)] = kvstyle
|
||||
else:
|
||||
self._styles["value.type.{}".format(name)] = kvstyle
|
||||
|
||||
def get(self, key):
|
||||
return self._styles.get(key)
|
||||
|
||||
def get_delim_style(self, highlighted=False):
|
||||
delim_style_lookup = ["delim.highlighted"] if highlighted else []
|
||||
delim_style_lookup.extend(["delim", "default"])
|
||||
return next(
|
||||
(
|
||||
self._styles.get(s)
|
||||
for s in delim_style_lookup
|
||||
if self._styles.get(s)
|
||||
),
|
||||
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.key
|
||||
|
||||
key_style_lookup = (
|
||||
["key.highlighted.%s" % key, "key.highlighted"]
|
||||
if highlighted
|
||||
else []
|
||||
)
|
||||
key_style_lookup.extend(["key.%s" % key, "key", "default"])
|
||||
|
||||
style = next(
|
||||
(
|
||||
self._styles.get(s)
|
||||
for s in key_style_lookup
|
||||
if self._styles.get(s)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if callable(style):
|
||||
return style(kv.meta.kstring)
|
||||
return style
|
||||
|
||||
def get_value_style(self, kv, highlighted=False):
|
||||
key = kv.key
|
||||
value_type = kv.value.__class__.__name__.lower()
|
||||
value_style_lookup = (
|
||||
[
|
||||
"value.highlighted.%s" % key,
|
||||
"value.highlighted.type.%s" % value_type,
|
||||
"value.highlighted",
|
||||
]
|
||||
if highlighted
|
||||
else []
|
||||
)
|
||||
value_style_lookup.extend(
|
||||
[
|
||||
"value.%s" % key,
|
||||
"value.type.%s" % value_type,
|
||||
"value",
|
||||
"default",
|
||||
]
|
||||
)
|
||||
|
||||
style = next(
|
||||
(
|
||||
self._styles.get(s)
|
||||
for s in value_style_lookup
|
||||
if self._styles.get(s)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if callable(style):
|
||||
return style(kv.meta.vstring)
|
||||
return style
|
||||
|
||||
|
||||
class FlowFormatter:
|
||||
"""FlowFormatter is a base class for Flow Formatters."""
|
||||
|
||||
def __init__(self):
|
||||
self._highlighted = list()
|
||||
|
||||
def _style_from_opts(self, opts, opts_key, style_constructor):
|
||||
"""Create style object from options.
|
||||
|
||||
Args:
|
||||
opts (dict): Options dictionary
|
||||
opts_key (str): The options style key to extract
|
||||
(e.g: console or html)
|
||||
style_constructor(callable): A callable that creates a derived
|
||||
style object
|
||||
"""
|
||||
if not opts or not opts.get("style"):
|
||||
return None
|
||||
|
||||
section_name = ".".join(["styles", opts.get("style")])
|
||||
if section_name not in opts.get("config").sections():
|
||||
return None
|
||||
|
||||
config = opts.get("config")[section_name]
|
||||
style = {}
|
||||
for key in config:
|
||||
(_, console, style_full_key) = key.partition(opts_key + ".")
|
||||
if not console:
|
||||
continue
|
||||
|
||||
(style_key, _, prop) = style_full_key.rpartition(".")
|
||||
if not prop or not style_key:
|
||||
raise Exception("malformed style config: {}".format(key))
|
||||
|
||||
if not style.get(style_key):
|
||||
style[style_key] = {}
|
||||
style[style_key][prop] = config[key]
|
||||
|
||||
return FlowStyle({k: style_constructor(**v) for k, v in style.items()})
|
||||
|
||||
def format_flow(self, buf, flow, style_obj=None, highlighted=None):
|
||||
"""Formats the flow into the provided buffer.
|
||||
|
||||
Args:
|
||||
buf (FlowBuffer): the flow buffer to append to
|
||||
flow (ovs_dbg.OFPFlow): the flow to format
|
||||
style_obj (FlowStyle): Optional; style to use
|
||||
highlighted (list): Optional; list of KeyValues to highlight
|
||||
"""
|
||||
last_printed_pos = 0
|
||||
|
||||
if style_obj:
|
||||
style_obj = style_obj or FlowStyle()
|
||||
for section in sorted(flow.sections, key=lambda x: x.pos):
|
||||
buf.append_extra(
|
||||
flow.orig[last_printed_pos : section.pos],
|
||||
style=style_obj.get("default"),
|
||||
)
|
||||
self.format_kv_list(
|
||||
buf, section.data, section.string, style_obj, highlighted
|
||||
)
|
||||
last_printed_pos = section.pos + len(section.string)
|
||||
else:
|
||||
# Don't pay the cost of formatting each section one by one.
|
||||
buf.append_extra(flow.orig.strip(), None)
|
||||
|
||||
def format_kv_list(self, buf, kv_list, full_str, style_obj, highlighted):
|
||||
"""Format a KeyValue List.
|
||||
|
||||
Args:
|
||||
buf (FlowBuffer): a FlowBuffer to append formatted KeyValues to
|
||||
kv_list (list[KeyValue]: the KeyValue list to format
|
||||
full_str (str): the full string containing all k-v
|
||||
style_obj (FlowStyle): a FlowStyle object to use
|
||||
highlighted (list): Optional; list of KeyValues to highlight
|
||||
"""
|
||||
for i, kv in enumerate(kv_list):
|
||||
written = self.format_kv(
|
||||
buf, kv, style_obj=style_obj, highlighted=highlighted
|
||||
)
|
||||
|
||||
end = (
|
||||
kv_list[i + 1].meta.kpos
|
||||
if i < (len(kv_list) - 1)
|
||||
else len(full_str)
|
||||
)
|
||||
|
||||
buf.append_extra(
|
||||
full_str[(kv.meta.kpos + written) : end].rstrip("\n\r"),
|
||||
style=style_obj.get("default"),
|
||||
)
|
||||
|
||||
def format_kv(self, buf, kv, style_obj, highlighted=None):
|
||||
"""Format a KeyValue
|
||||
|
||||
A formatted keyvalue has the following parts:
|
||||
{key}{delim}{value}[{delim}]
|
||||
|
||||
Args:
|
||||
buf (FlowBuffer): buffer to append the KeyValue to
|
||||
kv (KeyValue): The KeyValue to print
|
||||
style_obj (FlowStyle): The style object to use
|
||||
highlighted (list): Optional; list of KeyValues to highlight
|
||||
|
||||
Returns the number of printed characters.
|
||||
"""
|
||||
ret = 0
|
||||
key = kv.meta.kstring
|
||||
is_highlighted = (
|
||||
key in [k.key for k in highlighted] if highlighted else False
|
||||
)
|
||||
|
||||
key_style = style_obj.get_key_style(kv, is_highlighted)
|
||||
buf.append_key(kv, key_style) # format value
|
||||
ret += len(key)
|
||||
|
||||
if not kv.meta.vstring:
|
||||
return ret
|
||||
|
||||
if kv.meta.delim not in ("\n", "\t", "\r", ""):
|
||||
buf.append_delim(kv, style_obj.get_delim_style(is_highlighted))
|
||||
ret += len(kv.meta.delim)
|
||||
|
||||
value_style = style_obj.get_value_style(kv, is_highlighted)
|
||||
buf.append_value(kv, value_style) # format value
|
||||
ret += len(kv.meta.vstring)
|
||||
|
||||
if kv.meta.end_delim:
|
||||
buf.append_end_delim(kv, style_obj.get_delim_style(is_highlighted))
|
||||
ret += len(kv.meta.end_delim)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class FlowBuffer:
|
||||
"""A FlowBuffer is a base class for format buffers.
|
||||
|
||||
Childs must implement the following methods:
|
||||
append_key(self, kv, style)
|
||||
append_value(self, kv, style)
|
||||
append_delim(self, delim, style)
|
||||
append_end_delim(self, delim, style)
|
||||
append_extra(self, extra, style)
|
||||
"""
|
||||
|
||||
def append_key(self, kv, style):
|
||||
"""Append a key.
|
||||
Args:
|
||||
kv (KeyValue): the KeyValue instance to append
|
||||
style (Any): the style to use
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def append_delim(self, kv, style):
|
||||
"""Append a delimiter.
|
||||
Args:
|
||||
kv (KeyValue): the KeyValue instance to append
|
||||
style (Any): the style to use
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def append_end_delim(self, kv, style):
|
||||
"""Append an end delimiter.
|
||||
Args:
|
||||
kv (KeyValue): the KeyValue instance to append
|
||||
style (Any): the style to use
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def append_value(self, kv, style):
|
||||
"""Append a value.
|
||||
Args:
|
||||
kv (KeyValue): the KeyValue instance to append
|
||||
style (Any): the style to use
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def append_extra(self, extra, style):
|
||||
"""Append extra string.
|
||||
Args:
|
||||
kv (KeyValue): the KeyValue instance to append
|
||||
style (Any): the style to use
|
||||
"""
|
||||
raise NotImplementedError
|
@@ -12,10 +12,30 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import configparser
|
||||
import click
|
||||
import os
|
||||
|
||||
from ovs.flow.filter import OFFilter
|
||||
from ovs.dirs import PKGDATADIR
|
||||
|
||||
_default_config_file = "ovs-flowviz.conf"
|
||||
_default_config_path = next(
|
||||
(
|
||||
p
|
||||
for p in [
|
||||
os.path.join(
|
||||
os.getenv("HOME"), ".config", "ovs", _default_config_file
|
||||
),
|
||||
os.path.join(PKGDATADIR, _default_config_file),
|
||||
os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), _default_config_file)
|
||||
),
|
||||
]
|
||||
if os.path.exists(p)
|
||||
),
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
class Options(dict):
|
||||
@@ -48,6 +68,20 @@ def validate_input(ctx, param, value):
|
||||
@click.group(
|
||||
context_settings=dict(help_option_names=["-h", "--help"]),
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
help="Use config file",
|
||||
type=click.Path(),
|
||||
default=_default_config_path,
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--style",
|
||||
help="Select style (defined in config file)",
|
||||
default=None,
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--input",
|
||||
@@ -69,8 +103,17 @@ def validate_input(ctx, param, value):
|
||||
type=str,
|
||||
show_default=False,
|
||||
)
|
||||
@click.option(
|
||||
"-l",
|
||||
"--highlight",
|
||||
help="Highlight flows that match the filter expression."
|
||||
"Run 'ovs-flowviz filter' for a detailed description of the filtering "
|
||||
"syntax",
|
||||
type=str,
|
||||
show_default=False,
|
||||
)
|
||||
@click.pass_context
|
||||
def maincli(ctx, filename, filter):
|
||||
def maincli(ctx, config, style, filename, filter, highlight):
|
||||
"""
|
||||
OpenvSwitch flow visualization utility.
|
||||
|
||||
@@ -86,6 +129,19 @@ def maincli(ctx, filename, filter):
|
||||
except Exception as e:
|
||||
raise click.BadParameter("Wrong filter syntax: {}".format(e))
|
||||
|
||||
if highlight:
|
||||
try:
|
||||
ctx.obj["highlight"] = OFFilter(highlight)
|
||||
except Exception as e:
|
||||
raise click.BadParameter("Wrong filter syntax: {}".format(e))
|
||||
|
||||
config_file = config or _default_config_path
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(config_file)
|
||||
|
||||
ctx.obj["config"] = parser
|
||||
ctx.obj["style"] = style
|
||||
|
||||
|
||||
@maincli.command(hidden=True)
|
||||
@click.pass_context
|
||||
|
@@ -15,7 +15,10 @@
|
||||
import click
|
||||
|
||||
from ovs.flowviz.main import maincli
|
||||
from ovs.flowviz.process import JSONDatapathProcessor
|
||||
from ovs.flowviz.process import (
|
||||
ConsoleProcessor,
|
||||
JSONDatapathProcessor,
|
||||
)
|
||||
|
||||
|
||||
@maincli.group(subcommand_metavar="FORMAT")
|
||||
@@ -32,3 +35,22 @@ def json(opts):
|
||||
proc = JSONDatapathProcessor(opts)
|
||||
proc.process()
|
||||
print(proc.json_string())
|
||||
|
||||
|
||||
@datapath.command()
|
||||
@click.option(
|
||||
"-h",
|
||||
"--heat-map",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
show_default=True,
|
||||
help="Create heat-map with packet and byte counters",
|
||||
)
|
||||
@click.pass_obj
|
||||
def console(opts, heat_map):
|
||||
"""Print the flows in the console with some style."""
|
||||
proc = ConsoleProcessor(
|
||||
opts, "odp", heat_map=["packets", "bytes"] if heat_map else []
|
||||
)
|
||||
proc.process()
|
||||
proc.print()
|
||||
|
@@ -15,7 +15,10 @@
|
||||
import click
|
||||
|
||||
from ovs.flowviz.main import maincli
|
||||
from ovs.flowviz.process import JSONOpenFlowProcessor
|
||||
from ovs.flowviz.process import (
|
||||
ConsoleProcessor,
|
||||
JSONOpenFlowProcessor,
|
||||
)
|
||||
|
||||
|
||||
@maincli.group(subcommand_metavar="FORMAT")
|
||||
@@ -32,3 +35,24 @@ def json(opts):
|
||||
proc = JSONOpenFlowProcessor(opts)
|
||||
proc.process()
|
||||
print(proc.json_string())
|
||||
|
||||
|
||||
@openflow.command()
|
||||
@click.option(
|
||||
"-h",
|
||||
"--heat-map",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
show_default=True,
|
||||
help="Create heat-map with packet and byte counters",
|
||||
)
|
||||
@click.pass_obj
|
||||
def console(opts, heat_map):
|
||||
"""Print the flows in the console with some style."""
|
||||
proc = ConsoleProcessor(
|
||||
opts,
|
||||
"ofp",
|
||||
heat_map=["n_packets", "n_bytes"] if heat_map else [],
|
||||
)
|
||||
proc.process()
|
||||
proc.print()
|
||||
|
@@ -20,6 +20,14 @@ from ovs.flow.decoders import FlowEncoder
|
||||
from ovs.flow.odp import ODPFlow
|
||||
from ovs.flow.ofp import OFPFlow
|
||||
|
||||
from ovs.flowviz.console import (
|
||||
ConsoleFormatter,
|
||||
default_highlight,
|
||||
file_header,
|
||||
heat_pallete,
|
||||
)
|
||||
from ovs.flowviz.format import FlowStyle
|
||||
|
||||
|
||||
class FileProcessor(object):
|
||||
"""Base class for file-based Flow processing. It is able to create flows
|
||||
@@ -253,3 +261,84 @@ class JSONDatapathProcessor(FileProcessor):
|
||||
return json.dumps(
|
||||
thread_data(next(iter(self.data.values()))), **opts
|
||||
)
|
||||
|
||||
|
||||
class ConsoleProcessor(FileProcessor):
|
||||
"""A generic Console Processor that prints flows into the console"""
|
||||
|
||||
def __init__(self, opts, flow_type, heat_map=[]):
|
||||
super().__init__(opts, flow_type)
|
||||
self.heat_map = heat_map
|
||||
self.console = ConsoleFormatter(opts)
|
||||
if not self.console.style and self.opts.get("highlight"):
|
||||
# Add some style to highlights or else they won't be seen.
|
||||
self.console.style = FlowStyle()
|
||||
self.console.style.set_default_value_style(
|
||||
default_highlight(), True
|
||||
)
|
||||
self.console.style.set_default_key_style(default_highlight(), True)
|
||||
|
||||
self.flows = dict() # Dict of flow-lists, one per file and thread.
|
||||
self.min_max = dict() # Used for heat-map calculation.
|
||||
self.curr_file = None
|
||||
self.flows_list = None
|
||||
|
||||
def _init_list(self):
|
||||
self.flows_list = list()
|
||||
if len(self.heat_map) > 0:
|
||||
self.min = [-1] * len(self.heat_map)
|
||||
self.max = [0] * len(self.heat_map)
|
||||
|
||||
def _save_list(self, name):
|
||||
if self.flows_list:
|
||||
self.flows[name] = self.flows_list
|
||||
self.flows_list = None
|
||||
if len(self.heat_map) > 0:
|
||||
self.min_max[name] = (self.min, self.max)
|
||||
|
||||
def start_file(self, name, filename):
|
||||
self._init_list()
|
||||
self.curr_file = name
|
||||
|
||||
def start_thread(self, name):
|
||||
if not self.flows_list:
|
||||
self._init_list()
|
||||
|
||||
def stop_thread(self, name):
|
||||
full_name = self.curr_file + f" ({name})"
|
||||
self._save_list(full_name)
|
||||
|
||||
def stop_file(self, name, filename):
|
||||
self._save_list(name)
|
||||
|
||||
def process_flow(self, flow, name):
|
||||
# Running calculation of min and max values for all the fields that
|
||||
# take place in the heatmap.
|
||||
for i, field in enumerate(self.heat_map):
|
||||
val = flow.info.get(field)
|
||||
if self.min[i] == -1 or val < self.min[i]:
|
||||
self.min[i] = val
|
||||
if val > self.max[i]:
|
||||
self.max[i] = val
|
||||
|
||||
self.flows_list.append(flow)
|
||||
|
||||
def print(self):
|
||||
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 i, field in enumerate(self.heat_map):
|
||||
(min_val, max_val) = self.min_max[name][i]
|
||||
self.console.style.set_value_style(
|
||||
field, heat_pallete(min_val, max_val)
|
||||
)
|
||||
|
||||
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)
|
||||
|
@@ -105,9 +105,11 @@ setup_args = dict(
|
||||
extras_require={':sys_platform == "win32"': ['pywin32 >= 1.0'],
|
||||
'dns': ['unbound'],
|
||||
'flow': flow_extras_require,
|
||||
'flowviz': [*flow_extras_require, 'click'],
|
||||
'flowviz':
|
||||
[*flow_extras_require, 'click', 'rich'],
|
||||
},
|
||||
scripts=["ovs/flowviz/ovs-flowviz"],
|
||||
include_package_data=True,
|
||||
)
|
||||
|
||||
try:
|
||||
|
Reference in New Issue
Block a user