diff mbox

[ovs-dev,v2,1/2] Add dry-run option to ovs-dpctl and ovs-ofctl commands.

Message ID 1470333335-11989-2-git-send-email-rmoats@us.ibm.com
State Changes Requested
Headers show

Commit Message

Ryan Moats Aug. 4, 2016, 5:55 p.m. UTC
ovs-dpctl and ovs-ofctl lack a dry-run option.  Add it
and the necessary scaffolding to each.

Signed-off-by: Ryan Moats <rmoats@us.ibm.com>
---

v1->v2:
 Fixed typo in usage string

 lib/command-line.c             |  50 +++++++++----
 lib/command-line.h             |   6 +-
 lib/db-ctl-base.h              |   2 +-
 lib/dpctl.c                    |  44 ++++++------
 lib/dpctl.h                    |   3 +
 ovsdb/ovsdb-tool.c             |  28 ++++----
 tests/ovstest.c                |   4 +-
 tests/test-bitmap.c            |   6 +-
 tests/test-ccmap.c             |   6 +-
 tests/test-classifier.c        |  28 ++++----
 tests/test-cmap.c              |   6 +-
 tests/test-conntrack.c         |   6 +-
 tests/test-heap.c              |  14 ++--
 tests/test-jsonrpc.c           |  10 +--
 tests/test-netlink-conntrack.c |   8 +--
 tests/test-ovn.c               |  24 +++----
 tests/test-ovsdb.c             |  78 ++++++++++----------
 tests/test-reconnect.c         |  32 ++++-----
 tests/test-util.c              |  34 ++++-----
 tests/test-vconn.c             |  18 ++---
 utilities/ovs-dpctl.c          |   7 ++
 utilities/ovs-ofctl.8.in       |   3 +
 utilities/ovs-ofctl.c          | 160 ++++++++++++++++++++++-------------------
 23 files changed, 317 insertions(+), 260 deletions(-)

Comments

Ben Pfaff Aug. 14, 2016, 5:36 a.m. UTC | #1
On Thu, Aug 04, 2016 at 05:55:34PM +0000, Ryan Moats wrote:
> ovs-dpctl and ovs-ofctl lack a dry-run option.  Add it
> and the necessary scaffolding to each.
> 
> Signed-off-by: Ryan Moats <rmoats@us.ibm.com>

The point behind a --dry-run option is supposed to be that it does
everything up to but not actually including modifying state.  For
example, in ovs-vsctl it actually connects to the database and does the
whole operation up to the point where it sends the transaction to the
remote database, at which point it just doesn't do it.  This is valuable
for the user because it means that all of the syntax gets checked and
most of the semantics (e.g. "ovs-vsctl --dry-run del-br br0" will check
that a bridge br0 exists).

As implemented by this patch, though, --dry-run does nothing at all
beyond verifying that there is a plausible number of parameters to a
command that might modify state.  Then it exits silently and
successfully, giving the user the impression that something happened.

So, I think that such an option should be named something different, say
--read-only, and should cause the utility to exit with an error message.

I think that's a pretty simple change to the series so perhaps we can
still get this in on Monday?
Ryan Moats Aug. 14, 2016, 11:59 p.m. UTC | #2
Ben Pfaff <blp@ovn.org> wrote on 08/14/2016 12:36:31 AM:

> From: Ben Pfaff <blp@ovn.org>
> To: Ryan Moats/Omaha/IBM@IBMUS
> Cc: dev@openvswitch.org
> Date: 08/14/2016 12:36 AM
> Subject: Re: [ovs-dev] [PATCH v2 1/2] Add dry-run option to ovs-
> dpctl and ovs-ofctl commands.
>
> On Thu, Aug 04, 2016 at 05:55:34PM +0000, Ryan Moats wrote:
> > ovs-dpctl and ovs-ofctl lack a dry-run option.  Add it
> > and the necessary scaffolding to each.
> >
> > Signed-off-by: Ryan Moats <rmoats@us.ibm.com>
>
> The point behind a --dry-run option is supposed to be that it does
> everything up to but not actually including modifying state.  For
> example, in ovs-vsctl it actually connects to the database and does the
> whole operation up to the point where it sends the transaction to the
> remote database, at which point it just doesn't do it.  This is valuable
> for the user because it means that all of the syntax gets checked and
> most of the semantics (e.g. "ovs-vsctl --dry-run del-br br0" will check
> that a bridge br0 exists).
>
> As implemented by this patch, though, --dry-run does nothing at all
> beyond verifying that there is a plausible number of parameters to a
> command that might modify state.  Then it exits silently and
> successfully, giving the user the impression that something happened.
>
> So, I think that such an option should be named something different, say
> --read-only, and should cause the utility to exit with an error message.
>
> I think that's a pretty simple change to the series so perhaps we can
> still get this in on Monday?
>

Working on it right now...

On a side note - I'm starting to see consistent failures in the bfd - decay
case...
Is this just me, or are others also seeing it?
Ben Pfaff Aug. 15, 2016, 12:53 a.m. UTC | #3
On Sun, Aug 14, 2016 at 06:59:09PM -0500, Ryan Moats wrote:
> On a side note - I'm starting to see consistent failures in the bfd - decay
> case...
> Is this just me, or are others also seeing it?

We've had at least one report, but I haven't been seeing it myself.
diff mbox

Patch

diff --git a/lib/command-line.c b/lib/command-line.c
index bda5ed6..7f30646 100644
--- a/lib/command-line.c
+++ b/lib/command-line.c
@@ -87,20 +87,11 @@  ovs_cmdl_print_options(const struct option options[])
     ds_destroy(&ds);
 }
 
