diff mbox series

[ovs-dev,v2] dpctl: add add-flows command to dpctl

Message ID 160155937152.470185.6352533521999573298.stgit@ebuild
State Changes Requested
Headers show
Series [ovs-dev,v2] dpctl: add add-flows command to dpctl | expand

Commit Message

Eelco Chaudron Oct. 1, 2020, 1:37 p.m. UTC
When you would like to add, modify, or delete a lot of flows in the
datapath, for example when you want to measure performance, adding
one flow at the time won't scale. This as it takes a decent amount
of time to set up the datapath connection.

This new command is in-line with the same command available in
ovs-ofctl which allows the same thing, with the only difference that
we do not verify all lines before we start execution. This allows for
a continuous add/delete stream. For example with a command like this:

python3 -c 'while True:
  for i in range(0, 1000):
    print("add in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{}) 1".format(int(i / 256), i % 256))
  for i in range(0, 1000):
    print("delete in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{})".format(int(i / 256), i % 256))' \
|  sudo utilities/ovs-dpctl add-flows -


Signed-off-by: Eelco Chaudron <echaudro@redhat.com>
---
v2: - Added change to NEWS
    - Updated man page to be more clear

 NEWS                  |    3 +
 lib/dpctl.c           |  179 ++++++++++++++++++++++++++++++++++++++++++++-----
 lib/dpctl.man         |   11 +++
 utilities/ovs-dpctl.c |    5 +
 4 files changed, 177 insertions(+), 21 deletions(-)

Comments

Aaron Conole Oct. 1, 2020, 5:47 p.m. UTC | #1
Eelco Chaudron <echaudro@redhat.com> writes:

> When you would like to add, modify, or delete a lot of flows in the
> datapath, for example when you want to measure performance, adding
> one flow at the time won't scale. This as it takes a decent amount
> of time to set up the datapath connection.
>
> This new command is in-line with the same command available in
> ovs-ofctl which allows the same thing, with the only difference that
> we do not verify all lines before we start execution. This allows for
> a continuous add/delete stream. For example with a command like this:
>
> python3 -c 'while True:
>   for i in range(0, 1000):
>     print("add in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{}) 1".format(int(i / 256), i % 256))
>   for i in range(0, 1000):
>     print("delete in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{})".format(int(i / 256), i % 256))' \
> |  sudo utilities/ovs-dpctl add-flows -
>
>
> Signed-off-by: Eelco Chaudron <echaudro@redhat.com>
> ---

Acked-by: Aaron Conole <aconole@redhat.com>
Ilya Maximets Oct. 8, 2020, 3:43 p.m. UTC | #2
On 10/1/20 3:37 PM, Eelco Chaudron wrote:
> When you would like to add, modify, or delete a lot of flows in the
> datapath, for example when you want to measure performance, adding
> one flow at the time won't scale. This as it takes a decent amount
> of time to set up the datapath connection.
> 
> This new command is in-line with the same command available in
> ovs-ofctl which allows the same thing, with the only difference that
> we do not verify all lines before we start execution. This allows for
> a continuous add/delete stream. For example with a command like this:
> 
> python3 -c 'while True:
>   for i in range(0, 1000):
>     print("add in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{}) 1".format(int(i / 256), i % 256))
>   for i in range(0, 1000):
>     print("delete in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{})".format(int(i / 256), i % 256))' \
> |  sudo utilities/ovs-dpctl add-flows -
> 
> 
> Signed-off-by: Eelco Chaudron <echaudro@redhat.com>
> ---
> v2: - Added change to NEWS
>     - Updated man page to be more clear
> 
>  NEWS                  |    3 +
>  lib/dpctl.c           |  179 ++++++++++++++++++++++++++++++++++++++++++++-----
>  lib/dpctl.man         |   11 +++
>  utilities/ovs-dpctl.c |    5 +
>  4 files changed, 177 insertions(+), 21 deletions(-)
> 
> diff --git a/NEWS b/NEWS
> index 015facff5..93909ab07 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -35,7 +35,8 @@ Post-v2.13.0
>     - Tunnels: TC Flower offload
>       * Tunnel Local endpoint address masked match are supported.
>       * Tunnel Romte endpoint address masked match are supported.
> -
> +   - 'ovs-dpctl add-flows' command has been added which allows adding,
> +     deleting, or modifying flows based on information read from a file.

