Message ID | 20231201191449.2386134-3-amorenoz@redhat.com |
---|---|
State | RFC |
Delegated to: | Simon Horman |
Headers | show |
Series | Add flow visualization utility | expand |
Context | Check | Description |
---|---|---|
ovsrobot/apply-robot | success | apply and check: success |
ovsrobot/intel-ovs-compilation | fail | test: fail |
ovsrobot/github-robot-_Build_and_Test | fail | github build: failed |
On 1 Dec 2023, at 20:14, Adrian Moreno wrote: > process.py contains a useful base class that processes files > odp.py and ofp.py: contain datapath and openflow subcommand definitions > as well as the first formatting option: json. > > Also, this patch adds basic filtering support. > > Examples: > $ ovs-ofctl dump-flows br-int | ovs-flowviz openflow json > $ ovs-ofctl dump-flows br-int > flows.txt && ovs-flowviz -i flows.txt openflow json > $ ovs-ofctl appctl dpctl/dump-flows | ovs-flowviz -f 'ct' datapath json > $ ovs-ofctl appctl dpctl/dump-flows > flows.txt && ovs-flowviz -i flows.txt -f 'drop' datapath json Some small comments below! > Signed-off-by: Adrian Moreno <amorenoz@redhat.com> > --- > python/automake.mk | 5 +- > python/ovs/flowviz/__init__.py | 2 + > python/ovs/flowviz/main.py | 103 +++++++++++++++++- > python/ovs/flowviz/odp/cli.py | 42 ++++++++ > python/ovs/flowviz/ofp/cli.py | 42 ++++++++ > python/ovs/flowviz/process.py | 192 +++++++++++++++++++++++++++++++++ > 6 files changed, 384 insertions(+), 2 deletions(-) > create mode 100644 python/ovs/flowviz/odp/cli.py > create mode 100644 python/ovs/flowviz/ofp/cli.py > create mode 100644 python/ovs/flowviz/process.py > > diff --git a/python/automake.mk b/python/automake.mk > index 4302f0136..4845565b8 100644 > --- a/python/automake.mk > +++ b/python/automake.mk > @@ -67,8 +67,11 @@ ovs_flowviz = \ > python/ovs/flowviz/__init__.py \ > python/ovs/flowviz/main.py \ > python/ovs/flowviz/odp/__init__.py \ > + python/ovs/flowviz/odp/cli.py \ > python/ovs/flowviz/ofp/__init__.py \ > - python/ovs/flowviz/ovs-flowviz > + python/ovs/flowviz/ofp/cli.py \ > + python/ovs/flowviz/ovs-flowviz \ > + python/ovs/flowviz/process.py > > > # These python files are used at build time but not runtime, > diff --git a/python/ovs/flowviz/__init__.py b/python/ovs/flowviz/__init__.py > index e69de29bb..898dba522 100644 > --- a/python/ovs/flowviz/__init__.py > +++ b/python/ovs/flowviz/__init__.py > @@ -0,0 +1,2 @@ > +import ovs.flowviz.ofp.cli # noqa: F401 > +import ovs.flowviz.odp.cli # noqa: F401 > diff --git a/python/ovs/flowviz/main.py b/python/ovs/flowviz/main.py > index a2d5ca1fa..a45c06e48 100644 > --- a/python/ovs/flowviz/main.py > +++ b/python/ovs/flowviz/main.py > @@ -12,19 +12,67 @@ > # See the License for the specific language governing permissions and > # limitations under the License. > > +import os > + > import click Do we maybe want all imports together and sorted, i.e.: import click import os > +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 Ending comment line with a dot? > + def init(self): > + """Called before the flow processing begins.""" > + pass > + > + def start_file(self, alias, filename): > + """Called before the processing of a file begins. > + Args: > + alias(str): The alias name of the filename > + filename(str): The filename string > + """ > + pass > + > + def create_flow(self, line, idx): > + """Called for each line in the file. > + Args: > + line(str): The flow line > + idx(int): The line index > + > + Returns a Flow. > + Must be implemented by child classes. > + """ > + raise NotImplementedError > + > + def process_flow(self, flow, name): > + """Called for built flow (after filtering). > + Args: > + flow(Flow): The flow created by create_flow > + name(str): The name of the file from which the flow comes > + """ > + raise NotImplementedError > + > + def stop_file(self, alias, filename): > + """Called after the processing of a file ends. > + Args: > + alias(str): The alias name of the filename > + filename(str): The filename string > + """ > + pass > + > + def end(self): > + """Called after the processing ends.""" > + pass > + > + def process(self, do_filter=True): > + idx = 0 > + filenames = self.opts.get("filename") > + filt = self.opts.get("filter") if do_filter else None > + self.init() > + if filenames: > + for alias, filename in filenames: > + try: > + with open(filename) as f: > + self.start_file(alias, filename) > + for line in f: > + flow = self.create_flow(line, idx) > + idx += 1 > + if not flow or (filt and not filt.evaluate(flow)): > + continue > + self.process_flow(flow, alias) > + self.stop_file(alias, filename) > + except IOError as e: > + raise click.BadParameter( > + "Failed to read from file {} ({}): {}".format( > + filename, e.errno, e.strerror > + ) > + ) > + else: > + data = sys.stdin.read() > + self.start_file("stdin", "stdin") > + for line in data.split("\n"): > + line = line.strip() > + if line: > + flow = self.create_flow(line, idx) > + idx += 1 > + if ( > + not flow > + or not getattr(flow, "_sections", None) > + or (filt and not filt.evaluate(flow)) > + ): > + continue > + self.process_flow(flow, "stdin") > + self.stop_file("stdin", "stdin") > + self.end() > + > + > +class DatapathFactory(): > + """A mixin class that creates OpenFlow flows.""" Datapath flows > + > + def create_flow(self, line, idx): > + # Skip strings commonly found in Datapath flow dumps. > + if any(s in line for s in [ > + "flow-dump from the main thread", > + "flow-dump from pmd on core", > + ]): > + return None > + > + return ODPFlow(line, idx) > + > + > +class OpenFlowFactory(): > + """A mixin class that creates Datapath flows.""" OpenFlow flows > + > + def create_flow(self, line, idx): > + # Skip strings commonly found in OpenFlow flow dumps. > + if " reply " in line: > + return None > + > + return OFPFlow(line, idx) > + > + > +class JSONProcessor(FileProcessor): > + """A FileProcessor that prints flows in JSON format.""" > + > + def __init__(self, opts): > + super().__init__(opts) > + self.flows = dict() > + > + def start_file(self, name, filename): > + self.flows_list = list() > + > + def stop_file(self, name, filename): > + self.flows[name] = self.flows_list > + > + def process_flow(self, flow, name): > + self.flows_list.append(flow) > + > + def json_string(self): > + if len(self.flows.keys()) > 1: > + return json.dumps( > + [ > + {"name": name, "flows": [flow.dict() for flow in flows]} > + for name, flows in self.flows.items() > + ], > + indent=4, > + cls=FlowEncoder, > + ) > + return json.dumps( > + [flow.dict() for flow in self.flows_list], > + indent=4, > + cls=FlowEncoder, > + ) > -- > 2.43.0 > > _______________________________________________ > dev mailing list > dev@openvswitch.org > https://mail.openvswitch.org/mailman/listinfo/ovs-dev
On 1/30/24 16:47, Eelco Chaudron wrote: > On 1 Dec 2023, at 20:14, Adrian Moreno wrote: > >> process.py contains a useful base class that processes files >> odp.py and ofp.py: contain datapath and openflow subcommand definitions >> as well as the first formatting option: json. >> >> Also, this patch adds basic filtering support. >> >> Examples: >> $ ovs-ofctl dump-flows br-int | ovs-flowviz openflow json >> $ ovs-ofctl dump-flows br-int > flows.txt && ovs-flowviz -i flows.txt openflow json >> $ ovs-ofctl appctl dpctl/dump-flows | ovs-flowviz -f 'ct' datapath json >> $ ovs-ofctl appctl dpctl/dump-flows > flows.txt && ovs-flowviz -i flows.txt -f 'drop' datapath json > > Some small comments below! > >> Signed-off-by: Adrian Moreno <amorenoz@redhat.com> >> --- >> python/automake.mk | 5 +- >> python/ovs/flowviz/__init__.py | 2 + >> python/ovs/flowviz/main.py | 103 +++++++++++++++++- >> python/ovs/flowviz/odp/cli.py | 42 ++++++++ >> python/ovs/flowviz/ofp/cli.py | 42 ++++++++ >> python/ovs/flowviz/process.py | 192 +++++++++++++++++++++++++++++++++ >> 6 files changed, 384 insertions(+), 2 deletions(-) >> create mode 100644 python/ovs/flowviz/odp/cli.py >> create mode 100644 python/ovs/flowviz/ofp/cli.py >> create mode 100644 python/ovs/flowviz/process.py >> >> diff --git a/python/automake.mk b/python/automake.mk >> index 4302f0136..4845565b8 100644 >> --- a/python/automake.mk >> +++ b/python/automake.mk >> @@ -67,8 +67,11 @@ ovs_flowviz = \ >> python/ovs/flowviz/__init__.py \ >> python/ovs/flowviz/main.py \ >> python/ovs/flowviz/odp/__init__.py \ >> + python/ovs/flowviz/odp/cli.py \ >> python/ovs/flowviz/ofp/__init__.py \ >> - python/ovs/flowviz/ovs-flowviz >> + python/ovs/flowviz/ofp/cli.py \ >> + python/ovs/flowviz/ovs-flowviz \ >> + python/ovs/flowviz/process.py >> >> >> # These python files are used at build time but not runtime, >> diff --git a/python/ovs/flowviz/__init__.py b/python/ovs/flowviz/__init__.py >> index e69de29bb..898dba522 100644 >> --- a/python/ovs/flowviz/__init__.py >> +++ b/python/ovs/flowviz/__init__.py >> @@ -0,0 +1,2 @@ >> +import ovs.flowviz.ofp.cli # noqa: F401 >> +import ovs.flowviz.odp.cli # noqa: F401 >> diff --git a/python/ovs/flowviz/main.py b/python/ovs/flowviz/main.py >> index a2d5ca1fa..a45c06e48 100644 >> --- a/python/ovs/flowviz/main.py >> +++ b/python/ovs/flowviz/main.py >> @@ -12,19 +12,67 @@ >> # See the License for the specific language governing permissions and >> # limitations under the License. >> >> +import os >> + >> import click > > Do we maybe want all imports together and sorted, i.e.: > > import click > import os > Sure. Will do. >> +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 > > Ending comment line with a dot? > Sure. Will do. >> + def init(self): >> + """Called before the flow processing begins.""" >> + pass >> + >> + def start_file(self, alias, filename): >> + """Called before the processing of a file begins. >> + Args: >> + alias(str): The alias name of the filename >> + filename(str): The filename string >> + """ >> + pass >> + >> + def create_flow(self, line, idx): >> + """Called for each line in the file. >> + Args: >> + line(str): The flow line >> + idx(int): The line index >> + >> + Returns a Flow. >> + Must be implemented by child classes. >> + """ >> + raise NotImplementedError >> + >> + def process_flow(self, flow, name): >> + """Called for built flow (after filtering). >> + Args: >> + flow(Flow): The flow created by create_flow >> + name(str): The name of the file from which the flow comes >> + """ >> + raise NotImplementedError >> + >> + def stop_file(self, alias, filename): >> + """Called after the processing of a file ends. >> + Args: >> + alias(str): The alias name of the filename >> + filename(str): The filename string >> + """ >> + pass >> + >> + def end(self): >> + """Called after the processing ends.""" >> + pass >> + >> + def process(self, do_filter=True): >> + idx = 0 >> + filenames = self.opts.get("filename") >> + filt = self.opts.get("filter") if do_filter else None >> + self.init() >> + if filenames: >> + for alias, filename in filenames: >> + try: >> + with open(filename) as f: >> + self.start_file(alias, filename) >> + for line in f: >> + flow = self.create_flow(line, idx) >> + idx += 1 >> + if not flow or (filt and not filt.evaluate(flow)): >> + continue >> + self.process_flow(flow, alias) >> + self.stop_file(alias, filename) >> + except IOError as e: >> + raise click.BadParameter( >> + "Failed to read from file {} ({}): {}".format( >> + filename, e.errno, e.strerror >> + ) >> + ) >> + else: >> + data = sys.stdin.read() >> + self.start_file("stdin", "stdin") >> + for line in data.split("\n"): >> + line = line.strip() >> + if line: >> + flow = self.create_flow(line, idx) >> + idx += 1 >> + if ( >> + not flow >> + or not getattr(flow, "_sections", None) >> + or (filt and not filt.evaluate(flow)) >> + ): >> + continue >> + self.process_flow(flow, "stdin") >> + self.stop_file("stdin", "stdin") >> + self.end() >> + >> + >> +class DatapathFactory(): >> + """A mixin class that creates OpenFlow flows.""" > > Datapath flows > Oh boy, I got it the other way around! xD >> + >> + def create_flow(self, line, idx): >> + # Skip strings commonly found in Datapath flow dumps. >> + if any(s in line for s in [ >> + "flow-dump from the main thread", >> + "flow-dump from pmd on core", >> + ]): >> + return None >> + >> + return ODPFlow(line, idx) >> + >> + >> +class OpenFlowFactory(): >> + """A mixin class that creates Datapath flows.""" > > OpenFlow flows > >> + >> + def create_flow(self, line, idx): >> + # Skip strings commonly found in OpenFlow flow dumps. >> + if " reply " in line: >> + return None >> + >> + return OFPFlow(line, idx) >> + >> + >> +class JSONProcessor(FileProcessor): >> + """A FileProcessor that prints flows in JSON format.""" >> + >> + def __init__(self, opts): >> + super().__init__(opts) >> + self.flows = dict() >> + >> + def start_file(self, name, filename): >> + self.flows_list = list() >> + >> + def stop_file(self, name, filename): >> + self.flows[name] = self.flows_list >> + >> + def process_flow(self, flow, name): >> + self.flows_list.append(flow) >> + >> + def json_string(self): >> + if len(self.flows.keys()) > 1: >> + return json.dumps( >> + [ >> + {"name": name, "flows": [flow.dict() for flow in flows]} >> + for name, flows in self.flows.items() >> + ], >> + indent=4, >> + cls=FlowEncoder, >> + ) >> + return json.dumps( >> + [flow.dict() for flow in self.flows_list], >> + indent=4, >> + cls=FlowEncoder, >> + ) >> -- >> 2.43.0 >> >> _______________________________________________ >> dev mailing list >> dev@openvswitch.org >> https://mail.openvswitch.org/mailman/listinfo/ovs-dev >
diff --git a/python/automake.mk b/python/automake.mk index 4302f0136..4845565b8 100644 --- a/python/automake.mk +++ b/python/automake.mk @@ -67,8 +67,11 @@ ovs_flowviz = \ python/ovs/flowviz/__init__.py \ python/ovs/flowviz/main.py \ python/ovs/flowviz/odp/__init__.py \ + python/ovs/flowviz/odp/cli.py \ python/ovs/flowviz/ofp/__init__.py \ - python/ovs/flowviz/ovs-flowviz + python/ovs/flowviz/ofp/cli.py \ + python/ovs/flowviz/ovs-flowviz \ + python/ovs/flowviz/process.py # These python files are used at build time but not runtime, diff --git a/python/ovs/flowviz/__init__.py b/python/ovs/flowviz/__init__.py index e69de29bb..898dba522 100644 --- a/python/ovs/flowviz/__init__.py +++ b/python/ovs/flowviz/__init__.py @@ -0,0 +1,2 @@ +import ovs.flowviz.ofp.cli # noqa: F401 +import ovs.flowviz.odp.cli # noqa: F401 diff --git a/python/ovs/flowviz/main.py b/python/ovs/flowviz/main.py index a2d5ca1fa..a45c06e48 100644 --- a/python/ovs/flowviz/main.py +++ b/python/ovs/flowviz/main.py @@ -12,19 +12,67 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + import click +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 + def init(self): + """Called before the flow processing begins.""" + pass + + def start_file(self, alias, filename): + """Called before the processing of a file begins. + Args: + alias(str): The alias name of the filename + filename(str): The filename string + """ + pass + + def create_flow(self, line, idx): + """Called for each line in the file. + Args: + line(str): The flow line + idx(int): The line index + + Returns a Flow. + Must be implemented by child classes. + """ + raise NotImplementedError + + def process_flow(self, flow, name): + """Called for built flow (after filtering). + Args: + flow(Flow): The flow created by create_flow + name(str): The name of the file from which the flow comes + """ + raise NotImplementedError + + def stop_file(self, alias, filename): + """Called after the processing of a file ends. + Args: + alias(str): The alias name of the filename + filename(str): The filename string + """ + pass + + def end(self): + """Called after the processing ends.""" + pass + + def process(self, do_filter=True): + idx = 0 + filenames = self.opts.get("filename") + filt = self.opts.get("filter") if do_filter else None + self.init() + if filenames: + for alias, filename in filenames: + try: + with open(filename) as f: + self.start_file(alias, filename) + for line in f: + flow = self.create_flow(line, idx) + idx += 1 + if not flow or (filt and not filt.evaluate(flow)): + continue + self.process_flow(flow, alias) + self.stop_file(alias, filename) + except IOError as e: + raise click.BadParameter( + "Failed to read from file {} ({}): {}".format( + filename, e.errno, e.strerror + ) + ) + else: + data = sys.stdin.read() + self.start_file("stdin", "stdin") + for line in data.split("\n"): + line = line.strip() + if line: + flow = self.create_flow(line, idx) + idx += 1 + if ( + not flow + or not getattr(flow, "_sections", None) + or (filt and not filt.evaluate(flow)) + ): + continue + self.process_flow(flow, "stdin") + self.stop_file("stdin", "stdin") + self.end() + + +class DatapathFactory(): + """A mixin class that creates OpenFlow flows.""" + + def create_flow(self, line, idx): + # Skip strings commonly found in Datapath flow dumps. + if any(s in line for s in [ + "flow-dump from the main thread", + "flow-dump from pmd on core", + ]): + return None + + return ODPFlow(line, idx) + + +class OpenFlowFactory(): + """A mixin class that creates Datapath flows.""" + + def create_flow(self, line, idx): + # Skip strings commonly found in OpenFlow flow dumps. + if " reply " in line: + return None + + return OFPFlow(line, idx) + + +class JSONProcessor(FileProcessor): + """A FileProcessor that prints flows in JSON format.""" + + def __init__(self, opts): + super().__init__(opts) + self.flows = dict() + + def start_file(self, name, filename): + self.flows_list = list() + + def stop_file(self, name, filename): + self.flows[name] = self.flows_list + + def process_flow(self, flow, name): + self.flows_list.append(flow) + + def json_string(self): + if len(self.flows.keys()) > 1: + return json.dumps( + [ + {"name": name, "flows": [flow.dict() for flow in flows]} + for name, flows in self.flows.items() + ], + indent=4, + cls=FlowEncoder, + ) + return json.dumps( + [flow.dict() for flow in self.flows_list], + indent=4, + cls=FlowEncoder, + )
process.py contains a useful base class that processes files odp.py and ofp.py: contain datapath and openflow subcommand definitions as well as the first formatting option: json. Also, this patch adds basic filtering support. Examples: $ ovs-ofctl dump-flows br-int | ovs-flowviz openflow json $ ovs-ofctl dump-flows br-int > flows.txt && ovs-flowviz -i flows.txt openflow json $ ovs-ofctl appctl dpctl/dump-flows | ovs-flowviz -f 'ct' datapath json $ ovs-ofctl appctl dpctl/dump-flows > flows.txt && ovs-flowviz -i flows.txt -f 'drop' datapath json Signed-off-by: Adrian Moreno <amorenoz@redhat.com> --- python/automake.mk | 5 +- python/ovs/flowviz/__init__.py | 2 + python/ovs/flowviz/main.py | 103 +++++++++++++++++- python/ovs/flowviz/odp/cli.py | 42 ++++++++ python/ovs/flowviz/ofp/cli.py | 42 ++++++++ python/ovs/flowviz/process.py | 192 +++++++++++++++++++++++++++++++++ 6 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 python/ovs/flowviz/odp/cli.py create mode 100644 python/ovs/flowviz/ofp/cli.py create mode 100644 python/ovs/flowviz/process.py