-/* Runs the command designated by argv[0] within the command table specified by
- * 'commands', which must be terminated by a command whose 'name' member is a
- * null pointer.
- *
- * Command-line options should be stripped off, so that a typical invocation
- * looks like:
- *    struct ovs_cmdl_context ctx = {
- *        .argc = argc - optind,
- *        .argv = argv + optind,
- *    };
- *    ovs_cmdl_run_command(&ctx, my_commands);
- * */
-void
-ovs_cmdl_run_command(struct ovs_cmdl_context *ctx, const struct ovs_cmdl_command commands[])
+/* Whether to commit changes or not. */
+static bool dry_run; 
+
+static void
+_ovs_cmdl_run_command(struct ovs_cmdl_context *ctx, const struct ovs_cmdl_command commands[])
 {
     const struct ovs_cmdl_command *p;
 
@@ -118,6 +109,9 @@  ovs_cmdl_run_command(struct ovs_cmdl_context *ctx, const struct ovs_cmdl_command
                 VLOG_FATAL("'%s' command takes at most %d arguments",
                            p->name, p->max_args);
             } else {
+                if (p->mode == OVS_RW && dry_run) {
+                    return;
+                }
                 p->handler(ctx);
                 if (ferror(stdout)) {
                     VLOG_FATAL("write to stdout failed");
@@ -132,6 +126,34 @@  ovs_cmdl_run_command(struct ovs_cmdl_context *ctx, const struct ovs_cmdl_command
 
     VLOG_FATAL("unknown command '%s'; use --help for help", ctx->argv[0]);
 }
+
+/* Runs the command designated by argv[0] within the command table specified by
+ * 'commands', which must be terminated by a command whose 'name' member is a
+ * null pointer.
+ *
+ * Command-line options should be stripped off, so that a typical invocation
+ * looks like:
+ *    struct ovs_cmdl_context ctx = {
+ *        .argc = argc - optind,
+ *        .argv = argv + optind,
+ *    };
+ *    ovs_cmdl_run_command(&ctx, my_commands);
+ * */
+void
+ovs_cmdl_run_command(struct ovs_cmdl_context *ctx,
+                     const struct ovs_cmdl_command commands[])
+{
+    dry_run = false;
+    _ovs_cmdl_run_command(ctx, commands);
+}
+
+void
+ovs_cmdl_run_command_dry_run(struct ovs_cmdl_context *ctx,
+                             const struct ovs_cmdl_command commands[])
+{
+    dry_run = true;
+    _ovs_cmdl_run_command(ctx, commands);
+}
 
 /* Process title. */
 
diff --git a/lib/command-line.h b/lib/command-line.h
index e9e3b7b..d0c4c3e 100644
--- a/lib/command-line.h
+++ b/lib/command-line.h
@@ -41,12 +41,16 @@  struct ovs_cmdl_command {
     int min_args;
     int max_args;
     ovs_cmdl_handler handler;
+    enum { OVS_RO, OVS_RW } mode;    /* Does this command modify things? */
 };
 
 char *ovs_cmdl_long_options_to_short_options(const struct option *options);
 void ovs_cmdl_print_options(const struct option *options);
 void ovs_cmdl_print_commands(const struct ovs_cmdl_command *commands);
-void ovs_cmdl_run_command(struct ovs_cmdl_context *, const struct ovs_cmdl_command[]);
+void ovs_cmdl_run_command(struct ovs_cmdl_context *,
+                          const struct ovs_cmdl_command[]);
+void ovs_cmdl_run_command_dry_run(struct ovs_cmdl_context *,
+                                  const struct ovs_cmdl_command[]);
 
 void ovs_cmdl_proctitle_init(int argc, char **argv);
 #if defined(__FreeBSD__) || defined(__NetBSD__)
diff --git a/lib/db-ctl-base.h b/lib/db-ctl-base.h
index 0f4658e..e5a354d 100644
--- a/lib/db-ctl-base.h
+++ b/lib/db-ctl-base.h
@@ -120,7 +120,7 @@  struct ctl_command_syntax {
      * empty string if the command does not support any options. */
     const char *options;
 
-    enum { RO, RW } mode;       /* Does this command modify the database? */
+    enum { RO, RW } mode;   /* Does this command modify the database? */
 };
 
 /* A command extracted from command-line input plus the structs for
diff --git a/lib/dpctl.c b/lib/dpctl.c
index b470ab0..1b7356e 100644
--- a/lib/dpctl.c
+++ b/lib/dpctl.c
@@ -59,6 +59,7 @@  struct dpctl_command {
     int min_args;
     int max_args;
     dpctl_command_handler *handler;
+    enum { DP_RO, DP_RW} mode;
 };
 static const struct dpctl_command *get_all_dpctl_commands(void);
 static void dpctl_print(struct dpctl_params *dpctl_p, const char *fmt, ...)
@@ -1615,29 +1616,29 @@  out:
 }
 
 static const struct dpctl_command all_commands[] = {
-    { "add-dp", "add-dp dp [iface...]", 1, INT_MAX, dpctl_add_dp },
-    { "del-dp", "del-dp dp", 1, 1, dpctl_del_dp },
-    { "add-if", "add-if dp iface...", 2, INT_MAX, dpctl_add_if },
-    { "del-if", "del-if dp iface...", 2, INT_MAX, dpctl_del_if },
-    { "set-if", "set-if dp iface...", 2, INT_MAX, dpctl_set_if },
-    { "dump-dps", "", 0, 0, dpctl_dump_dps },
-    { "show", "[dp...]", 0, INT_MAX, dpctl_show },
-    { "dump-flows", "[dp]", 0, 2, dpctl_dump_flows },
-    { "add-flow", "add-flow [dp] flow actions", 2, 3, dpctl_add_flow },
-    { "mod-flow", "mod-flow [dp] flow actions", 2, 3, dpctl_mod_flow },
-    { "get-flow", "get-flow [dp] ufid", 1, 2, dpctl_get_flow },
-    { "del-flow", "del-flow [dp] flow", 1, 2, dpctl_del_flow },
-    { "del-flows", "[dp]", 0, 1, dpctl_del_flows },
-    { "dump-conntrack", "[dp] [zone=N]", 0, 2, dpctl_dump_conntrack },
-    { "flush-conntrack", "[dp] [zone=N]", 0, 2, dpctl_flush_conntrack },
-    { "help", "", 0, INT_MAX, dpctl_help },
-    { "list-commands", "", 0, INT_MAX, dpctl_list_commands },
+    { "add-dp", "add-dp dp [iface...]", 1, INT_MAX, dpctl_add_dp, DP_RW },
+    { "del-dp", "del-dp dp", 1, 1, dpctl_del_dp, DP_RW },
+    { "add-if", "add-if dp iface...", 2, INT_MAX, dpctl_add_if, DP_RW },
+    { "del-if", "del-if dp iface...", 2, INT_MAX, dpctl_del_if, DP_RW },
+    { "set-if", "set-if dp iface...", 2, INT_MAX, dpctl_set_if, DP_RW },
+    { "dump-dps", "", 0, 0, dpctl_dump_dps, DP_RO },
+    { "show", "[dp...]", 0, INT_MAX, dpctl_show, DP_RO },
+    { "dump-flows", "[dp]", 0, 2, dpctl_dump_flows, DP_RO },
+    { "add-flow", "add-flow [dp] flow actions", 2, 3, dpctl_add_flow, DP_RW },
+    { "mod-flow", "mod-flow [dp] flow actions", 2, 3, dpctl_mod_flow, DP_RW },
+    { "get-flow", "get-flow [dp] ufid", 1, 2, dpctl_get_flow, DP_RO },
+    { "del-flow", "del-flow [dp] flow", 1, 2, dpctl_del_flow, DP_RW },
+    { "del-flows", "[dp]", 0, 1, dpctl_del_flows, DP_RW },
+    { "dump-conntrack", "[dp] [zone=N]", 0, 2, dpctl_dump_conntrack, DP_RO },
+    { "flush-conntrack", "[dp] [zone=N]", 0, 2, dpctl_flush_conntrack, DP_RW },
+    { "help", "", 0, INT_MAX, dpctl_help, DP_RO },
+    { "list-commands", "", 0, INT_MAX, dpctl_list_commands, DP_RO },
 
     /* Undocumented commands for testing. */
-    { "parse-actions", "actions", 1, INT_MAX, dpctl_parse_actions },
-    { "normalize-actions", "actions", 2, INT_MAX, dpctl_normalize_actions },
+    { "parse-actions", "actions", 1, INT_MAX, dpctl_parse_actions, DP_RO }, 
+    { "normalize-actions", "actions", 2, INT_MAX, dpctl_normalize_actions, DP_RO },
 
-    { NULL, NULL, 0, 0, NULL },
+    { NULL, NULL, 0, 0, NULL, DP_RO },
 };
 
 static const struct dpctl_command *get_all_dpctl_commands(void)
@@ -1672,6 +1673,9 @@  dpctl_run_command(int argc, const char *argv[], struct dpctl_params *dpctl_p)
                             p->name, p->max_args);
                 return EINVAL;
             } else {
+                if (p->mode == DP_RW && dpctl_p->dry_run) {
+                    return 0;
+                }
                 return p->handler(argc, argv, dpctl_p);
             }
         }