This in a wrong section.

Anyway, this looks a bit wierd that in order to delete flows I need to
run 'add-flows' with 'delete' argument.
IIUC, ability to add/remove/modify flows within single add-flows command
of ovs-ofclt was added to utilize OF1.4 --bundle support, i.e. to make
such modifications atomically.  So, this is additional functionality, not
the basic one.  This is a bit annoying that I can not delete flows using
the same file that I used for addition.  Basic commands are missing.
Maybe it's better to add 'del-flows' and, probably, 'mod-flows' commands
so it will be easier to use?

Best regards, Ilya Maximets.
Eelco Chaudron Oct. 8, 2020, 3:50 p.m. UTC | #3
On 8 Oct 2020, at 17:43, Ilya Maximets wrote:

> On 10/1/20 3:37 PM, Eelco Chaudron wrote:
>> When you would like to add, modify, or delete a lot of flows in the
>> datapath, for example when you want to measure performance, adding
>> one flow at the time won't scale. This as it takes a decent amount
>> of time to set up the datapath connection.
>>
>> This new command is in-line with the same command available in
>> ovs-ofctl which allows the same thing, with the only difference that
>> we do not verify all lines before we start execution. This allows for
>> a continuous add/delete stream. For example with a command like this:
>>
>> python3 -c 'while True:
>>   for i in range(0, 1000):
>>     print("add in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{}) 
>> 1".format(int(i / 256), i % 256))
>>   for i in range(0, 1000):
>>     print("delete 
>> in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{})".format(int(i 
>> / 256), i % 256))' \
>> |  sudo utilities/ovs-dpctl add-flows -
>>
>>
>> Signed-off-by: Eelco Chaudron <echaudro@redhat.com>
>> ---
>> v2: - Added change to NEWS
>>     - Updated man page to be more clear
>>
>>  NEWS                  |    3 +
>>  lib/dpctl.c           |  179 
>> ++++++++++++++++++++++++++++++++++++++++++++-----
>>  lib/dpctl.man         |   11 +++
>>  utilities/ovs-dpctl.c |    5 +
>>  4 files changed, 177 insertions(+), 21 deletions(-)
>>
>> diff --git a/NEWS b/NEWS
>> index 015facff5..93909ab07 100644
>> --- a/NEWS
>> +++ b/NEWS
>> @@ -35,7 +35,8 @@ Post-v2.13.0
>>     - Tunnels: TC Flower offload
>>       * Tunnel Local endpoint address masked match are supported.
>>       * Tunnel Romte endpoint address masked match are supported.
>> -
>> +   - 'ovs-dpctl add-flows' command has been added which allows 
>> adding,
>> +     deleting, or modifying flows based on information read from a 
>> file.
>
> This in a wrong section.

Guess it should be in  “Linux datapath:”?

> Anyway, this looks a bit wierd that in order to delete flows I need to
> run 'add-flows' with 'delete' argument.
> IIUC, ability to add/remove/modify flows within single add-flows 
> command
> of ovs-ofclt was added to utilize OF1.4 --bundle support, i.e. to make
> such modifications atomically.  So, this is additional functionality, 
> not
> the basic one.  This is a bit annoying that I can not delete flows 
> using
> the same file that I used for addition.  Basic commands are missing.
> Maybe it's better to add 'del-flows' and, probably, 'mod-flows' 
> commands
> so it will be easier to use?

