2
0
mirror of https://github.com/openvswitch/ovs synced 2025-08-31 14:25:26 +00:00

python: ovs: flowviz: Add datapath html format.

Using the existing FlowTree and HTMLFormatter, create an HTML tree
visualization that also supports collapsing and expanding the entire
flow subtrees.

Examples:
$ ovs-appcl dpctl/dump-flows | ovs-flowviz --highlight drop datapath
html > /tmp/flows.html
$ ovs-appcl dpctl/dump-flows | ovs-flowviz -f "output.port=3"
datapath html > /tmp/flows.html

Both light and dark styles are supported.

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:
Adrian Moreno
2024-09-25 12:52:12 +02:00
committed by Ilya Maximets
parent 60c3a42283
commit f36b06510d
4 changed files with 359 additions and 2 deletions

View File

@@ -71,6 +71,7 @@ ovs_flowviz = \
python/ovs/flowviz/main.py \
python/ovs/flowviz/odp/__init__.py \
python/ovs/flowviz/odp/cli.py \
python/ovs/flowviz/odp/html.py \
python/ovs/flowviz/odp/tree.py \
python/ovs/flowviz/ofp/__init__.py \
python/ovs/flowviz/ofp/cli.py \

View File

@@ -98,6 +98,14 @@ class HTMLBuffer(FlowBuffer):
kv.meta.vstring, style.color if style else "", href
)
def append_value_omitted(self, kv):
"""Append an omitted value.
Args:
kv (KeyValue): the KeyValue instance to append
"""
dots = "." * len(kv.meta.vstring)
return self._append(dots, "", "")
def append_extra(self, extra, style):
"""Append extra string.
Args:
@@ -125,14 +133,15 @@ class HTMLFormatter(FlowFormatter):
self._style_from_opts(opts, "html", HTMLStyle) or FlowStyle()
)
def format_flow(self, buf, flow, highlighted=None):
def format_flow(self, buf, flow, highlighted=None, omitted=None):
"""Formats the flow into the provided buffer as a html object.
Args:
buf (FlowBuffer): the flow buffer to append to
flow (ovs_dbg.OFPFlow): the flow to format
highlighted (list): Optional; list of KeyValues to highlight
omitted (list): Optional; list of KeyValues to omit
"""
return super(HTMLFormatter, self).format_flow(
buf, flow, self.style, highlighted
buf, flow, self.style, highlighted, omitted
)

View File

@@ -15,6 +15,7 @@
import click
from ovs.flowviz.main import maincli
from ovs.flowviz.odp.html import HTMLTreeProcessor
from ovs.flowviz.odp.tree import ConsoleTreeProcessor
from ovs.flowviz.process import (
ConsoleProcessor,
@@ -74,3 +75,12 @@ def tree(opts, heat_map):
)
processor.process()
processor.print()
@datapath.command()
@click.pass_obj
def html(opts):
"""Print the flows in an HTML list sorted by recirc_id."""
processor = HTMLTreeProcessor(opts)
processor.process()
processor.print()

View File

@@ -0,0 +1,337 @@
# 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.
from ovs.flowviz.html_format import HTMLBuffer, HTMLFormatter
from ovs.flowviz.odp.tree import FlowTree
from ovs.flowviz.process import FileProcessor
class HTMLTree:
"""Class capable of printing a FlowTree in HTML."""
BODY_STYLE = """
<style>
body {{
background-color: {bg};
color: {fg};
}}
</style>"""
STYLE = """
<style>
.recirc {
font-weight: bold;
font-family: monospace;
font-size: 1.1rem;
border: 3px solid #ccc;
width: fit-content;
block-size: fit-content;
}
.block-matches {
font-family: monospace;
border: 1px solid #ccc;
width: fit-content;
block-size: fit-content;
margin-left: 1em;
padding: 4px;
}
.block-actions {
font-family: monospace;
border: 2px solid #ccc;
width: fit-content;
block-size: fit-content;
margin-top: 0.5em;
margin-bottom : 2em;
margin-left: 1em;
padding: 4px;
}
/* List styling */
ul il {
list-style-type: disc;
}
.flowlist > li::marker {
list-style: disc;
}
.actions > li::marker {
content: "\\21B3";
font-size: 1.5em;
}
/* Caret styling */
.caret {
cursor: pointer;
user-select: none;
}
.caret::before {
content: "\\25B6";
font-size: 1.2em;
display: inline-block;
margin-right: 5px;cursor: pointer;
}
/* Rotate the caret/arrow icon when clicked on. */
.caret-down::before {
transform: rotate(90deg);
}
.nested {
display: none;
}
.active {
display: block;
}
.focused {
border: 2px solid #0008ff;
}
</style>
""" # noqa: E501
SCRIPT = """
<script>
var caret = document.getElementsByClassName("caret");
var blocks = document.getElementsByClassName("block-matches");
var i;
for (i = 0; i < caret.length; i++) {
caret[i].addEventListener("click", function() {
this.parentElement.querySelector(".nested").classList.toggle("active");
this.classList.toggle("caret-down");
});
}
// Set focus to a flow block, expanding all parent elements.
function setFocus(targetId) {
var target = document.getElementById(targetId);
var others = document.getElementsByClassName("focused");
var i;
for (i = 0; i < others.length; i++) {
others[i].classList.remove("focused");
}
if (target) {
var element = target;
while (element !== null) {
if (element.classList.contains("nested")) {
element.classList.add("active");
}
if (element.previousElementSibling && element.previousElementSibling.classList.contains("caret")) {
element.previousElementSibling.classList.add("caret-down");
}
element = element.parentElement;
}
}
target.classList.toggle("focused");
}
window.addEventListener('hashchange', function () {
var targetId = window.location.hash.substring(1);
setFocus(targetId);
});
</script>
""" # noqa: E501
def __init__(self, name, flowtree, opts):
self.name = name.replace(" ", "_")
self.tree = flowtree
self.opts = opts
self.formatter = HTMLFormatter(opts)
@classmethod
def head(cls):
html = "<head>"
html += cls.STYLE
html += "</head>"
return html
@classmethod
def begin_body(cls, opts):
style = HTMLFormatter(opts).style
bg = (
style.get("background").color
if style.get("background")
else "#f0f0f0"
)
fg = style.get("default").color if style.get("default") else "black"
return cls.BODY_STYLE.format(bg=bg, fg=fg)
@classmethod
def end_body(cls):
return cls.SCRIPT
def format(self):
html_obj = f"<div id=flow_list-{self.name}>"
html_obj += '<ul class="active flowlist">'
for in_port in sorted(self.tree.recirc_nodes[0].keys()):
node = self.tree.recirc_nodes[0][in_port]
if node.visible:
html_obj += "<li>"
html_obj += self.format_recirc_node(node)
html_obj += "</li>"
html_obj += "</ul>"
html_obj += "</div>"
return html_obj
def format_recirc_node(self, node):
html_obj = '<div class="recirc">'
html_obj += "[recirc_id({}) in_port({})]".format(
hex(node.recirc), node.in_port
)
html_obj += "</div>"
html_obj += '<ul class="flowlist">' # nested
for block in node.visible_blocks():
html_block = "<li>"
html_block += self.format_block(block)
html_block += "</li>"
html_obj += html_block
html_obj += "</ul>"
return html_obj
def format_single_block(self, block):
block_id = "block_{}".format(block.flows[0].flow.id)
html_obj = f'<div id="{block_id}" class="block-matches">'
omit_first = {
"actions": "all",
}
omit_rest = {
"actions": "all",
"match": [kv.key for _, kv in block.equal_match],
}
for i, flow in enumerate(filter(lambda x: x.visible, block.flows)):
html_obj += '<div class="flow">'
omit = omit_rest if i > 0 else omit_first
buf = HTMLBuffer()
hl = None
if self.opts.get("highlight"):
result = self.opts.get("highlight").evaluate(flow.flow)
if result:
hl = result.kv
self.formatter.format_flow(buf, flow.flow, hl, omitted=omit)
html_obj += buf.text
html_obj += "</div>"
html_obj += "</div>" # Match list.
html_obj += '<ul class="actions"><li>'
html_obj += "<div>" # Match list.
if block.next_recirc_nodes:
html_obj += '<div class="caret block-actions">'
else:
html_obj += '<div class="block-actions">'
omit = {
"match": "all",
"info": "all",
"ufid": "all",
"dp_extra_info": "all",
}
buf = HTMLBuffer()
buf.append_extra("actions: ", None)
hl = None
if self.opts.get("highlight"):
result = self.opts.get("highlight").evaluate(block.flows[0].flow)
if result:
hl = result.kv
self.formatter.format_flow(buf, block.flows[0].flow, hl, omitted=omit)
html_obj += buf.text
html_obj += "</div>"
return html_obj
def format_block(self, block):
html_obj = self.format_single_block(block)
html_obj += '<ul class="nested recirclist">'
for node in block.next_recirc_nodes:
if node.visible:
html_obj += "<li>"
html_obj += self.format_recirc_node(node)
html_obj += "</li>"
html_obj += "</ul>"
html_obj += "</li>"
html_obj += "</ul>"
return html_obj
class HTMLTreeProcessor(FileProcessor):
def __init__(self, opts):
super().__init__(opts, "odp")
self.trees = {}
self.opts = opts
self.tree = None
self.curr_file = ""
def start_file(self, name, filename):
self.tree = FlowTree()
self.curr_file = name
def start_thread(self, name):
if not self.tree:
self.tree = FlowTree()
def stop_thread(self, name):
full_name = self.curr_file + f" ({name})"
if self.tree:
self.trees[full_name] = self.tree
self.tree = None
def process_flow(self, flow, name):
self.tree.add(flow, self.opts.get("filter"))
def process(self):
super().process(False)
def stop_file(self, name, filename):
if self.tree:
self.trees[name] = self.tree
self.tree = None
def print(self):
html_obj = "<html>"
html_obj += "<head>"
html_obj += HTMLTree.head()
html_obj += "</head>"
html_obj += "<body>"
html_obj += HTMLTree.begin_body(self.opts)
for name, tree in self.trees.items():
tree.build()
html_tree = HTMLTree(name, tree, self.opts)
html_obj += "<div>"
html_obj += "<h2>{}</h2>".format(name)
html_obj += html_tree.format()
html_obj += "</div>"
html_obj += HTMLTree.end_body()
html_obj += "</body>"
html_obj += "</html>"
print(html_obj)