diff --git a/lib/dpctl.h b/lib/dpctl.h
index 11a172d..a4bdacf 100644
--- a/lib/dpctl.h
+++ b/lib/dpctl.h
@@ -33,6 +33,9 @@  struct dpctl_params {
     /* --may-create: Allow mod-flows command to create a new flow? */
     bool may_create;
 
+    /* --dry-run: Commit changes? */
+    bool dry_run;
+
     /* -m, --more: Increase output verbosity. */
     int verbosity;
 
diff --git a/ovsdb/ovsdb-tool.c b/ovsdb/ovsdb-tool.c
index af83da2..6531f6a 100644
--- a/ovsdb/ovsdb-tool.c
+++ b/ovsdb/ovsdb-tool.c
@@ -573,20 +573,20 @@  do_list_commands(struct ovs_cmdl_context *ctx OVS_UNUSED)
 }
 
 static const struct ovs_cmdl_command all_commands[] = {
-    { "create", "[db [schema]]", 0, 2, do_create },
-    { "compact", "[db [dst]]", 0, 2, do_compact },
-    { "convert", "[db [schema [dst]]]", 0, 3, do_convert },
-    { "needs-conversion", NULL, 0, 2, do_needs_conversion },
-    { "db-version", "[db]",  0, 1, do_db_version },
-    { "db-cksum", "[db]", 0, 1, do_db_cksum },
-    { "schema-version", "[schema]", 0, 1, do_schema_version },
-    { "schema-cksum", "[schema]", 0, 1, do_schema_cksum },
-    { "query", "[db] trns", 1, 2, do_query },
-    { "transact", "[db] trns", 1, 2, do_transact },
-    { "show-log", "[db]", 0, 1, do_show_log },
-    { "help", NULL, 0, INT_MAX, do_help },
-    { "list-commands", NULL, 0, INT_MAX, do_list_commands },
-    { NULL, NULL, 0, 0, NULL },
+    { "create", "[db [schema]]", 0, 2, do_create, OVS_RW },
+    { "compact", "[db [dst]]", 0, 2, do_compact, OVS_RW },
+    { "convert", "[db [schema [dst]]]", 0, 3, do_convert, OVS_RW },
+    { "needs-conversion", NULL, 0, 2, do_needs_conversion, OVS_RO },
+    { "db-version", "[db]",  0, 1, do_db_version, OVS_RO },
+    { "db-cksum", "[db]", 0, 1, do_db_cksum, OVS_RO },
+    { "schema-version", "[schema]", 0, 1, do_schema_version, OVS_RO },
+    { "schema-cksum", "[schema]", 0, 1, do_schema_cksum, OVS_RO },
+    { "query", "[db] trns", 1, 2, do_query, OVS_RO },
+    { "transact", "[db] trns", 1, 2, do_transact, OVS_RO },
+    { "show-log", "[db]", 0, 1, do_show_log, OVS_RO },
+    { "help", NULL, 0, INT_MAX, do_help, OVS_RO },
+    { "list-commands", NULL, 0, INT_MAX, do_list_commands, OVS_RO },
+    { NULL, NULL, 0, 0, NULL, OVS_RO },
 };
 
 static const struct ovs_cmdl_command *get_all_commands(void)
diff --git a/tests/ovstest.c b/tests/ovstest.c
index 2fb7f66..745bd2e 100644
--- a/tests/ovstest.c
+++ b/tests/ovstest.c
@@ -33,7 +33,7 @@  static size_t allocated_commands = 0;
 static void
 add_command(struct ovs_cmdl_command *cmd)
 {
-    const struct ovs_cmdl_command nil = {NULL, NULL, 0, 0, NULL};
+    const struct ovs_cmdl_command nil = {NULL, NULL, 0, 0, NULL, OVS_RO};
 
     while (n_commands + 1 >= allocated_commands) {
         commands = x2nrealloc(commands, &allocated_commands,
@@ -86,7 +86,7 @@  help(struct ovs_cmdl_context *ctx OVS_UNUSED)
 static void
 add_top_level_commands(void)
 {
-    struct ovs_cmdl_command help_cmd = {"--help", NULL, 0, 0, help};
+    struct ovs_cmdl_command help_cmd = {"--help", NULL, 0, 0, help, OVS_RO };
 
     add_command(&help_cmd);
 }
diff --git a/tests/test-bitmap.c b/tests/test-bitmap.c
index 3dbc8df..484407b 100644
--- a/tests/test-bitmap.c
+++ b/tests/test-bitmap.c
@@ -149,9 +149,9 @@  run_benchmarks(struct ovs_cmdl_context *ctx)
 }
 
 static const struct ovs_cmdl_command commands[] = {
-    {"check", NULL, 0, 0, run_tests},
-    {"benchmark", NULL, 1, 1, run_benchmarks},
-    {NULL, NULL, 0, 0, NULL},
+    {"check", NULL, 0, 0, run_tests, OVS_RO},
+    {"benchmark", NULL, 1, 1, run_benchmarks, OVS_RO},
+    {NULL, NULL, 0, 0, NULL, OVS_RO},
 };
 
 static void
diff --git a/tests/test-ccmap.c b/tests/test-ccmap.c
index 4efe1b9..5ebc659 100644
--- a/tests/test-ccmap.c
+++ b/tests/test-ccmap.c
@@ -272,9 +272,9 @@  benchmark_ccmap(void)
 
 
 static const struct ovs_cmdl_command commands[] = {
-    {"check", NULL, 0, 1, run_tests},
-    {"benchmark", NULL, 3, 3, run_benchmarks},
-    {NULL, NULL, 0, 0, NULL},
+    {"check", NULL, 0, 1, run_tests, OVS_RO},
+    {"benchmark", NULL, 3, 3, run_benchmarks, OVS_RO},
+    {NULL, NULL, 0, 0, NULL, OVS_RO},
 };
 
 static void
diff --git a/tests/test-classifier.c b/tests/test-classifier.c
index cdd83f0..ebbc283 100644
--- a/tests/test-classifier.c
+++ b/tests/test-classifier.c
@@ -1838,23 +1838,23 @@  static void help(struct ovs_cmdl_context *ctx);
 
 static const struct ovs_cmdl_command commands[] = {
     /* Classifier tests. */
-    {"empty", NULL, 0, 0, test_empty},
-    {"destroy-null", NULL, 0, 0, test_destroy_null},
-    {"single-rule", NULL, 0, 0, test_single_rule},
-    {"rule-replacement", NULL, 0, 0, test_rule_replacement},
-    {"many-rules-in-one-list", NULL, 0, 1, test_many_rules_in_one_list},
-    {"many-rules-in-one-table", NULL, 0, 1, test_many_rules_in_one_table},
-    {"many-rules-in-two-tables", NULL, 0, 0, test_many_rules_in_two_tables},
-    {"many-rules-in-five-tables", NULL, 0, 0, test_many_rules_in_five_tables},
-    {"benchmark", NULL, 0, 5, run_benchmarks},
+    {"empty", NULL, 0, 0, test_empty, OVS_RO },
+    {"destroy-null", NULL, 0, 0, test_destroy_null, OVS_RO },
+    {"single-rule", NULL, 0, 0, test_single_rule, OVS_RO },
+    {"rule-replacement", NULL, 0, 0, test_rule_replacement, OVS_RO },
+    {"many-rules-in-one-list", NULL, 0, 1, test_many_rules_in_one_list, OVS_RO },
+    {"many-rules-in-one-table", NULL, 0, 1, test_many_rules_in_one_table, OVS_RO },
+    {"many-rules-in-two-tables", NULL, 0, 0, test_many_rules_in_two_tables, OVS_RO },
+    {"many-rules-in-five-tables", NULL, 0, 0, test_many_rules_in_five_tables, OVS_RO },
+    {"benchmark", NULL, 0, 5, run_benchmarks, OVS_RO },
 
     /* Miniflow and minimask tests. */
-    {"miniflow", NULL, 0, 0, test_miniflow},
-    {"minimask_has_extra", NULL, 0, 0, test_minimask_has_extra},
-    {"minimask_combine", NULL, 0, 0, test_minimask_combine},
+    {"miniflow", NULL, 0, 0, test_miniflow, OVS_RO },
+    {"minimask_has_extra", NULL, 0, 0, test_minimask_has_extra, OVS_RO },
+    {"minimask_combine", NULL, 0, 0, test_minimask_combine, OVS_RO },
 
-    {"--help", NULL, 0, 0, help},
-    {NULL, NULL, 0, 0, NULL},
+    {"--help", NULL, 0, 0, help, OVS_RO },
+    {NULL, NULL, 0, 0, NULL, OVS_RO },
 };
 
 static void
diff --git a/tests/test-cmap.c b/tests/test-cmap.c
index 4cac7de..e159a16 100644
--- a/tests/test-cmap.c
+++ b/tests/test-cmap.c
@@ -636,9 +636,9 @@  benchmark_hmap(void)
 }
 
 static const struct ovs_cmdl_command commands[] = {
-    {"check", NULL, 0, 1, run_tests},
-    {"benchmark", NULL, 3, 4, run_benchmarks},
-    {NULL, NULL, 0, 0, NULL},
+    {"check", NULL, 0, 1, run_tests, OVS_RO},
+    {"benchmark", NULL, 3, 4, run_benchmarks, OVS_RO},
+    {NULL, NULL, 0, 0, NULL, OVS_RO},
 };
 
 static void
diff --git a/tests/test-conntrack.c b/tests/test-conntrack.c
index 6c1b373..803e2b9 100644
--- a/tests/test-conntrack.c
+++ b/tests/test-conntrack.c
@@ -260,13 +260,13 @@  static const struct ovs_cmdl_command commands[] = {
      * is '1', each packet in a batch will have a different source and
      * destination port */
     {"benchmark", "n_threads n_pkts batch_size [change_connection]", 3, 4,
-     test_benchmark},
+     test_benchmark, OVS_RO},
     /* Reads packets from 'file' and sends them to the connection tracker,
      * 'batch_size' (1 by default) per call, with the commit flag set.
      * Prints the ct_state of each packet. */
-    {"pcap", "file [batch_size]", 1, 2, test_pcap},
+    {"pcap", "file [batch_size]", 1, 2, test_pcap, OVS_RO},
 
-    {NULL, NULL, 0, 0, NULL},
+    {NULL, NULL, 0, 0, NULL, OVS_RO},
 };
 
 static void