I still would like to do add/delete/mod in a single file, as it will 
help testing. However, I could add ‘del-flows' and, probably, 
'mod-flows' which will use their perspective name as the default action?
Ilya Maximets Oct. 8, 2020, 4 p.m. UTC | #4
On 10/8/20 5:50 PM, Eelco Chaudron wrote:
> 
> 
> On 8 Oct 2020, at 17:43, Ilya Maximets wrote:
> 
>> On 10/1/20 3:37 PM, Eelco Chaudron wrote:
>>> When you would like to add, modify, or delete a lot of flows in the
>>> datapath, for example when you want to measure performance, adding
>>> one flow at the time won't scale. This as it takes a decent amount
>>> of time to set up the datapath connection.
>>>
>>> This new command is in-line with the same command available in
>>> ovs-ofctl which allows the same thing, with the only difference that
>>> we do not verify all lines before we start execution. This allows for
>>> a continuous add/delete stream. For example with a command like this:
>>>
>>> python3 -c 'while True:
>>>   for i in range(0, 1000):
>>>     print("add in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{}) 1".format(int(i / 256), i % 256))
>>>   for i in range(0, 1000):
>>>     print("delete in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{})".format(int(i / 256), i % 256))' \
>>> |  sudo utilities/ovs-dpctl add-flows -
>>>
>>>
>>> Signed-off-by: Eelco Chaudron <echaudro@redhat.com>
>>> ---
>>> v2: - Added change to NEWS
>>>     - Updated man page to be more clear
>>>
>>>  NEWS                  |    3 +
>>>  lib/dpctl.c           |  179 ++++++++++++++++++++++++++++++++++++++++++++-----
>>>  lib/dpctl.man         |   11 +++
>>>  utilities/ovs-dpctl.c |    5 +
>>>  4 files changed, 177 insertions(+), 21 deletions(-)
>>>
>>> diff --git a/NEWS b/NEWS
>>> index 015facff5..93909ab07 100644
>>> --- a/NEWS
>>> +++ b/NEWS
>>> @@ -35,7 +35,8 @@ Post-v2.13.0
>>>     - Tunnels: TC Flower offload
>>>       * Tunnel Local endpoint address masked match are supported.
>>>       * Tunnel Romte endpoint address masked match are supported.
>>> -
>>> +   - 'ovs-dpctl add-flows' command has been added which allows adding,
>>> +     deleting, or modifying flows based on information read from a file.
>>
>> This in a wrong section.
> 
> Guess it should be in  “Linux datapath:”?

It should be in Post-v2.14.0. :)
And, please, keep 2 empty lines between releases.

For the entry itself, it might look like:

   - ovs-dpctl and 'ovs-appctl dpctl/':
     * New command ...

> 
>> Anyway, this looks a bit wierd that in order to delete flows I need to
>> run 'add-flows' with 'delete' argument.
>> IIUC, ability to add/remove/modify flows within single add-flows command
>> of ovs-ofclt was added to utilize OF1.4 --bundle support, i.e. to make
>> such modifications atomically.  So, this is additional functionality, not
>> the basic one.  This is a bit annoying that I can not delete flows using
>> the same file that I used for addition.  Basic commands are missing.
>> Maybe it's better to add 'del-flows' and, probably, 'mod-flows' commands
>> so it will be easier to use?
> 
> I still would like to do add/delete/mod in a single file, as it will help testing.

Sure.

> However, I could add ‘del-flows' and, probably, 'mod-flows' which will use their perspective name as the default action?

Yes, that would be good.

Additionally it might make sense to add some tests to tests/dpctl.at.
Just basic things, i.e. add and del some flows with single commands
and via flows listed in some file.

Best reards, Ilya Maximets.
Eelco Chaudron Oct. 13, 2020, 8:42 a.m. UTC | #5
On 8 Oct 2020, at 18:00, Ilya Maximets wrote:

