From patchwork Wed Jul 10 17:04:58 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: =?utf-8?q?Adri=C3=A1n_Moreno?= X-Patchwork-Id: 1958932 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=ECYSHaie; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org (client-ip=140.211.166.133; helo=smtp2.osuosl.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org) Received: from smtp2.osuosl.org (smtp2.osuosl.org [140.211.166.133]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4WK44d5Dtcz1xpN for ; Thu, 11 Jul 2024 03:05:53 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by smtp2.osuosl.org (Postfix) with ESMTP id 1BA6140FF7; Wed, 10 Jul 2024 17:05:52 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp2.osuosl.org ([127.0.0.1]) by localhost (smtp2.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id BWXRqYPDIXQs; Wed, 10 Jul 2024 17:05:49 +0000 (UTC) X-Comment: SPF check N/A for local connections - client-ip=140.211.9.56; helo=lists.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver= DKIM-Filter: OpenDKIM Filter v2.11.0 smtp2.osuosl.org 02EC34102A Authentication-Results: smtp2.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=ECYSHaie Received: from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56]) by smtp2.osuosl.org (Postfix) with ESMTPS id 02EC34102A; Wed, 10 Jul 2024 17:05:47 +0000 (UTC) Received: from lf-lists.osuosl.org (localhost [127.0.0.1]) by lists.linuxfoundation.org (Postfix) with ESMTP id 9E61BC0A98; Wed, 10 Jul 2024 17:05:47 +0000 (UTC) X-Original-To: dev@openvswitch.org Delivered-To: ovs-dev@lists.linuxfoundation.org Received: from smtp1.osuosl.org (smtp1.osuosl.org [140.211.166.138]) by lists.linuxfoundation.org (Postfix) with ESMTP id 27305C0A9D for ; Wed, 10 Jul 2024 17:05:46 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by smtp1.osuosl.org (Postfix) with ESMTP id 6E92582197 for ; Wed, 10 Jul 2024 17:05:36 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp1.osuosl.org ([127.0.0.1]) by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id Mz4QqBFKg22X for ; Wed, 10 Jul 2024 17:05:34 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=170.10.133.124; helo=us-smtp-delivery-124.mimecast.com; envelope-from=amorenoz@redhat.com; receiver= DMARC-Filter: OpenDMARC Filter v1.4.2 smtp1.osuosl.org 6A09A8211D Authentication-Results: smtp1.osuosl.org; dmarc=pass (p=none dis=none) header.from=redhat.com DKIM-Filter: OpenDKIM Filter v2.11.0 smtp1.osuosl.org 6A09A8211D Authentication-Results: smtp1.osuosl.org; dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=ECYSHaie Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by smtp1.osuosl.org (Postfix) with ESMTPS id 6A09A8211D for ; Wed, 10 Jul 2024 17:05:34 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1720631133; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=buZ8D0P6j0VQzrurrvi4m7f8Ks62JbU95qJCI/d33bY=; b=ECYSHaie05pFssbW1e712ufBCWSi6ud1texAOc1/mS4x2v0NUaX59cTib4vvB0LlyJ7mJA 2wX2jp1EvRSX6Q83v7cznhvz2AVWipa7FBsKwWTIBY9/MnHUi7xxBSInt5sTC7yuGsWwfz Vnk51qHz3MP8U8poLwWgGLiefW1yOz0= Received: from mx-prod-mc-01.mail-002.prod.us-west-2.aws.redhat.com (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-168-W8gbkomgPViaJumWwysQVA-1; Wed, 10 Jul 2024 13:05:31 -0400 X-MC-Unique: W8gbkomgPViaJumWwysQVA-1 Received: from mx-prod-int-02.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-02.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.15]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id 2172E196E099 for ; Wed, 10 Jul 2024 17:05:31 +0000 (UTC) Received: from antares.redhat.com (unknown [10.39.192.91]) by mx-prod-int-02.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id EF3881955F3B; Wed, 10 Jul 2024 17:05:29 +0000 (UTC) From: Adrian Moreno To: dev@openvswitch.org Date: Wed, 10 Jul 2024 19:04:58 +0200 Message-ID: <20240710170504.2162803-12-amorenoz@redhat.com> In-Reply-To: <20240710170504.2162803-1-amorenoz@redhat.com> References: <20240710170504.2162803-1-amorenoz@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.0 on 10.30.177.15 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Subject: [ovs-dev] [PATCH v5 11/13] python: ovs: flowviz: Add datapath html format. X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: ovs-dev-bounces@openvswitch.org Sender: "dev" 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 Acked-by: Eelco Chaudron --- 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 diff --git a/python/automake.mk b/python/automake.mk index 9640b5886..d534b52d9 100644 --- a/python/automake.mk +++ b/python/automake.mk @@ -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 \ diff --git a/python/ovs/flowviz/html_format.py b/python/ovs/flowviz/html_format.py index 3f3550da5..3293089e1 100644 --- a/python/ovs/flowviz/html_format.py +++ b/python/ovs/flowviz/html_format.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 ) diff --git a/python/ovs/flowviz/odp/cli.py b/python/ovs/flowviz/odp/cli.py index 36f5b3db2..73fadef95 100644 --- a/python/ovs/flowviz/odp/cli.py +++ b/python/ovs/flowviz/odp/cli.py @@ -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() diff --git a/python/ovs/flowviz/odp/html.py b/python/ovs/flowviz/odp/html.py new file mode 100644 index 000000000..48a2c82d0 --- /dev/null +++ b/python/ovs/flowviz/odp/html.py @@ -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 = """ + + """ # noqa: E501 + + 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 = "" + html += cls.STYLE + html += "" + + 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"
" + + html_obj += '
    ' + for in_port in sorted(self.tree.recirc_nodes[0].keys()): + node = self.tree.recirc_nodes[0][in_port] + if node.visible: + html_obj += "
  • " + html_obj += self.format_recirc_node(node) + html_obj += "
  • " + + html_obj += "
" + html_obj += "
" + return html_obj + + def format_recirc_node(self, node): + html_obj = '
' + html_obj += "[recirc_id({}) in_port({})]".format( + hex(node.recirc), node.in_port + ) + html_obj += "
" + + html_obj += '
    ' # nested + + for block in node.visible_blocks(): + html_block = "
  • " + html_block += self.format_block(block) + html_block += "
  • " + html_obj += html_block + + html_obj += "
" + return html_obj + + def format_single_block(self, block): + block_id = "block_{}".format(block.flows[0].flow.id) + html_obj = f'
' + + 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 += '
' + + 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 += "
" + + html_obj += "
" # Match list. + + html_obj += '
  • ' + html_obj += "
    " # Match list. + if block.next_recirc_nodes: + html_obj += '
    ' + else: + html_obj += '
    ' + + 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 += "
    " + return html_obj + + def format_block(self, block): + html_obj = self.format_single_block(block) + + html_obj += '
      ' + for node in block.next_recirc_nodes: + if node.visible: + html_obj += "
    • " + html_obj += self.format_recirc_node(node) + html_obj += "
    • " + html_obj += "
    " + html_obj += "
  • " + html_obj += "
" + 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_obj += "" + html_obj += HTMLTree.head() + html_obj += "" + + html_obj += "" + 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 += "
" + html_obj += "

{}

".format(name) + html_obj += html_tree.format() + html_obj += "
" + + html_obj += HTMLTree.end_body() + html_obj += "" + html_obj += "" + print(html_obj)