diff --git a/tests/test-heap.c b/tests/test-heap.c
index 6dab22b..88c9f25 100644
--- a/tests/test-heap.c
+++ b/tests/test-heap.c
@@ -461,16 +461,16 @@  test_heap_raw_delete(struct ovs_cmdl_context *ctx OVS_UNUSED)
 
 static const struct ovs_cmdl_command commands[] = {
     { "insert-delete-same-order", NULL, 0, 0,
-      test_heap_insert_delete_same_order, },
+      test_heap_insert_delete_same_order, OVS_RO },
     { "insert-delete-reverse-order", NULL, 0, 0,
-      test_heap_insert_delete_reverse_order, },
+      test_heap_insert_delete_reverse_order, OVS_RO },
     { "insert-delete-every-order", NULL, 0, 0,
-      test_heap_insert_delete_every_order, },
+      test_heap_insert_delete_every_order, OVS_RO },
     { "insert-delete-same-order-with-dups", NULL, 0, 0,
-      test_heap_insert_delete_same_order_with_dups, },
-    { "raw-insert", NULL, 0, 0, test_heap_raw_insert, },
-    { "raw-delete", NULL, 0, 0, test_heap_raw_delete, },
-    { NULL, NULL, 0, 0, NULL, },
+      test_heap_insert_delete_same_order_with_dups, OVS_RO },
+    { "raw-insert", NULL, 0, 0, test_heap_raw_insert, OVS_RO },
+    { "raw-delete", NULL, 0, 0, test_heap_raw_delete, OVS_RO },
+    { NULL, NULL, 0, 0, NULL, OVS_RO },
 };
 
 static void
diff --git a/tests/test-jsonrpc.c b/tests/test-jsonrpc.c
index be79064..684601a 100644
--- a/tests/test-jsonrpc.c
+++ b/tests/test-jsonrpc.c
@@ -331,11 +331,11 @@  do_help(struct ovs_cmdl_context *ctx OVS_UNUSED)
 }
 
 static struct ovs_cmdl_command all_commands[] = {
-    { "listen", NULL, 1, 1, do_listen },
-    { "request", NULL, 3, 3, do_request },
-    { "notify", NULL, 3, 3, do_notify },
-    { "help", NULL, 0, INT_MAX, do_help },
-    { NULL, NULL, 0, 0, NULL },
+    { "listen", NULL, 1, 1, do_listen, OVS_RO },
+    { "request", NULL, 3, 3, do_request, OVS_RO },
+    { "notify", NULL, 3, 3, do_notify, OVS_RO },
+    { "help", NULL, 0, INT_MAX, do_help, OVS_RO },
+    { NULL, NULL, 0, 0, NULL, OVS_RO },
 };
 
 static struct ovs_cmdl_command *
diff --git a/tests/test-netlink-conntrack.c b/tests/test-netlink-conntrack.c
index f0d48f7..000062d 100644
--- a/tests/test-netlink-conntrack.c
+++ b/tests/test-netlink-conntrack.c
@@ -161,14 +161,14 @@  static const struct ovs_cmdl_command commands[] = {
     /* Linux netlink connection tracker interface test. */
 
     /* Prints all the entries in the connection table and exits. */
-    {"dump", "[zone=zone]", 0, 1, test_nl_ct_dump},
+    {"dump", "[zone=zone]", 0, 1, test_nl_ct_dump, OVS_RO},
     /* Listens to all the connection tracking events and prints them to
      * standard output until killed. */
-    {"monitor", "", 0, 0, test_nl_ct_monitor},
+    {"monitor", "", 0, 0, test_nl_ct_monitor, OVS_RO},
     /* Flushes all the entries from all the tables.. */
-    {"flush", "[zone=zone]", 0, 1, test_nl_ct_flush},
+    {"flush", "[zone=zone]", 0, 1, test_nl_ct_flush, OVS_RO},
 
-    {NULL, NULL, 0, 0, NULL},
+    {NULL, NULL, 0, 0, NULL, OVS_RO},
 };
 
 static void
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index acb6a99..32e5f57 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -1532,23 +1532,23 @@  test_ovn_main(int argc, char *argv[])
 
     static const struct ovs_cmdl_command commands[] = {
         /* Lexer. */
-        {"lex", NULL, 0, 0, test_lex},
+        {"lex", NULL, 0, 0, test_lex, OVS_RO},
 
         /* Expressions. */
-        {"parse-expr", NULL, 0, 0, test_parse_expr},
-        {"annotate-expr", NULL, 0, 0, test_annotate_expr},
-        {"simplify-expr", NULL, 0, 0, test_simplify_expr},
-        {"normalize-expr", NULL, 0, 0, test_normalize_expr},
-        {"expr-to-flows", NULL, 0, 0, test_expr_to_flows},
-        {"evaluate-expr", NULL, 1, 1, test_evaluate_expr},
-        {"composition", NULL, 1, 1, test_composition},
-        {"tree-shape", NULL, 1, 1, test_tree_shape},
-        {"exhaustive", NULL, 1, 1, test_exhaustive},
+        {"parse-expr", NULL, 0, 0, test_parse_expr, OVS_RO},
+        {"annotate-expr", NULL, 0, 0, test_annotate_expr, OVS_RO},
+        {"simplify-expr", NULL, 0, 0, test_simplify_expr, OVS_RO},
+        {"normalize-expr", NULL, 0, 0, test_normalize_expr, OVS_RO},
+        {"expr-to-flows", NULL, 0, 0, test_expr_to_flows, OVS_RO},
+        {"evaluate-expr", NULL, 1, 1, test_evaluate_expr, OVS_RO},
+        {"composition", NULL, 1, 1, test_composition, OVS_RO},
+        {"tree-shape", NULL, 1, 1, test_tree_shape, OVS_RO},
+        {"exhaustive", NULL, 1, 1, test_exhaustive, OVS_RO},
 
         /* Actions. */
-        {"parse-actions", NULL, 0, 0, test_parse_actions},
+        {"parse-actions", NULL, 0, 0, test_parse_actions, OVS_RO},
 
-        {NULL, NULL, 0, 0, NULL},
+        {NULL, NULL, 0, 0, NULL, OVS_RO},
     };
     struct ovs_cmdl_context ctx;
     ctx.argc = argc - optind;
diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
index 4a68bca..5d89412 100644
--- a/tests/test-ovsdb.c
+++ b/tests/test-ovsdb.c
@@ -1667,13 +1667,13 @@  static void
 do_transact(struct ovs_cmdl_context *ctx)
 {
     static const struct ovs_cmdl_command do_transact_commands[] = {
-        { "commit", NULL, 0, 0, do_transact_commit },
-        { "abort", NULL, 0, 0, do_transact_abort },
-        { "insert", NULL, 2, 3, do_transact_insert },
-        { "delete", NULL, 1, 1, do_transact_delete },
-        { "modify", NULL, 2, 3, do_transact_modify },
-        { "print", NULL, 0, 0, do_transact_print },
-        { NULL, NULL, 0, 0, NULL },
+        { "commit", NULL, 0, 0, do_transact_commit, OVS_RO },
+        { "abort", NULL, 0, 0, do_transact_abort, OVS_RO },
+        { "insert", NULL, 2, 3, do_transact_insert, OVS_RO },
+        { "delete", NULL, 1, 1, do_transact_delete, OVS_RO },
+        { "modify", NULL, 2, 3, do_transact_modify, OVS_RO },
+        { "print", NULL, 0, 0, do_transact_print, OVS_RO },
+        { NULL, NULL, 0, 0, NULL, OVS_RO },
     };
 
     struct ovsdb_schema *schema;
@@ -2578,39 +2578,39 @@  do_idl_partial_update_map_column(struct ovs_cmdl_context *ctx)
 }
 
 static struct ovs_cmdl_command all_commands[] = {
-    { "log-io", NULL, 2, INT_MAX, do_log_io },
-    { "default-atoms", NULL, 0, 0, do_default_atoms },
-    { "default-data", NULL, 0, 0, do_default_data },
-    { "diff-data", NULL, 3, INT_MAX, do_diff_data},
-    { "parse-atomic-type", NULL, 1, 1, do_parse_atomic_type },
-    { "parse-base-type", NULL, 1, 1, do_parse_base_type },
-    { "parse-type", NULL, 1, 1, do_parse_type },
-    { "parse-atoms", NULL, 2, INT_MAX, do_parse_atoms },
-    { "parse-atom-strings", NULL, 2, INT_MAX, do_parse_atom_strings },
-    { "parse-data", NULL, 2, INT_MAX, do_parse_data },
-    { "parse-data-strings", NULL, 2, INT_MAX, do_parse_data_strings },
-    { "sort-atoms", NULL, 2, 2, do_sort_atoms },
-    { "parse-column", NULL, 2, 2, do_parse_column },
-    { "parse-table", NULL, 2, 3, do_parse_table },
-    { "parse-rows", NULL, 2, INT_MAX, do_parse_rows },
-    { "compare-rows", NULL, 2, INT_MAX, do_compare_rows },
-    { "parse-conditions", NULL, 2, INT_MAX, do_parse_conditions },
-    { "evaluate-conditions", NULL, 3, 3, do_evaluate_conditions },
-    { "evaluate-conditions-any", NULL, 3, 3, do_evaluate_conditions_any },
-    { "compare-conditions", NULL, 2, 2, do_compare_conditions },
-    { "parse-mutations", NULL, 2, INT_MAX, do_parse_mutations },
-    { "execute-mutations", NULL, 3, 3, do_execute_mutations },
-    { "query", NULL, 3, 3, do_query },
-    { "query-distinct", NULL, 4, 4, do_query_distinct },
-    { "transact", NULL, 1, INT_MAX, do_transact },
-    { "parse-schema", NULL, 1, 1, do_parse_schema },
-    { "execute", NULL, 2, INT_MAX, do_execute },
-    { "trigger", NULL, 2, INT_MAX, do_trigger },
-    { "idl", NULL, 1, INT_MAX, do_idl },
+    { "log-io", NULL, 2, INT_MAX, do_log_io, OVS_RO },
+    { "default-atoms", NULL, 0, 0, do_default_atoms, OVS_RO },
+    { "default-data", NULL, 0, 0, do_default_data, OVS_RO },
+    { "diff-data", NULL, 3, INT_MAX, do_diff_data, OVS_RO },
+    { "parse-atomic-type", NULL, 1, 1, do_parse_atomic_type, OVS_RO },
+    { "parse-base-type", NULL, 1, 1, do_parse_base_type, OVS_RO },
+    { "parse-type", NULL, 1, 1, do_parse_type, OVS_RO },
+    { "parse-atoms", NULL, 2, INT_MAX, do_parse_atoms, OVS_RO },
+    { "parse-atom-strings", NULL, 2, INT_MAX, do_parse_atom_strings, OVS_RO },
+    { "parse-data", NULL, 2, INT_MAX, do_parse_data, OVS_RO },
+    { "parse-data-strings", NULL, 2, INT_MAX, do_parse_data_strings, OVS_RO },
+    { "sort-atoms", NULL, 2, 2, do_sort_atoms, OVS_RO },
+    { "parse-column", NULL, 2, 2, do_parse_column, OVS_RO },
+    { "parse-table", NULL, 2, 3, do_parse_table, OVS_RO },
+    { "parse-rows", NULL, 2, INT_MAX, do_parse_rows, OVS_RO },
+    { "compare-rows", NULL, 2, INT_MAX, do_compare_rows, OVS_RO },
+    { "parse-conditions", NULL, 2, INT_MAX, do_parse_conditions, OVS_RO },
+    { "evaluate-conditions", NULL, 3, 3, do_evaluate_conditions, OVS_RO },
+    { "evaluate-conditions-any", NULL, 3, 3, do_evaluate_conditions_any, OVS_RO },
+    { "compare-conditions", NULL, 2, 2, do_compare_conditions, OVS_RO },
+    { "parse-mutations", NULL, 2, INT_MAX, do_parse_mutations, OVS_RO },
+    { "execute-mutations", NULL, 3, 3, do_execute_mutations, OVS_RO },
+    { "query", NULL, 3, 3, do_query, OVS_RO },
+    { "query-distinct", NULL, 4, 4, do_query_distinct, OVS_RO },
+    { "transact", NULL, 1, INT_MAX, do_transact, OVS_RO },
+    { "parse-schema", NULL, 1, 1, do_parse_schema, OVS_RO },
+    { "execute", NULL, 2, INT_MAX, do_execute, OVS_RO },
+    { "trigger", NULL, 2, INT_MAX, do_trigger, OVS_RO },
+    { "idl", NULL, 1, INT_MAX, do_idl, OVS_RO },
     { "idl-partial-update-map-column", NULL, 1, INT_MAX,
-                                       do_idl_partial_update_map_column },
-    { "help", NULL, 0, INT_MAX, do_help },
-    { NULL, NULL, 0, 0, NULL },
+                                       do_idl_partial_update_map_column, OVS_RO },
+    { "help", NULL, 0, INT_MAX, do_help, OVS_RO },
+    { NULL, NULL, 0, 0, NULL, OVS_RO },
 };
 
 static struct ovs_cmdl_command *