> On 10/8/20 5:50 PM, Eelco Chaudron wrote:
>>
>>
>> On 8 Oct 2020, at 17:43, Ilya Maximets wrote:
>>
>>> On 10/1/20 3:37 PM, Eelco Chaudron wrote:
>>>> When you would like to add, modify, or delete a lot of flows in the
>>>> datapath, for example when you want to measure performance, adding
>>>> one flow at the time won't scale. This as it takes a decent amount
>>>> of time to set up the datapath connection.
>>>>
>>>> This new command is in-line with the same command available in
>>>> ovs-ofctl which allows the same thing, with the only difference 
>>>> that
>>>> we do not verify all lines before we start execution. This allows 
>>>> for
>>>> a continuous add/delete stream. For example with a command like 
>>>> this:
>>>>
>>>> python3 -c 'while True:
>>>>   for i in range(0, 1000):
>>>>     print("add 
>>>> in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{}) 
>>>> 1".format(int(i / 256), i % 256))
>>>>   for i in range(0, 1000):
>>>>     print("delete 
>>>> in_port(0),eth(),eth_type(0x800),ipv4(src=100.1.{}.{})".format(int(i 
>>>> / 256), i % 256))' \
>>>> |  sudo utilities/ovs-dpctl add-flows -
>>>>
>>>>
>>>> Signed-off-by: Eelco Chaudron <echaudro@redhat.com>
>>>> ---
>>>> v2: - Added change to NEWS
>>>>     - Updated man page to be more clear
>>>>
>>>>  NEWS                  |    3 +
>>>>  lib/dpctl.c           |  179 
>>>> ++++++++++++++++++++++++++++++++++++++++++++-----
>>>>  lib/dpctl.man         |   11 +++
>>>>  utilities/ovs-dpctl.c |    5 +
>>>>  4 files changed, 177 insertions(+), 21 deletions(-)
>>>>
>>>> diff --git a/NEWS b/NEWS
>>>> index 015facff5..93909ab07 100644
>>>> --- a/NEWS
>>>> +++ b/NEWS
>>>> @@ -35,7 +35,8 @@ Post-v2.13.0
>>>>     - Tunnels: TC Flower offload
>>>>       * Tunnel Local endpoint address masked match are 
>>>> supported.
>>>>       * Tunnel Romte endpoint address masked match are 
>>>> supported.
>>>> -
>>>> +   - 'ovs-dpctl add-flows' command has been added which allows 
>>>> adding,
>>>> +     deleting, or modifying flows based on information read 
>>>> from a file.
>>>
>>> This in a wrong section.
>>
>> Guess it should be in  “Linux datapath:”?
>
> It should be in Post-v2.14.0. :)
> And, please, keep 2 empty lines between releases.
>
> For the entry itself, it might look like:
>
>    - ovs-dpctl and 'ovs-appctl dpctl/':
>      * New command ...

Ack, just sent a v3.

>>
>>> Anyway, this looks a bit wierd that in order to delete flows I need 
>>> to
>>> run 'add-flows' with 'delete' argument.
>>> IIUC, ability to add/remove/modify flows within single add-flows 
>>> command
>>> of ovs-ofclt was added to utilize OF1.4 --bundle support, i.e. to 
>>> make
>>> such modifications atomically.  So, this is additional 
>>> functionality, not
>>> the basic one.  This is a bit annoying that I can not delete flows 
>>> using
>>> the same file that I used for addition.  Basic commands are 
>>> missing.
>>> Maybe it's better to add 'del-flows' and, probably, 'mod-flows' 
>>> commands
>>> so it will be easier to use?
>>
>> I still would like to do add/delete/mod in a single file, as it will 
>> help testing.
>
> Sure.
>
>> However, I could add ‘del-flows' and, probably, 'mod-flows' which 
>> will use their perspective name as the default action?
>
> Yes, that would be good.

Done in v3, as well as the tests suggested below.

> Additionally it might make sense to add some tests to tests/dpctl.at.
> Just basic things, i.e. add and del some flows with single commands
> and via flows listed in some file.
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index 015facff5..93909ab07 100644
--- a/NEWS
+++ b/NEWS
@@ -35,7 +35,8 @@  Post-v2.13.0
    - Tunnels: TC Flower offload
      * Tunnel Local endpoint address masked match are supported.
      * Tunnel Romte endpoint address masked match are supported.
