+from ovs.flow.filter import OFFilter
+
class Options(dict):
"""Options dictionary"""
+def validate_input(ctx, param, value):
+ """Validate the "-i" option"""
+ result = list()
+ for input_str in value:
+ parts = input_str.strip().split(",")
+ if len(parts) == 2:
+ file_parts = tuple(parts)
+ elif len(parts) == 1:
+ file_parts = tuple(["Filename: " + parts[0], parts[0]])
+ else:
+ raise click.BadParameter(
+ "input filename should have the following format: "
+ "[alias,]FILENAME"
+ )
+
+ if not os.path.isfile(file_parts[1]):
+ raise click.BadParameter(
+ "input filename %s does not exist" % file_parts[1]
+ )
+ result.append(file_parts)
+ return result
+
+
@click.group(
subcommand_metavar="TYPE",
context_settings=dict(help_option_names=["-h", "--help"]),
)
+@click.option(
+ "-i",
+ "--input",
+ "filename",
+ help="Read flows from specified filepath. If not provided, flows will be"
+ " read from stdin. This option can be specified multiple times."
+ " Format [alias,]FILENAME. Where alias is a name that shall be used to"
+ " refer to this FILENAME",
+ multiple=True,
+ type=click.Path(),
+ callback=validate_input,
+)
+@click.option(
+ "-f",
+ "--filter",
+ help="Filter 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):
+def maincli(ctx, filename, filter):
"""
OpenvSwitch flow visualization utility.
@@ -32,6 +80,59 @@ def maincli(ctx):
(such as the output of ovs-ofctl dump-flows or ovs-appctl
dpctl/dump-flows)
and prints them in different formats.
"""
+ ctx.obj = Options()
+ ctx.obj["filename"] = filename or None
+ if filter:
+ try:
+ ctx.obj["filter"] = OFFilter(filter)
+ except Exception as e:
+ raise click.BadParameter("Wrong filter syntax: {}".format(e))
+
+
+@maincli.command(hidden=True)
+@click.pass_context
+def filter(ctx):
+ """
+ \b
+ Filter Syntax
+ *************
+
+ [! | not ] {key}[[.subkey]...] [OPERATOR] {value})] [LOGICAL OPERATOR] ...
+
+ \b
+ Comparison operators are:
+ = equality
+ < less than
+ > more than
+ ~= masking (valid for IP and Ethernet fields)
+
+ \b
+ Logical operators are:
+ !{expr}: NOT
+ {expr} && {expr}: AND
+ {expr} || {expr}: OR
+
+ \b
+ Matches and flow metadata:
+ To compare against a match or info field, use the field directly, e.g:
+ priority=100
+ n_bytes>10
+ Use simple keywords for flags:
+ tcp and ip_src=192.168.1.1
+ \b
+ Actions:
+ Actions values might be dictionaries, use subkeys to access individual
+ values, e.g:
+ output.port=3
+ Use simple keywords for flags
+ drop
+
+ \b
+ Examples of valid filters.
+ nw_addr~=192.168.1.1 && (tcp_dst=80 || tcp_dst=443)
+ arp=true && !arp_tsa=192.168.1.1
+ n_bytes>0 && drop=true"""
+ click.echo(ctx.command.get_help(ctx))
def main():
diff --git a/python/ovs/flowviz/odp/cli.py b/python/ovs/flowviz/odp/cli.py
new file mode 100644
index 000000000..ed2f82065
--- /dev/null
+++ b/python/ovs/flowviz/odp/cli.py
@@ -0,0 +1,42 @@
+# 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 click
+
+from ovs.flowviz.main import maincli
+from ovs.flowviz.process import (
+ DatapathFactory,
+ JSONProcessor,
+)
+
+
+@maincli.group(subcommand_metavar="FORMAT")
+@click.pass_obj
+def datapath(opts):
+ """Process Datapath Flows."""
+ pass
+
+
+class JSONPrint(DatapathFactory, JSONProcessor):
+ def __init__(self, opts):
+ super().__init__(opts)
+
+
+@datapath.command()
+@click.pass_obj
+def json(opts):
+ """Print the flows in JSON format."""
+ proc = JSONPrint(opts)
+ proc.process()
+ print(proc.json_string())
diff --git a/python/ovs/flowviz/ofp/cli.py b/python/ovs/flowviz/ofp/cli.py
new file mode 100644
index 000000000..b9a2a8aad
--- /dev/null
+++ b/python/ovs/flowviz/ofp/cli.py
@@ -0,0 +1,42 @@
+# 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 click
+
+from ovs.flowviz.main import maincli
+from ovs.flowviz.process import (
+ OpenFlowFactory,
+ JSONProcessor,
+)
+
+
+@maincli.group(subcommand_metavar="FORMAT")
+@click.pass_obj
+def openflow(opts):
+ """Process OpenFlow Flows."""
+ pass
+
+
+class JSONPrint(OpenFlowFactory, JSONProcessor):
+ def __init__(self, opts):
+ super().__init__(opts)
+
+
+@openflow.command()
+@click.pass_obj
+def json(opts):
+ """Print the flows in JSON format."""
+ proc = JSONPrint(opts)
+ proc.process()
+ print(proc.json_string())
diff --git a/python/ovs/flowviz/process.py b/python/ovs/flowviz/process.py
new file mode 100644
index 000000000..413506bf2
--- /dev/null
+++ b/python/ovs/flowviz/process.py
@@ -0,0 +1,192 @@
+# 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 sys
+import json
+import click
+
+from ovs.flow.decoders import FlowEncoder
+from ovs.flow.odp import ODPFlow
+from ovs.flow.ofp import OFPFlow
+
+
+class FileProcessor(object):
+ """Base class for file-based Flow processing. It is able to create flows
+ from strings found in a file (or stdin).
+
+ The process of parsing the flows is extendable in many ways by deriving
+ this class.
+
+ When process() is called, the base class will:
+ - call self.start_file() for each new file that get's processed
+ - call self.create_flow() for each flow line
+ - apply the filter defined in opts if provided (can be optionally
+ disabled)
+ - call self.process_flow() for after the flow has been filtered
+ - call self.stop_file() after the file has been processed entirely
+
+ In the case of stdin, the filename and file alias is 'stdin'.
+
+ Child classes must at least implement create_flow() and process_flow()
+ functions.
+
+ Args:
+ opts (dict): Options dictionary
+ """
+
+ def __init__(self, opts):
+ self.opts = opts
+
+ # Methods that must be implemented by derived classes