diff --git a/tests/test-reconnect.c b/tests/test-reconnect.c
index 76e43ea..72252b8 100644
--- a/tests/test-reconnect.c
+++ b/tests/test-reconnect.c
@@ -272,22 +272,22 @@  do_listen_error(struct ovs_cmdl_context *ctx)
 }
 
 static const struct ovs_cmdl_command all_commands[] = {
-    { "enable", NULL, 0, 0, do_enable },
-    { "disable", NULL, 0, 0, do_disable },
-    { "force-reconnect", NULL, 0, 0, do_force_reconnect },
-    { "disconnected", NULL, 0, 1, do_disconnected },
-    { "connecting", NULL, 0, 0, do_connecting },
-    { "connect-failed", NULL, 0, 1, do_connect_failed },
-    { "connected", NULL, 0, 0, do_connected },
-    { "activity", NULL, 0, 0, do_activity },
-    { "run", NULL, 0, 1, do_run },
-    { "advance", NULL, 1, 1, do_advance },
-    { "timeout", NULL, 0, 0, do_timeout },
-    { "set-max-tries", NULL, 1, 1, do_set_max_tries },
-    { "passive", NULL, 0, 0, do_set_passive },
-    { "listening", NULL, 0, 0, do_listening },
-    { "listen-error", NULL, 1, 1, do_listen_error },
-    { NULL, NULL, 0, 0, NULL },
+    { "enable", NULL, 0, 0, do_enable, OVS_RO },
+    { "disable", NULL, 0, 0, do_disable, OVS_RO },
+    { "force-reconnect", NULL, 0, 0, do_force_reconnect, OVS_RO },
+    { "disconnected", NULL, 0, 1, do_disconnected, OVS_RO },
+    { "connecting", NULL, 0, 0, do_connecting, OVS_RO },
+    { "connect-failed", NULL, 0, 1, do_connect_failed, OVS_RO },
+    { "connected", NULL, 0, 0, do_connected, OVS_RO },
+    { "activity", NULL, 0, 0, do_activity, OVS_RO },
+    { "run", NULL, 0, 1, do_run, OVS_RO },
+    { "advance", NULL, 1, 1, do_advance, OVS_RO },
+    { "timeout", NULL, 0, 0, do_timeout, OVS_RO },
+    { "set-max-tries", NULL, 1, 1, do_set_max_tries, OVS_RO },
+    { "passive", NULL, 0, 0, do_set_passive, OVS_RO },
+    { "listening", NULL, 0, 0, do_listening, OVS_RO },
+    { "listen-error", NULL, 1, 1, do_listen_error, OVS_RO },
+    { NULL, NULL, 0, 0, NULL, OVS_RO },
 };
 
 static const struct ovs_cmdl_command *
diff --git a/tests/test-util.c b/tests/test-util.c
index ef45903..e37c722 100644
--- a/tests/test-util.c
+++ b/tests/test-util.c
@@ -1149,25 +1149,25 @@  test_file_name(struct ovs_cmdl_context *ctx)
 #endif /* _WIN32 */
 
 static const struct ovs_cmdl_command commands[] = {
-    {"ctz", NULL, 0, 0, test_ctz},
-    {"clz", NULL, 0, 0, test_clz},
-    {"round_up_pow2", NULL, 0, 0, test_round_up_pow2},
-    {"round_down_pow2", NULL, 0, 0, test_round_down_pow2},
-    {"count_1bits", NULL, 0, 0, test_count_1bits},
-    {"log_2_floor", NULL, 0, 0, test_log_2_floor},
-    {"bitwise_copy", NULL, 0, 0, test_bitwise_copy},
-    {"bitwise_zero", NULL, 0, 0, test_bitwise_zero},
-    {"bitwise_one", NULL, 0, 0, test_bitwise_one},
-    {"bitwise_is_all_zeros", NULL, 0, 0, test_bitwise_is_all_zeros},
-    {"bitwise_rscan", NULL, 0, 0, test_bitwise_rscan},
-    {"follow-symlinks", NULL, 1, INT_MAX, test_follow_symlinks},
-    {"assert", NULL, 0, 0, test_assert},
-    {"ovs_scan", NULL, 0, 0, test_ovs_scan},
-    {"snprintf", NULL, 0, 0, test_snprintf},
+    {"ctz", NULL, 0, 0, test_ctz, OVS_RO},
+    {"clz", NULL, 0, 0, test_clz, OVS_RO},
+    {"round_up_pow2", NULL, 0, 0, test_round_up_pow2, OVS_RO},
+    {"round_down_pow2", NULL, 0, 0, test_round_down_pow2, OVS_RO},
+    {"count_1bits", NULL, 0, 0, test_count_1bits, OVS_RO},
+    {"log_2_floor", NULL, 0, 0, test_log_2_floor, OVS_RO},
+    {"bitwise_copy", NULL, 0, 0, test_bitwise_copy, OVS_RO},
+    {"bitwise_zero", NULL, 0, 0, test_bitwise_zero, OVS_RO},
+    {"bitwise_one", NULL, 0, 0, test_bitwise_one, OVS_RO},
+    {"bitwise_is_all_zeros", NULL, 0, 0, test_bitwise_is_all_zeros, OVS_RO},
+    {"bitwise_rscan", NULL, 0, 0, test_bitwise_rscan, OVS_RO},
+    {"follow-symlinks", NULL, 1, INT_MAX, test_follow_symlinks, OVS_RO},
+    {"assert", NULL, 0, 0, test_assert, OVS_RO},
+    {"ovs_scan", NULL, 0, 0, test_ovs_scan, OVS_RO},
+    {"snprintf", NULL, 0, 0, test_snprintf, OVS_RO},
 #ifndef _WIN32
-    {"file_name", NULL, 1, INT_MAX, test_file_name},
+    {"file_name", NULL, 1, INT_MAX, test_file_name, OVS_RO},
 #endif
-    {NULL, NULL, 0, 0, NULL},
+    {NULL, NULL, 0, 0, NULL, OVS_RO},
 };
 
 static void
diff --git a/tests/test-vconn.c b/tests/test-vconn.c
index faa824a..1bf9aa0 100644
--- a/tests/test-vconn.c
+++ b/tests/test-vconn.c
@@ -432,15 +432,15 @@  test_send_invalid_version_hello(struct ovs_cmdl_context *ctx)
 }
 
 static const struct ovs_cmdl_command commands[] = {
-    {"refuse-connection", NULL, 1, 1, test_refuse_connection},
-    {"accept-then-close", NULL, 1, 1, test_accept_then_close},
-    {"read-hello", NULL, 1, 1, test_read_hello},
-    {"send-plain-hello", NULL, 1, 1, test_send_plain_hello},
-    {"send-long-hello", NULL, 1, 1, test_send_long_hello},
-    {"send-echo-hello", NULL, 1, 1, test_send_echo_hello},
-    {"send-short-hello", NULL, 1, 1, test_send_short_hello},
-    {"send-invalid-version-hello", NULL, 1, 1, test_send_invalid_version_hello},
-    {NULL, NULL, 0, 0, NULL},
+    {"refuse-connection", NULL, 1, 1, test_refuse_connection, OVS_RO},
+    {"accept-then-close", NULL, 1, 1, test_accept_then_close, OVS_RO},
+    {"read-hello", NULL, 1, 1, test_read_hello, OVS_RO},
+    {"send-plain-hello", NULL, 1, 1, test_send_plain_hello, OVS_RO},
+    {"send-long-hello", NULL, 1, 1, test_send_long_hello, OVS_RO},
+    {"send-echo-hello", NULL, 1, 1, test_send_echo_hello, OVS_RO},
+    {"send-short-hello", NULL, 1, 1, test_send_short_hello, OVS_RO},
+    {"send-invalid-version-hello", NULL, 1, 1, test_send_invalid_version_hello, OVS_RO},
+    {NULL, NULL, 0, 0, NULL, OVS_RO},
 };
 
 static void