-
+   - 'ovs-dpctl add-flows' command has been added which allows adding,
+     deleting, or modifying flows based on information read from a file.
 
 v2.13.0 - 14 Feb 2020
 ---------------------
diff --git a/lib/dpctl.c b/lib/dpctl.c
index db2b1f896..eba2bdfa2 100644
--- a/lib/dpctl.c
+++ b/lib/dpctl.c
@@ -52,6 +52,12 @@ 
 #include "openvswitch/ofp-flow.h"
 #include "openvswitch/ofp-port.h"
 
+enum {
+    DPCTL_FLOWS_ADD = 0,
+    DPCTL_FLOWS_DEL,
+    DPCTL_FLOWS_MOD
+};
+
 typedef int dpctl_command_handler(int argc, const char *argv[],
                                   struct dpctl_params *);
 struct dpctl_command {
@@ -1102,28 +1108,22 @@  out_free:
 }
 
 static int
-dpctl_put_flow(int argc, const char *argv[], enum dpif_flow_put_flags flags,
-               struct dpctl_params *dpctl_p)
+dpctl_put_flow_dpif(struct dpif *dpif, const char *key_s,
+                    const char *actions_s,
+                    enum dpif_flow_put_flags flags,
+                    struct dpctl_params *dpctl_p)
 {
-    const char *key_s = argv[argc - 2];
-    const char *actions_s = argv[argc - 1];
     struct dpif_flow_stats stats;
     struct dpif_port dpif_port;
     struct dpif_port_dump port_dump;
     struct ofpbuf actions;
     struct ofpbuf key;
     struct ofpbuf mask;
-    struct dpif *dpif;
     ovs_u128 ufid;
     bool ufid_present;
     struct simap port_names;
     int n, error;
 
-    error = opt_dpif_open(argc, argv, dpctl_p, 4, &dpif);
-    if (error) {
-        return error;
-    }
-
     ufid_present = false;
     n = odp_ufid_from_string(key_s, &ufid);
     if (n < 0) {
@@ -1185,6 +1185,24 @@  out_freeactions:
 out_freekeymask:
     ofpbuf_uninit(&mask);
     ofpbuf_uninit(&key);
+    return error;
+}
+
+static int
+dpctl_put_flow(int argc, const char *argv[], enum dpif_flow_put_flags flags,
+               struct dpctl_params *dpctl_p)
+{
+    struct dpif *dpif;
+    int error;
+
+    error = opt_dpif_open(argc, argv, dpctl_p, 4, &dpif);
+    if (error) {
+        return error;
+    }
+
+    error = dpctl_put_flow_dpif(dpif, argv[argc - 2], argv[argc - 1], flags,
+                                dpctl_p);
+
     dpif_close(dpif);
     return error;
 }
@@ -1258,25 +1276,20 @@  out:
 }
 
 static int
-dpctl_del_flow(int argc, const char *argv[], struct dpctl_params *dpctl_p)
+dpctl_del_flow_dpif(struct dpif *dpif, const char *key_s,
+                    struct dpctl_params *dpctl_p)
 {
-    const char *key_s = argv[argc - 1];
     struct dpif_flow_stats stats;
     struct dpif_port dpif_port;
     struct dpif_port_dump port_dump;
     struct ofpbuf key;
     struct ofpbuf mask; /* To be ignored. */
-    struct dpif *dpif;
+
     ovs_u128 ufid;
     bool ufid_present;
     struct simap port_names;
     int n, error;
 
-    error = opt_dpif_open(argc, argv, dpctl_p, 3, &dpif);
-    if (error) {
-        return error;
-    }
-
     ufid_present = false;
     n = odp_ufid_from_string(key_s, &ufid);
     if (n < 0) {
@@ -1334,6 +1347,23 @@  out:
     ofpbuf_uninit(&mask);
     ofpbuf_uninit(&key);
     simap_destroy(&port_names);
+    return error;
+}
+
+static int
+dpctl_del_flow(int argc, const char *argv[], struct dpctl_params *dpctl_p)
+{
+    const char *key_s = argv[argc - 1];
+    struct dpif *dpif;
+    int error;
+
+    error = opt_dpif_open(argc, argv, dpctl_p, 3, &dpif);
+    if (error) {
+        return error;
+    }
+
+    error = dpctl_del_flow_dpif(dpif, key_s, dpctl_p);
+
     dpif_close(dpif);
     return error;
 }
@@ -1356,6 +1386,118 @@  dpctl_del_flows(int argc, const char *argv[], struct dpctl_params *dpctl_p)
     return error;
 }
 
