@@ -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 \
@@ -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
)
@@ -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()
new file mode 100644
@@ -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)
Using the existing FlowTree and HTMLFormatter, create an HTML tree visualization that also supports collapsing and expanding 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. Signed-off-by: Adrian Moreno <amorenoz@redhat.com> --- python/automake.mk | 1 + python/ovs/flowviz/html_format.py | 13 +- python/ovs/flowviz/odp/cli.py | 10 + python/ovs/flowviz/odp/html.py | 337 ++++++++++++++++++++++++++++++ 4 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 python/ovs/flowviz/odp/html.py