diff --git a/utilities/ovs-dpctl.c b/utilities/ovs-dpctl.c
index 4897ea3..c52f946 100644
--- a/utilities/ovs-dpctl.c
+++ b/utilities/ovs-dpctl.c
@@ -77,12 +77,14 @@  parse_options(int argc, char *argv[])
     enum {
         OPT_CLEAR = UCHAR_MAX + 1,
         OPT_MAY_CREATE,
+        OPT_DRY_RUN,
         VLOG_OPTION_ENUMS
     };
     static const struct option long_options[] = {
         {"statistics", no_argument, NULL, 's'},
         {"clear", no_argument, NULL, OPT_CLEAR},
         {"may-create", no_argument, NULL, OPT_MAY_CREATE},
+        {"dry-run", no_argument, NULL, OPT_DRY_RUN},
         {"more", no_argument, NULL, 'm'},
         {"timeout", required_argument, NULL, 't'},
         {"help", no_argument, NULL, 'h'},
@@ -115,6 +117,10 @@  parse_options(int argc, char *argv[])
             dpctl_p.may_create = true;
             break;
 
+        case OPT_DRY_RUN:
+            dpctl_p.dry_run = true;
+            break;
+
         case 'm':
             dpctl_p.verbosity++;
             break;
@@ -186,6 +192,7 @@  usage(void *userdata OVS_UNUSED)
            "  -m, --more                  increase verbosity of output\n"
            "\nOptions for mod-flow:\n"
            "  --may-create                create flow if it doesn't exist\n"
+           "  --dry-run                   do not commit changes\n"
            "  --clear                     reset existing stats to zero\n"
            "\nOther options:\n"
            "  -t, --timeout=SECS          give up after SECS seconds\n"
diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in
index 84057c0..a8d84df 100644
--- a/utilities/ovs-ofctl.8.in
+++ b/utilities/ovs-ofctl.8.in
@@ -2968,6 +2968,9 @@  depending on its configuration.
 \fB\-\-strict\fR
 Uses strict matching when running flow modification commands.
 .
+.IP "\fB\-\-dry-run\fR"
+Do not commit any changes.
+.
 .IP "\fB\-\-bundle\fR"
 Execute flow mods as an OpenFlow 1.4 atomic bundle transaction.
 .RS
diff --git a/utilities/ovs-ofctl.c b/utilities/ovs-ofctl.c
index 42d358f..361a1a2 100644
--- a/utilities/ovs-ofctl.c
+++ b/utilities/ovs-ofctl.c
@@ -79,6 +79,9 @@  static bool bundle = false;
 /* --color: Use color markers. */
 static bool enable_color;
 
+/* --dry-run: Do not commit any changes. */
+static bool dry_run;
+
 /* --strict: Use strict matching for flow mod commands?  Additionally governs
  * use of nx_pull_match() instead of nx_pull_match_loose() in parse-nx-match.
  */
@@ -142,7 +145,11 @@  main(int argc, char *argv[])
     ctx.argv = argv + optind;
 
     daemon_become_new_user(false);
-    ovs_cmdl_run_command(&ctx, get_all_commands());
+    if (dry_run) {
+        ovs_cmdl_run_command_dry_run(&ctx, get_all_commands());
+    } else {
+        ovs_cmdl_run_command(&ctx, get_all_commands());
+    }
     return 0;
 }
 
@@ -180,6 +187,7 @@  parse_options(int argc, char *argv[])
         OPT_BUNDLE,
         OPT_COLOR,
         OPT_MAY_CREATE,
+        OPT_DRY_RUN,
         DAEMON_OPTION_ENUMS,
         OFP_VERSION_OPTION_ENUMS,
         VLOG_OPTION_ENUMS
@@ -200,6 +208,7 @@  parse_options(int argc, char *argv[])
         {"bundle", no_argument, NULL, OPT_BUNDLE},
         {"color", optional_argument, NULL, OPT_COLOR},
         {"may-create", no_argument, NULL, OPT_MAY_CREATE},
+        {"dry-run", no_argument, NULL, OPT_DRY_RUN},
         DAEMON_LONG_OPTIONS,
         OFP_VERSION_LONG_OPTIONS,
         VLOG_LONG_OPTIONS,
@@ -281,6 +290,10 @@  parse_options(int argc, char *argv[])
             strict = true;
             break;
 
+        case OPT_DRY_RUN:
+            dry_run = true;
+            break;
+
         case OPT_READD:
             readd = true;
             break;