+static int
+dpctl_parse_add_flows_line(struct ds *s, char **flow, char **action)
+{
+    int command = DPCTL_FLOWS_ADD;
+    const char *line = ds_cstr(s);
+    size_t len;
+
+    /* First figure out the command, or fallback to FLOWS_ADD. */
+    line += strspn(line, " \t\r\n");
+    len = strcspn(line, ", \t\r\n");
+
+    if (!strncmp(line, "add", len)) {
+         command = DPCTL_FLOWS_ADD;
+    } else if (!strncmp(line, "delete", len)) {
+        command = DPCTL_FLOWS_DEL;
+    } else if (!strncmp(line, "modify", len)) {
+        command = DPCTL_FLOWS_MOD;
+    } else {
+        len = 0;
+    }
+    line += len;
+
+    /* Isolate flow and action (for add/modify). */
+    line += strspn(line, " \t\r\n");
+    len = strcspn(line, " \t\r\n");
+
+    if (len == 0) {
+        *flow = NULL;
+        *action = NULL;
+        return command;
+    }
+
+    *flow = strndup(line, len);
+
+    line += len;
+    line += strspn(line, " \t\r\n");
+    if (strlen(line)) {
+        *action = xstrdup(line);
+    } else {
+        *action = NULL;
+    }
+
+    return command;
+}
+
+static int
+dpctl_add_flows(int argc, const char *argv[], struct dpctl_params *dpctl_p)
+{
+    const char *file_name = argv[argc - 1];
+    int line_number = 0;
+    struct dpif *dpif;
+    struct ds line;
+    FILE *stream;
+    int error;
+
+    error = opt_dpif_open(argc, argv, dpctl_p, 4, &dpif);
+    if (error) {
+        return error;
+    }
+
+    stream = !strcmp(file_name, "-") ? stdin : fopen(file_name, "r");
+    if (!stream) {
+        error = errno;
+        dpctl_error(dpctl_p, error, "Opening file \"%s\" failed", file_name);
+        goto out_close_dpif;
+    }
+
+    ds_init(&line);
+    while (!ds_get_preprocessed_line(&line, stream, &line_number)) {
+        /* We do not process all the lines first and then execute the actions
+         * as we would like to take commands as a continuous stream of
+         * commands from stdin.
+         */
+        char *flow = NULL;
+        char *action = NULL;
+        int cmd = dpctl_parse_add_flows_line(&line, &flow, &action);
+
+        if ((!flow && !action)
+            || ((cmd == DPCTL_FLOWS_ADD || cmd == DPCTL_FLOWS_MOD) && !action)
+            || (cmd == DPCTL_FLOWS_DEL && action)) {
+            dpctl_error(dpctl_p, 0,
+                        "Failed parsing line number %u, skipped!",
+                        line_number);
+        } else {
+            switch (cmd) {
+            case DPCTL_FLOWS_ADD:
+                dpctl_put_flow_dpif(dpif, flow, action,
+                                    DPIF_FP_CREATE, dpctl_p);
+                break;
+            case DPCTL_FLOWS_MOD:
+                dpctl_put_flow_dpif(dpif, flow, action,
+                                    DPIF_FP_MODIFY, dpctl_p);
+                break;
+            case DPCTL_FLOWS_DEL:
+                dpctl_del_flow_dpif(dpif, flow, dpctl_p);
+                break;
+            }
+        }
+
+        free(flow);
+        free(action);
+    }
+
+    ds_destroy(&line);
+    if (stream != stdin) {
+        fclose(stream);
+    }
+out_close_dpif:
+    dpif_close(dpif);
+    return 0;
+}
+
 static int
 dpctl_help(int argc OVS_UNUSED, const char *argv[] OVS_UNUSED,
            struct dpctl_params *dpctl_p)