@@ -452,6 +465,7 @@  usage(void)
     vlog_usage();
     printf("\nOther options:\n"
            "  --strict                    use strict match for flow commands\n"
+           "  --dry-run                   do not commit changes\n"
            "  --readd                     replace flows that haven't changed\n"
            "  -F, --flow-format=FORMAT    force particular flow format\n"
            "  -P, --packet-in-format=FRMT force particular packet in format\n"
@@ -4120,136 +4134,136 @@  ofctl_parse_key_value(struct ovs_cmdl_context *ctx)
 
 static const struct ovs_cmdl_command all_commands[] = {
     { "show", "switch",
-      1, 1, ofctl_show },
+      1, 1, ofctl_show, OVS_RO },
     { "monitor", "switch [misslen] [invalid_ttl] [watch:[...]]",
-      1, 3, ofctl_monitor },
+      1, 3, ofctl_monitor, OVS_RO },
     { "snoop", "switch",
-      1, 1, ofctl_snoop },
+      1, 1, ofctl_snoop, OVS_RO },
     { "dump-desc", "switch",
-      1, 1, ofctl_dump_desc },
+      1, 1, ofctl_dump_desc, OVS_RO },
     { "dump-tables", "switch",
-      1, 1, ofctl_dump_tables },
+      1, 1, ofctl_dump_tables, OVS_RO },
     { "dump-table-features", "switch",
-      1, 1, ofctl_dump_table_features },
+      1, 1, ofctl_dump_table_features, OVS_RO },
     { "dump-table-desc", "switch",
-      1, 1, ofctl_dump_table_desc },
+      1, 1, ofctl_dump_table_desc, OVS_RO },
     { "dump-flows", "switch",
-      1, 2, ofctl_dump_flows },
+      1, 2, ofctl_dump_flows, OVS_RO },
     { "dump-aggregate", "switch",
-      1, 2, ofctl_dump_aggregate },
+      1, 2, ofctl_dump_aggregate, OVS_RO },
     { "queue-stats", "switch [port [queue]]",
-      1, 3, ofctl_queue_stats },
+      1, 3, ofctl_queue_stats, OVS_RO },
     { "queue-get-config", "switch [port [queue]]",
-      1, 3, ofctl_queue_get_config },
+      1, 3, ofctl_queue_get_config, OVS_RO },
     { "add-flow", "switch flow",
-      2, 2, ofctl_add_flow },
+      2, 2, ofctl_add_flow, OVS_RW },
     { "add-flows", "switch file",
-      2, 2, ofctl_add_flows },
+      2, 2, ofctl_add_flows, OVS_RW },
     { "mod-flows", "switch flow",
-      2, 2, ofctl_mod_flows },
+      2, 2, ofctl_mod_flows, OVS_RW },
     { "del-flows", "switch [flow]",
-      1, 2, ofctl_del_flows },
+      1, 2, ofctl_del_flows, OVS_RW },
     { "replace-flows", "switch file",
-      2, 2, ofctl_replace_flows },
+      2, 2, ofctl_replace_flows, OVS_RW },
     { "diff-flows", "source1 source2",
-      2, 2, ofctl_diff_flows },
+      2, 2, ofctl_diff_flows, OVS_RW },
     { "add-meter", "switch meter",
-      2, 2, ofctl_add_meter },
+      2, 2, ofctl_add_meter, OVS_RW },
     { "mod-meter", "switch meter",
-      2, 2, ofctl_mod_meter },
+      2, 2, ofctl_mod_meter, OVS_RW },
     { "del-meter", "switch meter",
-      2, 2, ofctl_del_meters },
+      2, 2, ofctl_del_meters, OVS_RW },
     { "del-meters", "switch",
-      1, 1, ofctl_del_meters },
+      1, 1, ofctl_del_meters, OVS_RW },
     { "dump-meter", "switch meter",
-      2, 2, ofctl_dump_meters },
+      2, 2, ofctl_dump_meters, OVS_RO },
     { "dump-meters", "switch",
-      1, 1, ofctl_dump_meters },
+      1, 1, ofctl_dump_meters, OVS_RO },
     { "meter-stats", "switch [meter]",
-      1, 2, ofctl_meter_stats },
+      1, 2, ofctl_meter_stats, OVS_RO },
     { "meter-features", "switch",
-      1, 1, ofctl_meter_features },
+      1, 1, ofctl_meter_features, OVS_RO },
     { "packet-out", "switch in_port actions packet...",
-      4, INT_MAX, ofctl_packet_out },
+      4, INT_MAX, ofctl_packet_out, OVS_RW },
     { "dump-ports", "switch [port]",
-      1, 2, ofctl_dump_ports },
+      1, 2, ofctl_dump_ports, OVS_RO },
     { "dump-ports-desc", "switch [port]",
-      1, 2, ofctl_dump_ports_desc },
+      1, 2, ofctl_dump_ports_desc, OVS_RO },
     { "mod-port", "switch iface act",
-      3, 3, ofctl_mod_port },
+      3, 3, ofctl_mod_port, OVS_RW },
     { "mod-table", "switch mod",
-      3, 3, ofctl_mod_table },
+      3, 3, ofctl_mod_table, OVS_RW },
     { "get-frags", "switch",
-      1, 1, ofctl_get_frags },
+      1, 1, ofctl_get_frags, OVS_RO },
     { "set-frags", "switch frag_mode",
-      2, 2, ofctl_set_frags },
+      2, 2, ofctl_set_frags, OVS_RW },
     { "probe", "target",
-      1, 1, ofctl_probe },
+      1, 1, ofctl_probe, OVS_RO },
     { "ping", "target [n]",
-      1, 2, ofctl_ping },
+      1, 2, ofctl_ping, OVS_RO },
     { "benchmark", "target n count",
-      3, 3, ofctl_benchmark },
+      3, 3, ofctl_benchmark, OVS_RO },
 
     { "dump-ipfix-bridge", "switch",
-      1, 1, ofctl_dump_ipfix_bridge},
+      1, 1, ofctl_dump_ipfix_bridge, OVS_RO },
     { "dump-ipfix-flow", "switch",
-      1, 1, ofctl_dump_ipfix_flow},
+      1, 1, ofctl_dump_ipfix_flow, OVS_RO },
 
     { "ofp-parse", "file",
-      1, 1, ofctl_ofp_parse },
+      1, 1, ofctl_ofp_parse, OVS_RW },
     { "ofp-parse-pcap", "pcap",
-      1, INT_MAX, ofctl_ofp_parse_pcap },
+      1, INT_MAX, ofctl_ofp_parse_pcap, OVS_RW },
 
     { "add-group", "switch group",
-      1, 2, ofctl_add_group },
+      1, 2, ofctl_add_group, OVS_RW },
     { "add-groups", "switch file",
-      1, 2, ofctl_add_groups },
+      1, 2, ofctl_add_groups, OVS_RW },
     { "mod-group", "switch group",
-      1, 2, ofctl_mod_group },
+      1, 2, ofctl_mod_group, OVS_RW },
     { "del-groups", "switch [group]",
-      1, 2, ofctl_del_groups },
+      1, 2, ofctl_del_groups, OVS_RW },
     { "insert-buckets", "switch [group]",
-      1, 2, ofctl_insert_bucket },
+      1, 2, ofctl_insert_bucket, OVS_RW },
     { "remove-buckets", "switch [group]",
-      1, 2, ofctl_remove_bucket },
+      1, 2, ofctl_remove_bucket, OVS_RW },
     { "dump-groups", "switch [group]",
-      1, 2, ofctl_dump_group_desc },
+      1, 2, ofctl_dump_group_desc, OVS_RO },
     { "dump-group-stats", "switch [group]",
-      1, 2, ofctl_dump_group_stats },
+      1, 2, ofctl_dump_group_stats, OVS_RO },
     { "dump-group-features", "switch",
-      1, 1, ofctl_dump_group_features },
+      1, 1, ofctl_dump_group_features, OVS_RO },
 
     { "bundle", "switch file",
-      2, 2, ofctl_bundle },
+      2, 2, ofctl_bundle, OVS_RW },
 
     { "add-tlv-map", "switch map",
-      2, 2, ofctl_add_tlv_map },
+      2, 2, ofctl_add_tlv_map, OVS_RO },
     { "del-tlv-map", "switch [map]",
-      1, 2, ofctl_del_tlv_map },
+      1, 2, ofctl_del_tlv_map, OVS_RO },
     { "dump-tlv-map", "switch",
-      1, 1, ofctl_dump_tlv_map },
-    { "help", NULL, 0, INT_MAX, ofctl_help },
-    { "list-commands", NULL, 0, INT_MAX, ofctl_list_commands },
+      1, 1, ofctl_dump_tlv_map, OVS_RO },
+    { "help", NULL, 0, INT_MAX, ofctl_help, OVS_RO },
+    { "list-commands", NULL, 0, INT_MAX, ofctl_list_commands, OVS_RO },
 
     /* Undocumented commands for testing. */
-    { "parse-flow", NULL, 1, 1, ofctl_parse_flow },
-    { "parse-flows", NULL, 1, 1, ofctl_parse_flows },
-    { "parse-nx-match", NULL, 0, 0, ofctl_parse_nxm },
-    { "parse-nxm", NULL, 0, 0, ofctl_parse_nxm },
-    { "parse-oxm", NULL, 1, 1, ofctl_parse_oxm },
-    { "parse-actions", NULL, 1, 1, ofctl_parse_actions },
-    { "parse-instructions", NULL, 1, 1, ofctl_parse_instructions },
-    { "parse-ofp10-match", NULL, 0, 0, ofctl_parse_ofp10_match },
-    { "parse-ofp11-match", NULL, 0, 0, ofctl_parse_ofp11_match },
-    { "parse-pcap", NULL, 1, INT_MAX, ofctl_parse_pcap },
-    { "check-vlan", NULL, 2, 2, ofctl_check_vlan },
-    { "print-error", NULL, 1, 1, ofctl_print_error },
-    { "encode-error-reply", NULL, 2, 2, ofctl_encode_error_reply },
-    { "ofp-print", NULL, 1, 2, ofctl_ofp_print },
-    { "encode-hello", NULL, 1, 1, ofctl_encode_hello },
-    { "parse-key-value", NULL, 1, INT_MAX, ofctl_parse_key_value },
-
-    { NULL, NULL, 0, 0, NULL },
+    { "parse-flow", NULL, 1, 1, ofctl_parse_flow, OVS_RW },
+    { "parse-flows", NULL, 1, 1, ofctl_parse_flows, OVS_RW },
+    { "parse-nx-match", NULL, 0, 0, ofctl_parse_nxm, OVS_RW },
+    { "parse-nxm", NULL, 0, 0, ofctl_parse_nxm, OVS_RW },
+    { "parse-oxm", NULL, 1, 1, ofctl_parse_oxm, OVS_RW },
+    { "parse-actions", NULL, 1, 1, ofctl_parse_actions, OVS_RW },
+    { "parse-instructions", NULL, 1, 1, ofctl_parse_instructions, OVS_RW },
+    { "parse-ofp10-match", NULL, 0, 0, ofctl_parse_ofp10_match, OVS_RW },
+    { "parse-ofp11-match", NULL, 0, 0, ofctl_parse_ofp11_match, OVS_RW },
+    { "parse-pcap", NULL, 1, INT_MAX, ofctl_parse_pcap, OVS_RW },
+    { "check-vlan", NULL, 2, 2, ofctl_check_vlan, OVS_RW },
+    { "print-error", NULL, 1, 1, ofctl_print_error, OVS_RW },
+    { "encode-error-reply", NULL, 2, 2, ofctl_encode_error_reply, OVS_RW },
+    { "ofp-print", NULL, 1, 2, ofctl_ofp_print, OVS_RW },
+    { "encode-hello", NULL, 1, 1, ofctl_encode_hello, OVS_RW },
+    { "parse-key-value", NULL, 1, INT_MAX, ofctl_parse_key_value, OVS_RW },
+
+    { NULL, NULL, 0, 0, NULL, OVS_RO },
 };
 
 static const struct ovs_cmdl_command *get_all_commands(void)