@@ -2506,6 +2648,7 @@  static const struct dpctl_command all_commands[] = {
     { "dump-flows", "[dp] [filter=..] [type=..]",
       0, 3, dpctl_dump_flows, DP_RO },
     { "add-flow", "[dp] flow actions", 2, 3, dpctl_add_flow, DP_RW },
+    { "add-flows", "[dp] file", 1, 2, dpctl_add_flows, DP_RW },
     { "mod-flow", "[dp] flow actions", 2, 3, dpctl_mod_flow, DP_RW },
     { "get-flow", "[dp] ufid", 1, 2, dpctl_get_flow, DP_RO },
     { "del-flow", "[dp] flow", 1, 2, dpctl_del_flow, DP_RW },
diff --git a/lib/dpctl.man b/lib/dpctl.man
index 727d1f7be..2addea521 100644
--- a/lib/dpctl.man
+++ b/lib/dpctl.man
@@ -194,6 +194,17 @@  ovs-dpctl add-flow myDP \\
 .
 .RE
 .TP
+.DO "\*(DX\fBadd\-flows\fR [\fIdp\fR] \fIfile\fR"
+Reads flow entries from \fIfile\fR (or \fBstdin\fR if \fIfile\fR is
+\fB\-\fR) and adds each entry to the datapath.
+.
+Each flow specification (e.g., each line in \fIfile\fR) may start with
+\fBadd\fR, \fBmodify\fR, or \fBdelete\fR keyword to specify whether a
+flow is to be added, modified, or deleted. A flow specification without
+one of these keywords is treated as a flow add.  All flow modifications
+are executed as individual transactions in the order specified.
+.
+.TP
 .DO "[\fB\-s\fR | \fB\-\-statistics\fR]" "\*(DX\fBdel\-flow\fR" "[\fIdp\fR] \fIflow\fR"
 Deletes the flow from \fIdp\fR's flow table that matches \fIflow\fR.
 If \fB\-s\fR or \fB\-\-statistics\fR is specified, then
diff --git a/utilities/ovs-dpctl.c b/utilities/ovs-dpctl.c
index 7d99607f4..3ff3f714c 100644
--- a/utilities/ovs-dpctl.c
+++ b/utilities/ovs-dpctl.c
@@ -191,6 +191,7 @@  usage(void *userdata OVS_UNUSED)
            "  show DP...               show basic info on each DP\n"
            "  dump-flows [DP]          display flows in DP\n"
            "  add-flow [DP] FLOW ACTIONS add FLOW with ACTIONS to DP\n"
+           "  add-flows [DP] FILE        add flows from FILE\n"
            "  mod-flow [DP] FLOW ACTIONS change FLOW actions to ACTIONS in DP\n"
            "  get-flow [DP] ufid:UFID    fetch flow corresponding to UFID\n"
            "  del-flow [DP] FLOW         delete FLOW from DP\n"
@@ -205,8 +206,8 @@  usage(void *userdata OVS_UNUSED)
            "Each IFACE on add-dp, add-if, and set-if may be followed by\n"
            "comma-separated options.  See ovs-dpctl(8) for syntax, or the\n"
            "Interface table in ovs-vswitchd.conf.db(5) for an options list.\n"
-           "For COMMAND dump-flows, add-flow, mod-flow, del-flow and\n"
-           "del-flows, DP is optional if there is only one datapath.\n",
+           "For COMMAND dump-flows, add-flow, add-flows, mod-flow, del-flow\n"
+           "and del-flows, DP is optional if there is only one datapath.\n",
            program_name, program_name);
     vlog_usage();
     printf("\nOptions for show and mod-flow:\n"