[ovs-dev,v2,3/3] Support accepting and displaying table names in OVS tools.

Message ID 20180112205722.25360-4-blp@ovn.org
State Superseded
Headers show
Series
  • support table names in CLI
Related show

Commit Message

Ben Pfaff Jan. 12, 2018, 8:57 p.m.
OpenFlow has little-known support for naming tables.  Open vSwitch has
supported table names for ages, but it has never used or displayed them
outside of commands dedicated to table manipulation.  This commit adds
support for table names in ovs-ofctl.  When a table has a name, it displays
that name in flows and actions, so that, for example, the following:
    table=1, arp, actions=resubmit(,2)
might become:
    table=ingress_acl, arp, actions=resubmit(,mac_learning)
given appropriately named tables.

For backward compatibility, only interactive ovs-ofctl commands by default
display table names; to display them in scripts, use the new --names
option.

This feature was inspired by a talk that Kei Nohguchi presented at Open
vSwitch 2017 Fall Conference.

CC: Kei Nohguchi <kei@nohguchi.com>
Signed-off-by: Ben Pfaff <blp@ovn.org>
---
 NEWS                              |  11 ++
 include/openvswitch/ofp-actions.h |   2 +
 include/openvswitch/ofp-parse.h   |  20 ++-
 include/openvswitch/ofp-print.h   |  12 +-
 include/openvswitch/ofp-util.h    |   8 ++
 lib/learn.c                       |  20 ++-
 lib/learn.h                       |   6 +-
 lib/learning-switch.c             |   2 +-
 lib/ofp-actions.c                 |  45 +++---
 lib/ofp-parse.c                   |  80 +++++++----
 lib/ofp-print.c                   | 202 ++++++++++++++++----------
 lib/ofp-util.c                    | 109 ++++++++++++--
 lib/vconn.c                       |  10 +-
 ofproto/ofproto-dpif.c            |   4 +-
 ofproto/ofproto.c                 |   2 +-
 ovn/controller/ofctrl.c           |  12 +-
 ovn/controller/pinctrl.c          |   2 +-
 ovn/utilities/ovn-sbctl.c         |   2 +-
 ovn/utilities/ovn-trace.c         |   2 +-
 tests/ofproto.at                  |  89 ++++++++----
 utilities/ovs-ofctl.8.in          |  90 +++++++-----
 utilities/ovs-ofctl.c             | 294 +++++++++++++++++++++++++++++++-------
 utilities/ovs-testcontroller.c    |   2 +-
 23 files changed, 739 insertions(+), 287 deletions(-)

Comments

Kei Nohguchi Jan. 12, 2018, 9:07 p.m. | #1
Hi Ben,

On Fri, Jan 12, 2018 at 12:57:22PM -0800, Ben Pfaff wrote:
> OpenFlow has little-known support for naming tables.  Open vSwitch has
> supported table names for ages, but it has never used or displayed them
> outside of commands dedicated to table manipulation.  This commit adds
> support for table names in ovs-ofctl.  When a table has a name, it displays
> that name in flows and actions, so that, for example, the following:
>     table=1, arp, actions=resubmit(,2)
> might become:
>     table=ingress_acl, arp, actions=resubmit(,mac_learning)
> given appropriately named tables.
> 
> For backward compatibility, only interactive ovs-ofctl commands by default
> display table names; to display them in scripts, use the new --names
> option.
> 
> This feature was inspired by a talk that Kei Nohguchi presented at Open
> vSwitch 2017 Fall Conference.
> 
> CC: Kei Nohguchi <kei@nohguchi.com>
> Signed-off-by: Ben Pfaff <blp@ovn.org>

Woo!  Thank you, Ben, for conidering that!

I'll apply it locally and get back to you.

Kei
Ben Pfaff Jan. 31, 2018, 7:37 p.m. | #2
On Fri, Jan 12, 2018 at 01:07:24PM -0800, Kei Nohguchi wrote:
> Hi Ben,
> 
> On Fri, Jan 12, 2018 at 12:57:22PM -0800, Ben Pfaff wrote:
> > OpenFlow has little-known support for naming tables.  Open vSwitch has
> > supported table names for ages, but it has never used or displayed them
> > outside of commands dedicated to table manipulation.  This commit adds
> > support for table names in ovs-ofctl.  When a table has a name, it displays
> > that name in flows and actions, so that, for example, the following:
> >     table=1, arp, actions=resubmit(,2)
> > might become:
> >     table=ingress_acl, arp, actions=resubmit(,mac_learning)
> > given appropriately named tables.
> > 
> > For backward compatibility, only interactive ovs-ofctl commands by default
> > display table names; to display them in scripts, use the new --names
> > option.
> > 
> > This feature was inspired by a talk that Kei Nohguchi presented at Open
> > vSwitch 2017 Fall Conference.
> > 
> > CC: Kei Nohguchi <kei@nohguchi.com>
> > Signed-off-by: Ben Pfaff <blp@ovn.org>
> 
> Woo!  Thank you, Ben, for conidering that!
> 
> I'll apply it locally and get back to you.

Do you still plan to try this out?

I just sent out v3, but it's just a rebase:
https://patchwork.ozlabs.org/project/openvswitch/list/?series=26321

Patch

diff --git a/NEWS b/NEWS
index cb020d00d00c..edc2cff7cbd5 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,14 @@ 
+Post-v2.9.0
+--------------------
+   - ovs-ofctl:
+     * ovs-ofctl now accepts and display table names in place of numbers.  By
+       default it always accepts names and in interactive use it displays them;
+       use --names or --no-names to override.  See ovs-ofctl(8) for details.
+   - ovs-vswitchd:
+     * Previous versions gave OpenFlow tables default names of the form
+       "table#".  These are not helpful names for the purpose of accepting
+       and displaying table names, so now tables by default have no names.
+
 Post-v2.8.0
 --------------------
    - NSH implementation now conforms to latest draft (draft-ietf-sfc-nsh-28).
diff --git a/include/openvswitch/ofp-actions.h b/include/openvswitch/ofp-actions.h
index 454c705ccf73..cba027b1d945 100644
--- a/include/openvswitch/ofp-actions.h
+++ b/include/openvswitch/ofp-actions.h
@@ -1068,6 +1068,7 @@  uint32_t ofpacts_get_meter(const struct ofpact[], size_t ofpacts_len);
 struct ofpact_format_params {
     /* Input. */
     const struct ofputil_port_map *port_map;
+    const struct ofputil_table_map *table_map;
 
     /* Output. */
     struct ds *s;
@@ -1080,6 +1081,7 @@  const char *ofpact_name(enum ofpact_type);
 struct ofpact_parse_params {
     /* Input. */
     const struct ofputil_port_map *port_map;
+    const struct ofputil_table_map *table_map;
 
     /* Output. */
     struct ofpbuf *ofpacts;
diff --git a/include/openvswitch/ofp-parse.h b/include/openvswitch/ofp-parse.h
index 013a8f3edf70..c4228a50358f 100644
--- a/include/openvswitch/ofp-parse.h
+++ b/include/openvswitch/ofp-parse.h
@@ -45,26 +45,33 @@  enum ofputil_protocol;
 
 char *parse_ofp_str(struct ofputil_flow_mod *, int command, const char *str_,
                     const struct ofputil_port_map *,
+                    const struct ofputil_table_map *,
                     enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
 
 char *parse_ofp_flow_mod_str(struct ofputil_flow_mod *, const char *string,
-                             const struct ofputil_port_map *, int command,
+                             const struct ofputil_port_map *,
+                             const struct ofputil_table_map *,
+                             int command,
                              enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
 
 char *parse_ofp_packet_out_str(struct ofputil_packet_out *po, const char *str_,
                                const struct ofputil_port_map *,
+                               const struct ofputil_table_map *,
                                enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
 
 char *parse_ofp_table_mod(struct ofputil_table_mod *,
                           const char *table_id, const char *flow_miss_handling,
+                          const struct ofputil_table_map *,
                           uint32_t *usable_versions)
     OVS_WARN_UNUSED_RESULT;
 
 char *parse_ofp_flow_mod_file(const char *file_name,
-                              const struct ofputil_port_map *, int command,
+                              const struct ofputil_port_map *,
+                              const struct ofputil_table_map *,
+                              int command,
                               struct ofputil_flow_mod **fms, size_t *n_fms,
                               enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
@@ -72,6 +79,7 @@  char *parse_ofp_flow_mod_file(const char *file_name,
 char *parse_ofp_flow_stats_request_str(struct ofputil_flow_stats_request *,
                                        bool aggregate, const char *string,
                                        const struct ofputil_port_map *,
+                                       const struct ofputil_table_map *,
                                        enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
 
@@ -86,12 +94,14 @@  char *parse_ofp_meter_mod_str(struct ofputil_meter_mod *, const char *string,
 
 char *parse_flow_monitor_request(struct ofputil_flow_monitor_request *,
                                  const char *,
-                                 const struct ofputil_port_map *port_map,
+                                 const struct ofputil_port_map *,
+                                 const struct ofputil_table_map *,
                                  enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
 
 char *parse_ofp_group_mod_file(const char *file_name,
-                               const struct ofputil_port_map *, int command,
+                               const struct ofputil_port_map *,
+                               const struct ofputil_table_map *, int command,
                                struct ofputil_group_mod **gms, size_t *n_gms,
                                enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
@@ -99,11 +109,13 @@  char *parse_ofp_group_mod_file(const char *file_name,
 char *parse_ofp_group_mod_str(struct ofputil_group_mod *, int command,
                               const char *string,
                               const struct ofputil_port_map *,
+                              const struct ofputil_table_map *,
                               enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
 
 char *parse_ofp_bundle_file(const char *file_name,
                             const struct ofputil_port_map *,
+                            const struct ofputil_table_map *,
                             struct ofputil_bundle_msg **, size_t *n_bms,
                             enum ofputil_protocol *)
     OVS_WARN_UNUSED_RESULT;
diff --git a/include/openvswitch/ofp-print.h b/include/openvswitch/ofp-print.h
index d02634e3e91c..ed113786a28c 100644
--- a/include/openvswitch/ofp-print.h
+++ b/include/openvswitch/ofp-print.h
@@ -31,6 +31,7 @@  struct ofp_header;
 struct ofputil_flow_stats;
 struct ofputil_port_map;
 struct ofputil_table_features;
+struct ofputil_table_map;
 struct ofputil_table_stats;
 struct dp_packet;
 
@@ -39,7 +40,7 @@  extern "C" {
 #endif
 
 void ofp_print(FILE *, const void *, size_t, const struct ofputil_port_map *,
-               int verbosity);
+               const struct ofputil_table_map *, int verbosity);
 void ofp_print_packet(FILE *stream, const void *data,
                       size_t len, ovs_be32 packet_type);
 void ofp_print_dp_packet(FILE *stream, const struct dp_packet *packet);
@@ -48,7 +49,7 @@  void ofp10_match_print(struct ds *, const struct ofp10_match *,
                        const struct ofputil_port_map *, int verbosity);
 
 char *ofp_to_string(const void *, size_t, const struct ofputil_port_map *,
-                    int verbosity);
+                    const struct ofputil_table_map *, int verbosity);
 char *ofp10_match_to_string(const struct ofp10_match *,
                             const struct ofputil_port_map *, int verbosity);
 char *ofp_packet_to_string(const void *data, size_t len, ovs_be32 packet_type);
@@ -59,10 +60,13 @@  void ofp_print_table_features(
     struct ds *, const struct ofputil_table_features *features,
     const struct ofputil_table_features *prev_features,
     const struct ofputil_table_stats *stats,
-    const struct ofputil_table_stats *prev_stats);
+    const struct ofputil_table_stats *prev_stats,
+    const struct ofputil_table_map *table_map);
 
 void ofp_print_flow_stats(struct ds *, const struct ofputil_flow_stats *,
-                          const struct ofputil_port_map *, bool show_stats);
+                          const struct ofputil_port_map *,
+                          const struct ofputil_table_map *,
+                          bool show_stats);
 
 #ifdef  __cplusplus
 }
diff --git a/include/openvswitch/ofp-util.h b/include/openvswitch/ofp-util.h
index d9780dd44582..5dd1b34c216b 100644
--- a/include/openvswitch/ofp-util.h
+++ b/include/openvswitch/ofp-util.h
@@ -816,6 +816,14 @@  void ofputil_table_map_put(struct ofputil_table_map *,
                            uint8_t, const char *name);
 void ofputil_table_map_destroy(struct ofputil_table_map *);
 
+/* Table numbers. */
+bool ofputil_table_from_string(const char *, const struct ofputil_table_map *,
+                               uint8_t *tablep);
+void ofputil_format_table(uint8_t table, const struct ofputil_table_map *,
+                         struct ds *);
+void ofputil_table_to_string(uint8_t, const struct ofputil_table_map *,
+                            char *namebuf, size_t bufsize);
+
 /* Abstract ofp_table_mod. */
 struct ofputil_table_mod {
     uint8_t table_id;         /* ID of the table, 0xff indicates all tables. */
diff --git a/lib/learn.c b/lib/learn.c
index 9e321371c0a5..5164082f8e52 100644
--- a/lib/learn.c
+++ b/lib/learn.c
@@ -381,6 +381,7 @@  learn_parse_spec(const char *orig, char *name, char *value,
  * error.  The caller is responsible for freeing the returned string. */
 static char * OVS_WARN_UNUSED_RESULT
 learn_parse__(char *orig, char *arg, const struct ofputil_port_map *port_map,
+              const struct ofputil_table_map *table_map,
               struct ofpbuf *ofpacts)
 {
     struct ofpact_learn *learn;
@@ -396,8 +397,10 @@  learn_parse__(char *orig, char *arg, const struct ofputil_port_map *port_map,
     match_init_catchall(&match);
     while (ofputil_parse_key_value(&arg, &name, &value)) {
         if (!strcmp(name, "table")) {
-            learn->table_id = atoi(value);
-            if (learn->table_id == 255) {
+            if (!ofputil_table_from_string(value, table_map,
+                                           &learn->table_id)) {
+                return xasprintf("unknown table \"%s\"", value);
+            } else if (learn->table_id == 255) {
                 return xasprintf("%s: table id 255 not valid for `learn' "
                                  "action", orig);
             }
@@ -465,10 +468,11 @@  learn_parse__(char *orig, char *arg, const struct ofputil_port_map *port_map,
  * Modifies 'arg'. */
 char * OVS_WARN_UNUSED_RESULT
 learn_parse(char *arg, const struct ofputil_port_map *port_map,
+            const struct ofputil_table_map *table_map,
             struct ofpbuf *ofpacts)
 {
     char *orig = xstrdup(arg);
-    char *error = learn_parse__(orig, arg, port_map, ofpacts);
+    char *error = learn_parse__(orig, arg, port_map, table_map, ofpacts);
     free(orig);
     return error;
 }
@@ -477,16 +481,18 @@  learn_parse(char *arg, const struct ofputil_port_map *port_map,
  * describes. */
 void
 learn_format(const struct ofpact_learn *learn,
-             const struct ofputil_port_map *port_map, struct ds *s)
+             const struct ofputil_port_map *port_map,
+             const struct ofputil_table_map *table_map,
+             struct ds *s)
 {
     const struct ofpact_learn_spec *spec;
     struct match match;
 
     match_init_catchall(&match);
 
-    ds_put_format(s, "%slearn(%s%stable=%s%"PRIu8,
-                  colors.learn, colors.end, colors.special, colors.end,
-                  learn->table_id);
+    ds_put_format(s, "%slearn(%s%stable=%s",
+                  colors.learn, colors.end, colors.special, colors.end);
+    ofputil_format_table(learn->table_id, table_map, s);
     if (learn->idle_timeout != OFP_FLOW_PERMANENT) {
         ds_put_format(s, ",%sidle_timeout=%s%"PRIu16,
                       colors.param, colors.end, learn->idle_timeout);
diff --git a/lib/learn.h b/lib/learn.h
index 31d3a14e827a..2bdfee702dac 100644
--- a/lib/learn.h
+++ b/lib/learn.h
@@ -28,6 +28,7 @@  struct ofpbuf;
 struct ofpact_learn;
 struct ofputil_flow_mod;
 struct ofputil_port_map;
+struct ofputil_table_map;
 struct nx_action_learn;
 
 /* NXAST_LEARN helper functions.
@@ -41,9 +42,10 @@  void learn_execute(const struct ofpact_learn *, const struct flow *,
 void learn_mask(const struct ofpact_learn *, struct flow_wildcards *);
 
 char *learn_parse(char *, const struct ofputil_port_map *,
-                  struct ofpbuf *ofpacts)
+                  const struct ofputil_table_map *, struct ofpbuf *ofpacts)
     OVS_WARN_UNUSED_RESULT;
 void learn_format(const struct ofpact_learn *,
-                  const struct ofputil_port_map *, struct ds *);
+                  const struct ofputil_port_map *,
+                  const struct ofputil_table_map *, struct ds *);
 
 #endif /* learn.h */
diff --git a/lib/learning-switch.c b/lib/learning-switch.c
index 5b014e5f3757..f840f034875f 100644
--- a/lib/learning-switch.c
+++ b/lib/learning-switch.c
@@ -369,7 +369,7 @@  lswitch_process_packet(struct lswitch *sw, const struct ofpbuf *msg)
     } else if (type == OFPTYPE_FLOW_REMOVED) {
         /* Nothing to do. */
     } else if (VLOG_IS_DBG_ENABLED()) {
-        char *s = ofp_to_string(msg->data, msg->size, NULL, 2);
+        char *s = ofp_to_string(msg->data, msg->size, NULL, NULL, 2);
         VLOG_DBG_RL(&rl, "%016llx: OpenFlow packet ignored: %s",
                     sw->datapath_id, s);
         free(s);
diff --git a/lib/ofp-actions.c b/lib/ofp-actions.c
index 93792ddfca4b..47b6fcd6c691 100644
--- a/lib/ofp-actions.c
+++ b/lib/ofp-actions.c
@@ -4398,14 +4398,10 @@  parse_RESUBMIT(char *arg, const struct ofpact_parse_params *pp)
 
     table_s = strsep(&arg, ",");
     if (table_s && table_s[0]) {
-        uint32_t table_id = 0;
-        char *error;
-
-        error = str_to_u32(table_s, &table_id);
-        if (error) {
-            return error;
+        if (!ofputil_table_from_string(table_s, pp->table_map,
+                                       &resubmit->table_id)) {
+            return xasprintf("%s: resubmit to unknown table", table_s);
         }
-        resubmit->table_id = table_id;
     } else {
         resubmit->table_id = 255;
     }
@@ -4441,7 +4437,7 @@  format_RESUBMIT(const struct ofpact_resubmit *a,
         }
         ds_put_char(fp->s, ',');
         if (a->table_id != 255) {
-            ds_put_format(fp->s, "%"PRIu8, a->table_id);
+            ofputil_format_table(a->table_id, fp->table_map, fp->s);
         }
         if (a->with_ct_orig) {
             ds_put_cstr(fp->s, ",ct");
@@ -5024,14 +5020,14 @@  encode_LEARN(const struct ofpact_learn *learn,
 static char * OVS_WARN_UNUSED_RESULT
 parse_LEARN(char *arg, const struct ofpact_parse_params *pp)
 {
-    return learn_parse(arg, pp->port_map, pp->ofpacts);
+    return learn_parse(arg, pp->port_map, pp->table_map, pp->ofpacts);
 }
 
 static void
 format_LEARN(const struct ofpact_learn *a,
              const struct ofpact_format_params *fp)
 {
-    learn_format(a, fp->port_map, fp->s);
+    learn_format(a, fp->port_map, fp->table_map, fp->s);
 }
 
 /* Action structure for NXAST_CONJUNCTION. */
@@ -5372,10 +5368,11 @@  static void
 format_UNROLL_XLATE(const struct ofpact_unroll_xlate *a,
                     const struct ofpact_format_params *fp)
 {
-    ds_put_format(fp->s, "%sunroll_xlate(%s%stable=%s%"PRIu8
-                  ", %scookie=%s%"PRIu64"%s)%s",
+    ds_put_format(fp->s, "%sunroll_xlate(%s%stable=%s",
                   colors.paren,   colors.end,
-                  colors.special, colors.end, a->rule_table_id,
+                  colors.special, colors.end);
+    ofputil_format_table(a->rule_table_id, fp->table_map, fp->s);
+    ds_put_format(fp->s, ", %scookie=%s%"PRIu64"%s)%s",
                   colors.param,   colors.end, ntohll(a->rule_cookie),
                   colors.paren,   colors.end);
 }
@@ -6038,8 +6035,10 @@  parse_CT(char *arg, const struct ofpact_parse_params *pp)
         } else if (!strcmp(key, "force")) {
             oc->flags |= NX_CT_F_FORCE;
         } else if (!strcmp(key, "table")) {
-            error = str_to_u8(value, "recirc_table", &oc->recirc_table);
-            if (!error && oc->recirc_table == NX_CT_RECIRC_NONE) {
+            if (!ofputil_table_from_string(value, pp->table_map,
+                                           &oc->recirc_table)) {
+                error = xasprintf("unknown table %s", value);
+            } else if (oc->recirc_table == NX_CT_RECIRC_NONE) {
                 error = xasprintf("invalid table %#"PRIx8, oc->recirc_table);
             }
         } else if (!strcmp(key, "zone")) {
@@ -6125,8 +6124,9 @@  format_CT(const struct ofpact_conntrack *a,
         ds_put_format(fp->s, "%sforce%s,", colors.value, colors.end);
     }
     if (a->recirc_table != NX_CT_RECIRC_NONE) {
-        ds_put_format(fp->s, "%stable=%s%"PRIu8",",
-                      colors.special, colors.end, a->recirc_table);
+        ds_put_format(fp->s, "%stable=%s", colors.special, colors.end);
+        ofputil_format_table(a->recirc_table, fp->table_map, fp->s);
+        ds_put_char(fp->s, ',');
     }
     if (a->zone_src.field) {
         ds_put_format(fp->s, "%szone=%s", colors.param, colors.end);
@@ -6816,19 +6816,18 @@  static char * OVS_WARN_UNUSED_RESULT
 parse_GOTO_TABLE(char *arg, const struct ofpact_parse_params *pp)
 {
     struct ofpact_goto_table *ogt = ofpact_put_GOTO_TABLE(pp->ofpacts);
-    char *table_s = strsep(&arg, ",");
-    if (!table_s || !table_s[0]) {
-        return xstrdup("instruction goto-table needs table id");
+    if (!ofputil_table_from_string(arg, pp->table_map, &ogt->table_id)) {
+        return xasprintf("unknown table \"%s\"", arg);
     }
-    return str_to_u8(table_s, "table", &ogt->table_id);
+    return NULL;
 }
 
 static void
 format_GOTO_TABLE(const struct ofpact_goto_table *a,
                   const struct ofpact_format_params *fp)
 {
-    ds_put_format(fp->s, "%sgoto_table:%s%"PRIu8,
-                  colors.param, colors.end, a->table_id);
+    ds_put_format(fp->s, "%sgoto_table:%s", colors.param, colors.end);
+    ofputil_format_table(a->table_id, fp->table_map, fp->s);
 }
 
 static void
diff --git a/lib/ofp-parse.c b/lib/ofp-parse.c
index b68081740f09..1e30c20f4966 100644
--- a/lib/ofp-parse.c
+++ b/lib/ofp-parse.c
@@ -322,6 +322,7 @@  extract_actions(char *s)
 static char * OVS_WARN_UNUSED_RESULT
 parse_ofp_str__(struct ofputil_flow_mod *fm, int command, char *string,
                 const struct ofputil_port_map *port_map,
+                const struct ofputil_table_map *table_map,
                 enum ofputil_protocol *usable_protocols)
 {
     enum {
@@ -450,7 +451,10 @@  parse_ofp_str__(struct ofputil_flow_mod *fm, int command, char *string,
             }
 
             if (!strcmp(name, "table")) {
-                error = str_to_u8(value, "table", &fm->table_id);
+                if (!ofputil_table_from_string(value, table_map,
+                                               &fm->table_id)) {
+                    return xasprintf("unknown table \"%s\"", value);
+                }
                 if (fm->table_id != 0xff) {
                     *usable_protocols &= OFPUTIL_P_TID;
                 }
@@ -559,6 +563,7 @@  parse_ofp_str__(struct ofputil_flow_mod *fm, int command, char *string,
         ofpbuf_init(&ofpacts, 32);
         struct ofpact_parse_params pp = {
             .port_map = port_map,
+            .table_map = table_map,
             .ofpacts = &ofpacts,
             .usable_protocols = &action_usable_protocols
         };
@@ -610,12 +615,14 @@  parse_ofp_str__(struct ofputil_flow_mod *fm, int command, char *string,
 char * OVS_WARN_UNUSED_RESULT
 parse_ofp_str(struct ofputil_flow_mod *fm, int command, const char *str_,
               const struct ofputil_port_map *port_map,
+              const struct ofputil_table_map *table_map,
               enum ofputil_protocol *usable_protocols)
 {
     char *string = xstrdup(str_);
     char *error;
 
-    error = parse_ofp_str__(fm, command, string, port_map, usable_protocols);
+    error = parse_ofp_str__(fm, command, string, port_map, table_map,
+                            usable_protocols);
     if (error) {
         fm->ofpacts = NULL;
         fm->ofpacts_len = 0;
@@ -630,6 +637,7 @@  parse_ofp_str(struct ofputil_flow_mod *fm, int command, const char *str_,
 static char * OVS_WARN_UNUSED_RESULT
 parse_ofp_packet_out_str__(struct ofputil_packet_out *po, char *string,
                            const struct ofputil_port_map *port_map,
+                           const struct ofputil_table_map *table_map,
                            enum ofputil_protocol *usable_protocols)
 {
     enum ofputil_protocol action_usable_protocols;
@@ -719,6 +727,7 @@  parse_ofp_packet_out_str__(struct ofputil_packet_out *po, char *string,
     if (act_str) {
         struct ofpact_parse_params pp = {
             .port_map = port_map,
+            .table_map = table_map,
             .ofpacts = &ofpacts,
             .usable_protocols = &action_usable_protocols,
         };
@@ -750,12 +759,14 @@  out:
 char * OVS_WARN_UNUSED_RESULT
 parse_ofp_packet_out_str(struct ofputil_packet_out *po, const char *str_,
                          const struct ofputil_port_map *port_map,
+                         const struct ofputil_table_map *table_map,
                          enum ofputil_protocol *usable_protocols)
 {
     char *string = xstrdup(str_);
     char *error;
 
-    error = parse_ofp_packet_out_str__(po, string, port_map, usable_protocols);
+    error = parse_ofp_packet_out_str__(po, string, port_map, table_map,
+                                       usable_protocols);
     if (error) {
         po->ofpacts = NULL;
         po->ofpacts_len = 0;
@@ -996,6 +1007,7 @@  static char * OVS_WARN_UNUSED_RESULT
 parse_flow_monitor_request__(struct ofputil_flow_monitor_request *fmr,
                              const char *str_,
                              const struct ofputil_port_map *port_map,
+                             const struct ofputil_table_map *table_map,
                              char *string,
                              enum ofputil_protocol *usable_protocols)
 {
@@ -1040,7 +1052,10 @@  parse_flow_monitor_request__(struct ofputil_flow_monitor_request *fmr,
             }
 
             if (!strcmp(name, "table")) {
-                error = str_to_u8(value, "table", &fmr->table_id);
+                if (!ofputil_table_from_string(value, table_map,
+                                               &fmr->table_id)) {
+                    error = xasprintf("unknown table \"%s\"", value);
+                }
             } else if (!strcmp(name, "out_port")) {
                 fmr->out_port = u16_to_ofp(atoi(value));
             } else {
@@ -1064,11 +1079,12 @@  char * OVS_WARN_UNUSED_RESULT
 parse_flow_monitor_request(struct ofputil_flow_monitor_request *fmr,
                            const char *str_,
                            const struct ofputil_port_map *port_map,
+                           const struct ofputil_table_map *table_map,
                            enum ofputil_protocol *usable_protocols)
 {
     char *string = xstrdup(str_);
-    char *error = parse_flow_monitor_request__(fmr, str_, port_map, string,
-                                               usable_protocols);
+    char *error = parse_flow_monitor_request__(fmr, str_, port_map, table_map,
+                                               string, usable_protocols);
     free(string);
     return error;
 }
@@ -1084,10 +1100,12 @@  parse_flow_monitor_request(struct ofputil_flow_monitor_request *fmr,
  * error.  The caller is responsible for freeing the returned string. */
 char * OVS_WARN_UNUSED_RESULT
 parse_ofp_flow_mod_str(struct ofputil_flow_mod *fm, const char *string,
-                       const struct ofputil_port_map *port_map, int command,
+                       const struct ofputil_port_map *port_map,
+                       const struct ofputil_table_map *table_map,
+                       int command,
                        enum ofputil_protocol *usable_protocols)
 {
-    char *error = parse_ofp_str(fm, command, string, port_map,
+    char *error = parse_ofp_str(fm, command, string, port_map, table_map,
                                 usable_protocols);
 
     if (!error) {
@@ -1167,16 +1185,16 @@  exit:
  * error.  The caller is responsible for freeing the returned string. */
 char * OVS_WARN_UNUSED_RESULT
 parse_ofp_table_mod(struct ofputil_table_mod *tm, const char *table_id,
-                    const char *setting, uint32_t *usable_versions)
+                    const char *setting,
+                    const struct ofputil_table_map *table_map,
+                    uint32_t *usable_versions)
 {
     *usable_versions = 0;
     if (!strcasecmp(table_id, "all")) {
         tm->table_id = OFPTT_ALL;
-    } else {
-        char *error = str_to_u8(table_id, "table_id", &tm->table_id);
-        if (error) {
-            return error;
-        }
+    } else if (!ofputil_table_from_string(table_id, table_map,
+                                          &tm->table_id)) {
+        return xasprintf("unknown table \"%s\"", table_id);
     }
 
     tm->miss = OFPUTIL_TABLE_MISS_DEFAULT;
@@ -1240,7 +1258,9 @@  parse_ofp_table_mod(struct ofputil_table_mod *tm, const char *table_id,
  * error.  The caller is responsible for freeing the returned string. */
 char * OVS_WARN_UNUSED_RESULT
 parse_ofp_flow_mod_file(const char *file_name,
-                        const struct ofputil_port_map *port_map, int command,
+                        const struct ofputil_port_map *port_map,
+                        const struct ofputil_table_map *table_map,
+                        int command,
                         struct ofputil_flow_mod **fms, size_t *n_fms,
                         enum ofputil_protocol *usable_protocols)
 {
@@ -1271,7 +1291,7 @@  parse_ofp_flow_mod_file(const char *file_name,
             *fms = x2nrealloc(*fms, &allocated_fms, sizeof **fms);
         }
         error = parse_ofp_flow_mod_str(&(*fms)[*n_fms], ds_cstr(&s), port_map,
-                                       command, &usable);
+                                       table_map, command, &usable);
         if (error) {
             char *err_msg;
             size_t i;
@@ -1307,12 +1327,14 @@  char * OVS_WARN_UNUSED_RESULT
 parse_ofp_flow_stats_request_str(struct ofputil_flow_stats_request *fsr,
                                  bool aggregate, const char *string,
                                  const struct ofputil_port_map *port_map,
+                                 const struct ofputil_table_map *table_map,
                                  enum ofputil_protocol *usable_protocols)
 {
     struct ofputil_flow_mod fm;
     char *error;
 
-    error = parse_ofp_str(&fm, -1, string, port_map, usable_protocols);
+    error = parse_ofp_str(&fm, -1, string, port_map, table_map,
+                          usable_protocols);
     if (error) {
         return error;
     }
@@ -1438,8 +1460,9 @@  exit:
 
 static char * OVS_WARN_UNUSED_RESULT
 parse_bucket_str(struct ofputil_bucket *bucket, char *str_,
-                 const struct ofputil_port_map *port_map, uint8_t group_type,
-                 enum ofputil_protocol *usable_protocols)
+                 const struct ofputil_port_map *port_map,
+                 const struct ofputil_table_map *table_map,
+                 uint8_t group_type, enum ofputil_protocol *usable_protocols)
 {
     char *pos, *key, *value;
     struct ofpbuf ofpacts;
@@ -1497,6 +1520,7 @@  parse_bucket_str(struct ofputil_bucket *bucket, char *str_,
     ofpbuf_init(&ofpacts, 0);
     struct ofpact_parse_params pp = {
         .port_map = port_map,
+        .table_map = table_map,
         .ofpacts = &ofpacts,
         .usable_protocols = usable_protocols,
     };
@@ -1575,6 +1599,7 @@  static char * OVS_WARN_UNUSED_RESULT
 parse_ofp_group_mod_str__(struct ofputil_group_mod *gm, int command,
                           char *string,
                           const struct ofputil_port_map *port_map,
+                          const struct ofputil_table_map *table_map,
                           enum ofputil_protocol *usable_protocols)
 {
     enum {
@@ -1828,7 +1853,7 @@  parse_ofp_group_mod_str__(struct ofputil_group_mod *gm, int command,
         }
 
         bucket = xzalloc(sizeof(struct ofputil_bucket));
-        error = parse_bucket_str(bucket, bkt_str, port_map,
+        error = parse_bucket_str(bucket, bkt_str, port_map, table_map,
                                  gm->type, usable_protocols);
         if (error) {
             free(bucket);
@@ -1862,11 +1887,12 @@  char * OVS_WARN_UNUSED_RESULT
 parse_ofp_group_mod_str(struct ofputil_group_mod *gm, int command,
                         const char *str_,
                         const struct ofputil_port_map *port_map,
+                        const struct ofputil_table_map *table_map,
                         enum ofputil_protocol *usable_protocols)
 {
     char *string = xstrdup(str_);
-    char *error = parse_ofp_group_mod_str__(gm, command, string,
-                                            port_map, usable_protocols);
+    char *error = parse_ofp_group_mod_str__(gm, command, string, port_map,
+                                            table_map, usable_protocols);
     free(string);
     return error;
 }
@@ -1878,6 +1904,7 @@  parse_ofp_group_mod_str(struct ofputil_group_mod *gm, int command,
 char * OVS_WARN_UNUSED_RESULT
 parse_ofp_group_mod_file(const char *file_name,
                          const struct ofputil_port_map *port_map,
+                         const struct ofputil_table_map *table_map,
                          int command,
                          struct ofputil_group_mod **gms, size_t *n_gms,
                          enum ofputil_protocol *usable_protocols)
@@ -1915,7 +1942,7 @@  parse_ofp_group_mod_file(const char *file_name,
             *gms = new_gms;
         }
         error = parse_ofp_group_mod_str(&(*gms)[*n_gms], command, ds_cstr(&s),
-                                        port_map, &usable);
+                                        port_map, table_map, &usable);
         if (error) {
             size_t i;
 
@@ -1956,6 +1983,7 @@  parse_ofp_group_mod_file(const char *file_name,
 char * OVS_WARN_UNUSED_RESULT
 parse_ofp_bundle_file(const char *file_name,
                       const struct ofputil_port_map *port_map,
+                      const struct ofputil_table_map *table_map,
                       struct ofputil_bundle_msg **bms, size_t *n_bms,
                       enum ofputil_protocol *usable_protocols)
 {
@@ -2003,7 +2031,7 @@  parse_ofp_bundle_file(const char *file_name,
         if (!strncmp(s, "flow", len)) {
             s += len;
             error = parse_ofp_flow_mod_str(&(*bms)[*n_bms].fm, s, port_map,
-                                           -2, &usable);
+                                           table_map, -2, &usable);
             if (error) {
                 break;
             }
@@ -2011,7 +2039,7 @@  parse_ofp_bundle_file(const char *file_name,
         } else if (!strncmp(s, "group", len)) {
             s += len;
             error = parse_ofp_group_mod_str(&(*bms)[*n_bms].gm, -2, s,
-                                            port_map, &usable);
+                                            port_map, table_map, &usable);
             if (error) {
                 break;
             }
@@ -2019,7 +2047,7 @@  parse_ofp_bundle_file(const char *file_name,
         } else if (!strncmp(s, "packet-out", len)) {
             s += len;
             error = parse_ofp_packet_out_str(&(*bms)[*n_bms].po, s, port_map,
-                                             &usable);
+                                             table_map, &usable);
             if (error) {
                 break;
             }
diff --git a/lib/ofp-print.c b/lib/ofp-print.c
index 639d3bddf36d..dede3197cb98 100644
--- a/lib/ofp-print.c
+++ b/lib/ofp-print.c
@@ -119,7 +119,8 @@  format_hex_arg(struct ds *s, const uint8_t *data, size_t len)
 
 static enum ofperr
 ofp_print_packet_in(struct ds *string, const struct ofp_header *oh,
-                    const struct ofputil_port_map *port_map, int verbosity)
+                    const struct ofputil_port_map *port_map,
+                    const struct ofputil_table_map *table_map, int verbosity)
 {
     char reasonbuf[OFPUTIL_PACKET_IN_REASON_BUFSIZE];
     struct ofputil_packet_in_private pin;
@@ -134,8 +135,10 @@  ofp_print_packet_in(struct ds *string, const struct ofp_header *oh,
         return error;
     }
 
-    if (public->table_id) {
-        ds_put_format(string, " table_id=%"PRIu8, public->table_id);
+    if (public->table_id
+        || ofputil_table_map_get_name(table_map, public->table_id)) {
+        ds_put_format(string, " table_id=");
+        ofputil_format_table(public->table_id, table_map, string);
     }
 
     if (public->cookie != OVS_BE64_MAX) {
@@ -207,6 +210,7 @@  ofp_print_packet_in(struct ds *string, const struct ofp_header *oh,
 
     struct ofpact_format_params fp = {
         .port_map = port_map,
+        .table_map = table_map,
         .s = string,
     };
 
@@ -240,7 +244,8 @@  ofp_print_packet_in(struct ds *string, const struct ofp_header *oh,
 
 static enum ofperr
 ofp_print_packet_out(struct ds *string, const struct ofp_header *oh,
-                     const struct ofputil_port_map *port_map, int verbosity)
+                     const struct ofputil_port_map *port_map,
+                     const struct ofputil_table_map *table_map, int verbosity)
 {
     struct ofputil_packet_out po;
     struct ofpbuf ofpacts;
@@ -259,6 +264,7 @@  ofp_print_packet_out(struct ds *string, const struct ofp_header *oh,
     ds_put_cstr(string, " actions=");
     struct ofpact_format_params fp = {
         .port_map = port_map,
+        .table_map = table_map,
         .s = string,
     };
     ofpacts_format(po.ofpacts, po.ofpacts_len, &fp);
@@ -815,7 +821,8 @@  ofp_print_flow_flags(struct ds *s, enum ofputil_flow_mod_flags flags)
 
 static enum ofperr
 ofp_print_flow_mod(struct ds *s, const struct ofp_header *oh,
-                   const struct ofputil_port_map *port_map, int verbosity)
+                   const struct ofputil_port_map *port_map,
+                   const struct ofputil_table_map *table_map, int verbosity)
 {
     struct ofputil_flow_mod fm;
     struct ofpbuf ofpacts;
@@ -855,8 +862,10 @@  ofp_print_flow_mod(struct ds *s, const struct ofp_header *oh,
     default:
         ds_put_format(s, "cmd:%d", fm.command);
     }
-    if (fm.table_id != 0) {
-        ds_put_format(s, " table:%d", fm.table_id);
+    if (fm.table_id != 0
+        || ofputil_table_map_get_name(table_map, fm.table_id)) {
+        ds_put_format(s, " table:");
+        ofputil_format_table(fm.table_id, table_map, s);
     }
 
     ds_put_char(s, ' ');
@@ -927,6 +936,7 @@  ofp_print_flow_mod(struct ds *s, const struct ofp_header *oh,
     ds_put_cstr(s, "actions=");
     struct ofpact_format_params fp = {
         .port_map = port_map,
+        .table_map = table_map,
         .s = s,
     };
     ofpacts_format(fm.ofpacts, fm.ofpacts_len, &fp);
@@ -994,7 +1004,8 @@  ofp_flow_removed_reason_to_string(enum ofp_flow_removed_reason reason,
 
 static enum ofperr
 ofp_print_flow_removed(struct ds *string, const struct ofp_header *oh,
-                       const struct ofputil_port_map *port_map)
+                       const struct ofputil_port_map *port_map,
+                       const struct ofputil_table_map *table_map)
 {
     char reasonbuf[OFP_FLOW_REMOVED_REASON_BUFSIZE];
     struct ofputil_flow_removed fr;
@@ -1013,7 +1024,8 @@  ofp_print_flow_removed(struct ds *string, const struct ofp_header *oh,
                                                     sizeof reasonbuf));
 
     if (fr.table_id != 255) {
-        ds_put_format(string, " table_id=%"PRIu8, fr.table_id);
+        ds_put_format(string, " table_id=");
+        ofputil_format_table(fr.table_id, table_map, string);
     }
 
     if (fr.cookie != htonll(0)) {
@@ -1133,7 +1145,8 @@  ofputil_table_vacancy_to_string(enum ofputil_table_vacancy vacancy)
 }
 
 static enum ofperr
-ofp_print_table_mod(struct ds *string, const struct ofp_header *oh)
+ofp_print_table_mod(struct ds *string, const struct ofp_header *oh,
+                  const struct ofputil_table_map *table_map)
 {
     struct ofputil_table_mod pm;
     enum ofperr error;
@@ -1146,7 +1159,8 @@  ofp_print_table_mod(struct ds *string, const struct ofp_header *oh)
     if (pm.table_id == 0xff) {
         ds_put_cstr(string, " table_id: ALL_TABLES");
     } else {
-        ds_put_format(string, " table_id=%"PRIu8, pm.table_id);
+        ds_put_format(string, " table_id=");
+        ofputil_format_table(pm.table_id, table_map, string);
     }
 
     if (pm.miss != OFPUTIL_TABLE_MISS_DEFAULT) {
@@ -1176,9 +1190,11 @@  ofp_print_table_mod(struct ds *string, const struct ofp_header *oh)
 
 /* This function will print the Table description properties. */
 static void
-ofp_print_table_desc(struct ds *string, const struct ofputil_table_desc *td)
+ofp_print_table_desc(struct ds *string, const struct ofputil_table_desc *td,
+                     const struct ofputil_table_map *table_map)
 {
-    ds_put_format(string, "\n  table %"PRIu8, td->table_id);
+    ds_put_format(string, "\n  table ");
+    ofputil_format_table(td->table_id, table_map, string);
     ds_put_cstr(string, ":\n");
     ds_put_format(string, "   eviction=%s eviction_flags=",
                   ofputil_table_eviction_to_string(td->eviction));
@@ -1198,7 +1214,8 @@  ofp_print_table_desc(struct ds *string, const struct ofputil_table_desc *td)
 }
 
 static enum ofperr
-ofp_print_table_status_message(struct ds *string, const struct ofp_header *oh)
+ofp_print_table_status_message(struct ds *string, const struct ofp_header *oh,
+                               const struct ofputil_table_map *table_map)
 {
     struct ofputil_table_status ts;
     enum ofperr error;
@@ -1215,7 +1232,7 @@  ofp_print_table_status_message(struct ds *string, const struct ofp_header *oh)
     }
 
     ds_put_format(string, "\ntable_desc:-");
-    ofp_print_table_desc(string, &ts.desc);
+    ofp_print_table_desc(string, &ts.desc, table_map);
 
     return 0;
 }
@@ -1606,7 +1623,8 @@  ofp_print_hello(struct ds *string, const struct ofp_header *oh)
 
 static enum ofperr
 ofp_print_error_msg(struct ds *string, const struct ofp_header *oh,
-                    const struct ofputil_port_map *port_map)
+                    const struct ofputil_port_map *port_map,
+                    const struct ofputil_table_map *table_map)
 {
     struct ofpbuf payload;
     enum ofperr error;
@@ -1622,7 +1640,7 @@  ofp_print_error_msg(struct ds *string, const struct ofp_header *oh,
     if (error == OFPERR_OFPHFC_INCOMPATIBLE || error == OFPERR_OFPHFC_EPERM) {
         ds_put_printable(string, payload.data, payload.size);
     } else {
-        s = ofp_to_string(payload.data, payload.size, port_map, 1);
+        s = ofp_to_string(payload.data, payload.size, port_map, table_map, 1);
         ds_put_cstr(string, s);
         free(s);
     }
@@ -1676,7 +1694,8 @@  ofp_print_ofpst_desc_reply(struct ds *string, const struct ofp_header *oh)
 
 static enum ofperr
 ofp_print_flow_stats_request(struct ds *string, const struct ofp_header *oh,
-                             const struct ofputil_port_map *port_map)
+                             const struct ofputil_port_map *port_map,
+                             const struct ofputil_table_map *table_map)
 {
     struct ofputil_flow_stats_request fsr;
     enum ofperr error;
@@ -1687,7 +1706,8 @@  ofp_print_flow_stats_request(struct ds *string, const struct ofp_header *oh,
     }
 
     if (fsr.table_id != 0xff) {
-        ds_put_format(string, " table=%"PRIu8, fsr.table_id);
+        ds_put_format(string, " table=");
+        ofputil_format_table(fsr.table_id, table_map, string);
     }
 
     if (fsr.out_port != OFPP_ANY) {
@@ -1707,7 +1727,9 @@  ofp_print_flow_stats_request(struct ds *string, const struct ofp_header *oh,
  * ages, otherwise they are omitted. */
 void
 ofp_print_flow_stats(struct ds *string, const struct ofputil_flow_stats *fs,
-                     const struct ofputil_port_map *port_map, bool show_stats)
+                     const struct ofputil_port_map *port_map,
+                     const struct ofputil_table_map *table_map,
+                     bool show_stats)
 {
     if (show_stats || fs->cookie) {
         ds_put_format(string, "%scookie=%s0x%"PRIx64", ",
@@ -1719,9 +1741,11 @@  ofp_print_flow_stats(struct ds *string, const struct ofputil_flow_stats *fs,
         ds_put_cstr(string, ", ");
     }
 
-    if (show_stats || fs->table_id) {
-        ds_put_format(string, "%stable=%s%"PRIu8", ",
-                      colors.special, colors.end, fs->table_id);
+    if (show_stats || fs->table_id
+        || ofputil_table_map_get_name(table_map, fs->table_id) != NULL) {
+        ds_put_format(string, "%stable=%s", colors.special, colors.end);
+        ofputil_format_table(fs->table_id, table_map, string);
+        ds_put_cstr(string, ", ");
     }
     if (show_stats) {
         ds_put_format(string, "%sn_packets=%s%"PRIu64", ",
@@ -1764,6 +1788,7 @@  ofp_print_flow_stats(struct ds *string, const struct ofputil_flow_stats *fs,
     ds_put_format(string, "%sactions=%s", colors.actions, colors.end);
     struct ofpact_format_params fp = {
         .port_map = port_map,
+        .table_map = table_map,
         .s = string,
     };
     ofpacts_format(fs->ofpacts, fs->ofpacts_len, &fp);
@@ -1771,7 +1796,8 @@  ofp_print_flow_stats(struct ds *string, const struct ofputil_flow_stats *fs,
 
 static enum ofperr
 ofp_print_flow_stats_reply(struct ds *string, const struct ofp_header *oh,
-                           const struct ofputil_port_map *port_map)
+                           const struct ofputil_port_map *port_map,
+                           const struct ofputil_table_map *table_map)
 {
     struct ofpbuf b = ofpbuf_const_initializer(oh, ntohs(oh->length));
     struct ofpbuf ofpacts;
@@ -1786,7 +1812,7 @@  ofp_print_flow_stats_reply(struct ds *string, const struct ofp_header *oh,
             break;
         }
         ds_put_cstr(string, "\n ");
-        ofp_print_flow_stats(string, &fs, port_map, true);
+        ofp_print_flow_stats(string, &fs, port_map, table_map, true);
      }
     ofpbuf_uninit(&ofpacts);
 
@@ -1991,7 +2017,8 @@  ofp_print_ofpst_port_reply(struct ds *string, const struct ofp_header *oh,
 }
 
 static enum ofperr
-ofp_print_table_stats_reply(struct ds *string, const struct ofp_header *oh)
+ofp_print_table_stats_reply(struct ds *string, const struct ofp_header *oh,
+                            const struct ofputil_table_map *table_map)
 {
     struct ofpbuf b = ofpbuf_const_initializer(oh, ntohs(oh->length));
     ofpraw_pull_assert(&b);
@@ -2011,7 +2038,8 @@  ofp_print_table_stats_reply(struct ds *string, const struct ofp_header *oh)
         ds_put_char(string, '\n');
         ofp_print_table_features(string,
                                  &features, i ? &prev_features : NULL,
-                                 &stats, i ? &prev_stats : NULL);
+                                 &stats, i ? &prev_stats : NULL,
+                                 table_map);
         prev_features = features;
         prev_stats = stats;
     }
@@ -2469,7 +2497,8 @@  nx_flow_monitor_flags_to_name(uint32_t bit)
 static enum ofperr
 ofp_print_nxst_flow_monitor_request(struct ds *string,
                                     const struct ofp_header *oh,
-                                    const struct ofputil_port_map *port_map)
+                                    const struct ofputil_port_map *port_map,
+                                    const struct ofputil_table_map *table_map)
 {
     struct ofpbuf b = ofpbuf_const_initializer(oh, ntohs(oh->length));
     for (;;) {
@@ -2491,7 +2520,8 @@  ofp_print_nxst_flow_monitor_request(struct ds *string,
         }
 
         if (request.table_id != 0xff) {
-            ds_put_format(string, " table=%"PRIu8, request.table_id);
+            ds_put_format(string, " table=");
+            ofputil_format_table(request.table_id, table_map, string);
         }
 
         ds_put_char(string, ' ');
@@ -2503,7 +2533,8 @@  ofp_print_nxst_flow_monitor_request(struct ds *string,
 static enum ofperr
 ofp_print_nxst_flow_monitor_reply(struct ds *string,
                                   const struct ofp_header *oh,
-                                  const struct ofputil_port_map *port_map)
+                                  const struct ofputil_port_map *port_map,
+                                  const struct ofputil_table_map *table_map)
 {
     uint64_t ofpacts_stub[1024 / 8];
     struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
@@ -2542,7 +2573,8 @@  ofp_print_nxst_flow_monitor_reply(struct ds *string,
             continue;
         }
 
-        ds_put_format(string, " table=%"PRIu8, update.table_id);
+        ds_put_format(string, " table=");
+        ofputil_format_table(update.table_id, table_map, string);
         if (update.idle_timeout != OFP_FLOW_PERMANENT) {
             ds_put_format(string, " idle_timeout=%"PRIu16,
                           update.idle_timeout);
@@ -2563,6 +2595,7 @@  ofp_print_nxst_flow_monitor_reply(struct ds *string,
             ds_put_cstr(string, "actions=");
             struct ofpact_format_params fp = {
                 .port_map = port_map,
+                .table_map = table_map,
                 .s = string,
             };
             ofpacts_format(update.ofpacts, update.ofpacts_len, &fp);
@@ -2643,7 +2676,8 @@  ofp_print_group(struct ds *s, uint32_t group_id, uint8_t type,
                 const struct ovs_list *p_buckets,
                 const struct ofputil_group_props *props,
                 enum ofp_version ofp_version, bool suppress_type,
-                const struct ofputil_port_map *port_map)
+                const struct ofputil_port_map *port_map,
+                const struct ofputil_table_map *table_map)
 {
     struct ofputil_bucket *bucket;
 
@@ -2698,6 +2732,7 @@  ofp_print_group(struct ds *s, uint32_t group_id, uint8_t type,
         ds_put_cstr(s, "actions=");
         struct ofpact_format_params fp = {
             .port_map = port_map,
+            .table_map = table_map,
             .s = s,
         };
         ofpacts_format(bucket->ofpacts, bucket->ofpacts_len, &fp);
@@ -2720,7 +2755,8 @@  ofp_print_ofpst_group_desc_request(struct ds *string,
 
 static enum ofperr
 ofp_print_group_desc(struct ds *s, const struct ofp_header *oh,
-                     const struct ofputil_port_map *port_map)
+                     const struct ofputil_port_map *port_map,
+                     const struct ofputil_table_map *table_map)
 {
     struct ofpbuf b = ofpbuf_const_initializer(oh, ntohs(oh->length));
     for (;;) {
@@ -2735,7 +2771,7 @@  ofp_print_group_desc(struct ds *s, const struct ofp_header *oh,
         ds_put_char(s, '\n');
         ds_put_char(s, ' ');
         ofp_print_group(s, gd.group_id, gd.type, &gd.buckets, &gd.props,
-                        oh->version, false, port_map);
+                        oh->version, false, port_map, table_map);
         ofputil_uninit_group_desc(&gd);
      }
 }
@@ -2842,7 +2878,8 @@  ofp_print_group_features(struct ds *string, const struct ofp_header *oh)
 static void
 ofp_print_group_mod__(struct ds *s, enum ofp_version ofp_version,
                       const struct ofputil_group_mod *gm,
-                      const struct ofputil_port_map *port_map)
+                      const struct ofputil_port_map *port_map,
+                      const struct ofputil_table_map *table_map)
 {
     bool bucket_command = false;
 
@@ -2887,12 +2924,13 @@  ofp_print_group_mod__(struct ds *s, enum ofp_version ofp_version,
     }
 
     ofp_print_group(s, gm->group_id, gm->type, &gm->buckets, &gm->props,
-                    ofp_version, bucket_command, port_map);
+                    ofp_version, bucket_command, port_map, table_map);
 }
 
 static enum ofperr
 ofp_print_group_mod(struct ds *s, const struct ofp_header *oh,
-                    const struct ofputil_port_map *port_map)
+                    const struct ofputil_port_map *port_map,
+                    const struct ofputil_table_map *table_map)
 {
     struct ofputil_group_mod gm;
     int error;
@@ -2901,7 +2939,7 @@  ofp_print_group_mod(struct ds *s, const struct ofp_header *oh,
     if (error) {
         return error;
     }
-    ofp_print_group_mod__(s, oh->version, &gm, port_map);
+    ofp_print_group_mod__(s, oh->version, &gm, port_map, table_map);
     ofputil_uninit_group_mod(&gm);
     return 0;
 }
@@ -3071,11 +3109,13 @@  ofp_print_table_features(struct ds *s,
                          const struct ofputil_table_features *features,
                          const struct ofputil_table_features *prev_features,
                          const struct ofputil_table_stats *stats,
-                         const struct ofputil_table_stats *prev_stats)
+                         const struct ofputil_table_stats *prev_stats,
+                         const struct ofputil_table_map *table_map)
 {
     int i;
 
-    ds_put_format(s, "  table %"PRIu8, features->table_id);
+    ds_put_format(s, "  table ");
+    ofputil_format_table(features->table_id, table_map, s);
     if (features->name[0]) {
         ds_put_format(s, " (\"%s\")", features->name);
     }
@@ -3170,7 +3210,8 @@  ofp_print_table_features(struct ds *s,
 }
 
 static enum ofperr
-ofp_print_table_features_reply(struct ds *s, const struct ofp_header *oh)
+ofp_print_table_features_reply(struct ds *s, const struct ofp_header *oh,
+                               const struct ofputil_table_map *table_map)
 {
     struct ofpbuf b = ofpbuf_const_initializer(oh, ntohs(oh->length));
 
@@ -3185,13 +3226,15 @@  ofp_print_table_features_reply(struct ds *s, const struct ofp_header *oh)
         }
 
         ds_put_char(s, '\n');
-        ofp_print_table_features(s, &tf, i ? &prev : NULL, NULL, NULL);
+        ofp_print_table_features(s, &tf, i ? &prev : NULL, NULL, NULL,
+                                 table_map);
         prev = tf;
     }
 }
 
 static enum ofperr
-ofp_print_table_desc_reply(struct ds *s, const struct ofp_header *oh)
+ofp_print_table_desc_reply(struct ds *s, const struct ofp_header *oh,
+                           const struct ofputil_table_map *table_map)
 {
     struct ofpbuf b = ofpbuf_const_initializer(oh, ntohs(oh->length));
     for (;;) {
@@ -3202,7 +3245,7 @@  ofp_print_table_desc_reply(struct ds *s, const struct ofp_header *oh)
         if (retval) {
             return retval != EOF ? retval : 0;
         }
-        ofp_print_table_desc(s, &td);
+        ofp_print_table_desc(s, &td, table_map);
     }
 }
 
@@ -3268,7 +3311,9 @@  ofp_print_bundle_ctrl(struct ds *s, const struct ofp_header *oh)
 
 static enum ofperr
 ofp_print_bundle_add(struct ds *s, const struct ofp_header *oh,
-                     const struct ofputil_port_map *port_map, int verbosity)
+                     const struct ofputil_port_map *port_map,
+                     const struct ofputil_table_map *table_map,
+                     int verbosity)
 {
     int error;
     struct ofputil_bundle_add_msg badd;
@@ -3285,7 +3330,7 @@  ofp_print_bundle_add(struct ds *s, const struct ofp_header *oh,
 
     ds_put_char(s, '\n');
     char *msg = ofp_to_string(badd.msg, ntohs(badd.msg->length), port_map,
-                              verbosity);
+                              table_map, verbosity);
     ds_put_and_free_cstr(s, msg);
 
     return 0;
@@ -3376,7 +3421,8 @@  ofp_print_tlv_table_reply(struct ds *s, const struct ofp_header *oh)
  * request forward is taken from rf.request.type */
 static enum ofperr
 ofp_print_requestforward(struct ds *string, const struct ofp_header *oh,
-                         const struct ofputil_port_map *port_map)
+                         const struct ofputil_port_map *port_map,
+                         const struct ofputil_table_map *table_map)
 {
     struct ofputil_requestforward rf;
     enum ofperr error;
@@ -3391,7 +3437,8 @@  ofp_print_requestforward(struct ds *string, const struct ofp_header *oh,
     switch (rf.reason) {
     case OFPRFR_GROUP_MOD:
         ds_put_cstr(string, "group_mod");
-        ofp_print_group_mod__(string, oh->version, rf.group_mod, port_map);
+        ofp_print_group_mod__(string, oh->version, rf.group_mod, port_map,
+                              table_map);
         break;
 
     case OFPRFR_METER_MOD:
@@ -3491,7 +3538,8 @@  ofp_print_nxt_ct_flush_zone(struct ds *string, const struct nx_zone_id *nzi)
 
 static enum ofperr
 ofp_to_string__(const struct ofp_header *oh,
-                const struct ofputil_port_map *port_map, enum ofpraw raw,
+                const struct ofputil_port_map *port_map,
+                const struct ofputil_table_map *table_map, enum ofpraw raw,
                 struct ds *string, int verbosity)
 {
     const void *msg = oh;
@@ -3509,7 +3557,7 @@  ofp_to_string__(const struct ofp_header *oh,
         return ofp_print_ofpst_group_desc_request(string, oh);
 
     case OFPTYPE_GROUP_DESC_STATS_REPLY:
-        return ofp_print_group_desc(string, oh, port_map);
+        return ofp_print_group_desc(string, oh, port_map, table_map);
 
     case OFPTYPE_GROUP_FEATURES_STATS_REQUEST:
         ofp_print_stats(string, oh);
@@ -3519,21 +3567,21 @@  ofp_to_string__(const struct ofp_header *oh,
         return ofp_print_group_features(string, oh);
 
     case OFPTYPE_GROUP_MOD:
-        return ofp_print_group_mod(string, oh, port_map);
+        return ofp_print_group_mod(string, oh, port_map, table_map);
 
     case OFPTYPE_TABLE_FEATURES_STATS_REQUEST:
     case OFPTYPE_TABLE_FEATURES_STATS_REPLY:
-        return ofp_print_table_features_reply(string, oh);
+        return ofp_print_table_features_reply(string, oh, table_map);
 
     case OFPTYPE_TABLE_DESC_REQUEST:
     case OFPTYPE_TABLE_DESC_REPLY:
-        return ofp_print_table_desc_reply(string, oh);
+        return ofp_print_table_desc_reply(string, oh, table_map);
 
     case OFPTYPE_HELLO:
         return ofp_print_hello(string, oh);
 
     case OFPTYPE_ERROR:
-        return ofp_print_error_msg(string, oh, port_map);
+        return ofp_print_error_msg(string, oh, port_map, table_map);
 
     case OFPTYPE_ECHO_REQUEST:
     case OFPTYPE_ECHO_REPLY:
@@ -3555,25 +3603,26 @@  ofp_to_string__(const struct ofp_header *oh,
         return ofp_print_set_config(string, oh);
 
     case OFPTYPE_PACKET_IN:
-        return ofp_print_packet_in(string, oh, port_map, verbosity);
+        return ofp_print_packet_in(string, oh, port_map, table_map, verbosity);
 
     case OFPTYPE_FLOW_REMOVED:
-        return ofp_print_flow_removed(string, oh, port_map);
+        return ofp_print_flow_removed(string, oh, port_map, table_map);
 
     case OFPTYPE_PORT_STATUS:
         return ofp_print_port_status(string, oh);
 
     case OFPTYPE_PACKET_OUT:
-        return ofp_print_packet_out(string, oh, port_map, verbosity);
+        return ofp_print_packet_out(string, oh, port_map, table_map,
+                                    verbosity);
 
     case OFPTYPE_FLOW_MOD:
-        return ofp_print_flow_mod(string, oh, port_map, verbosity);
+        return ofp_print_flow_mod(string, oh, port_map, table_map, verbosity);
 
     case OFPTYPE_PORT_MOD:
         return ofp_print_port_mod(string, oh, port_map);
 
     case OFPTYPE_TABLE_MOD:
-        return ofp_print_table_mod(string, oh);
+        return ofp_print_table_mod(string, oh, table_map);
 
     case OFPTYPE_METER_MOD:
         return ofp_print_meter_mod(string, oh);
@@ -3595,10 +3644,10 @@  ofp_to_string__(const struct ofp_header *oh,
         return ofp_print_role_status_message(string, oh);
 
     case OFPTYPE_REQUESTFORWARD:
-        return ofp_print_requestforward(string, oh, port_map);
+        return ofp_print_requestforward(string, oh, port_map, table_map);
 
     case OFPTYPE_TABLE_STATUS:
-        return ofp_print_table_status_message(string, oh);
+        return ofp_print_table_status_message(string, oh, table_map);
 
     case OFPTYPE_METER_STATS_REQUEST:
     case OFPTYPE_METER_CONFIG_STATS_REQUEST:
@@ -3625,7 +3674,7 @@  ofp_to_string__(const struct ofp_header *oh,
     case OFPTYPE_FLOW_STATS_REQUEST:
     case OFPTYPE_AGGREGATE_STATS_REQUEST:
         ofp_print_stats(string, oh);
-        return ofp_print_flow_stats_request(string, oh, port_map);
+        return ofp_print_flow_stats_request(string, oh, port_map, table_map);
 
     case OFPTYPE_TABLE_STATS_REQUEST:
         ofp_print_stats(string, oh);
@@ -3645,7 +3694,7 @@  ofp_to_string__(const struct ofp_header *oh,
 
     case OFPTYPE_FLOW_STATS_REPLY:
         ofp_print_stats(string, oh);
-        return ofp_print_flow_stats_reply(string, oh, port_map);
+        return ofp_print_flow_stats_reply(string, oh, port_map, table_map);
 
     case OFPTYPE_QUEUE_STATS_REPLY:
         ofp_print_stats(string, oh);
@@ -3657,7 +3706,7 @@  ofp_to_string__(const struct ofp_header *oh,
 
     case OFPTYPE_TABLE_STATS_REPLY:
         ofp_print_stats(string, oh);
-        return ofp_print_table_stats_reply(string, oh);
+        return ofp_print_table_stats_reply(string, oh, table_map);
 
     case OFPTYPE_AGGREGATE_STATS_REPLY:
         ofp_print_stats(string, oh);
@@ -3699,16 +3748,19 @@  ofp_to_string__(const struct ofp_header *oh,
         break;
 
     case OFPTYPE_FLOW_MONITOR_STATS_REQUEST:
-        return ofp_print_nxst_flow_monitor_request(string, msg, port_map);
+        return ofp_print_nxst_flow_monitor_request(string, msg, port_map,
+                                                   table_map);
 
     case OFPTYPE_FLOW_MONITOR_STATS_REPLY:
-        return ofp_print_nxst_flow_monitor_reply(string, msg, port_map);
+        return ofp_print_nxst_flow_monitor_reply(string, msg, port_map,
+                                                 table_map);
 
     case OFPTYPE_BUNDLE_CONTROL:
         return ofp_print_bundle_ctrl(string, msg);
 
     case OFPTYPE_BUNDLE_ADD_MESSAGE:
-        return ofp_print_bundle_add(string, msg, port_map, verbosity);
+        return ofp_print_bundle_add(string, msg, port_map, table_map,
+                                    verbosity);
 
     case OFPTYPE_NXT_TLV_TABLE_MOD:
         return ofp_print_tlv_table_mod(string, msg);
@@ -3720,7 +3772,8 @@  ofp_to_string__(const struct ofp_header *oh,
         return ofp_print_tlv_table_reply(string, msg);
 
     case OFPTYPE_NXT_RESUME:
-        return ofp_print_packet_in(string, msg, port_map, verbosity);
+        return ofp_print_packet_in(string, msg, port_map, table_map,
+                                   verbosity);
     case OFPTYPE_IPFIX_BRIDGE_STATS_REQUEST:
         break;
     case OFPTYPE_IPFIX_BRIDGE_STATS_REPLY:
@@ -3751,7 +3804,9 @@  add_newline(struct ds *s)
  * for freeing the string. */
 char *
 ofp_to_string(const void *oh_, size_t len,
-              const struct ofputil_port_map *port_map, int verbosity)
+              const struct ofputil_port_map *port_map,
+              const struct ofputil_table_map *table_map,
+              int verbosity)
 {
     struct ds string = DS_EMPTY_INITIALIZER;
     const struct ofp_header *oh = oh_;
@@ -3787,7 +3842,8 @@  ofp_to_string(const void *oh_, size_t len,
             ofp_header_to_string__(oh, raw, &string);
             size_t header_len = string.length;
 
-            error = ofp_to_string__(oh, port_map, raw, &string, verbosity);
+            error = ofp_to_string__(oh, port_map, table_map,
+                                    raw, &string, verbosity);
             if (error) {
                 if (string.length > header_len) {
                     ds_chomp(&string, ' ');
@@ -3827,9 +3883,11 @@  print_and_free(FILE *stream, char *string)
  * numbers increase verbosity. */
 void
 ofp_print(FILE *stream, const void *oh, size_t len,
-          const struct ofputil_port_map *port_map, int verbosity)
+          const struct ofputil_port_map *port_map,
+          const struct ofputil_table_map *table_map, int verbosity)
 {
-    print_and_free(stream, ofp_to_string(oh, len, port_map, verbosity));
+    print_and_free(stream, ofp_to_string(oh, len, port_map, table_map,
+                                         verbosity));
 }
 
 /* Dumps the contents of the Ethernet frame in the 'len' bytes starting at
diff --git a/lib/ofp-util.c b/lib/ofp-util.c
index f3b2e3f6108c..09ad93132918 100644
--- a/lib/ofp-util.c
+++ b/lib/ofp-util.c
@@ -7378,16 +7378,16 @@  ofputil_port_get_reserved_name(ofp_port_t port)
     }
 }
 
-/* A port name doesn't need to be quoted if it is alphanumeric and starts with
- * a letter. */
+/* A table or port name doesn't need to be quoted if it is alphanumeric and
+ * starts with a letter. */
 static bool
-port_name_needs_quotes(const char *port_name)
+name_needs_quotes(const char *name)
 {
-    if (!isalpha((unsigned char) port_name[0])) {
+    if (!isalpha((unsigned char) name[0])) {
         return true;
     }
 
-    for (const char *p = port_name + 1; *p; p++) {
+    for (const char *p = name + 1; *p; p++) {
         if (!isalnum((unsigned char) *p)) {
             return true;
         }
@@ -7395,13 +7395,14 @@  port_name_needs_quotes(const char *port_name)
     return false;
 }
 
+/* Appends port or table 'name' to 's', quoting it if necessary. */
 static void
-put_port_name(const char *port_name, struct ds *s)
+put_name(const char *name, struct ds *s)
 {
-    if (port_name_needs_quotes(port_name)) {
-        json_string_escape(port_name, s);
+    if (name_needs_quotes(name)) {
+        json_string_escape(name, s);
     } else {
-        ds_put_cstr(s, port_name);
+        ds_put_cstr(s, name);
     }
 }
 
@@ -7420,7 +7421,7 @@  ofputil_format_port(ofp_port_t port, const struct ofputil_port_map *port_map,
 
     const char *port_name = ofputil_port_map_get_name(port_map, port);
     if (port_name) {
-        put_port_name(port_name, s);
+        put_name(port_name, s);
         return;
     }
 
@@ -7445,7 +7446,7 @@  ofputil_port_to_string(ofp_port_t port,
     const char *port_name = ofputil_port_map_get_name(port_map, port);
     if (port_name) {
         struct ds s = DS_EMPTY_INITIALIZER;
-        put_port_name(port_name, &s);
+        put_name(port_name, &s);
         ovs_strlcpy(namebuf, ds_cstr(&s), bufsize);
         ds_destroy(&s);
         return;
@@ -7644,6 +7645,92 @@  ofputil_table_map_destroy(struct ofputil_table_map *map)
     ofputil_name_map_destroy(&map->map);
 }
 
+/* Table numbers. */
+
+/* Stores the table number represented by 's' into '*tablep'.  's' may be an
+ * integer or, if 'table_map' is nonnull, a name (quoted or unquoted).
+ *
+ * Returns true if successful, false if 's' is not a valid OpenFlow table
+ * number or name.  The caller should issue an error message in this case,
+ * because this function usually does not.  (This gives the caller an
+ * opportunity to look up the table name another way, e.g. by contacting the
+ * switch and listing the names of all its tables). */
+bool
+ofputil_table_from_string(const char *s,
+                          const struct ofputil_table_map *table_map,
+                          uint8_t *tablep)
+{
+    *tablep = 0;
+    if (*s == '-') {
+        VLOG_WARN("Negative value %s is not a valid table number.", s);
+        return false;
+    }
+
+    unsigned int table;
+    if (str_to_uint(s, 10, &table)) {
+        if (table > 255) {
+            VLOG_WARN("table %u is outside the supported range 0 through 255",
+                      table);
+            return false;
+        }
+        *tablep = table;
+        return true;
+    } else {
+        if (s[0] != '"') {
+            table = ofputil_table_map_get_number(table_map, s);
+        } else {
+            size_t length = strlen(s);
+            char *name = NULL;
+            if (length > 1
+                && s[length - 1] == '"'
+                && json_string_unescape(s + 1, length - 2, &name)) {
+                table = ofputil_table_map_get_number(table_map, name);
+            }
+            free(name);
+        }
+        if (table != UINT8_MAX) {
+            *tablep = table;
+            return true;
+        }
+
+        return false;
+    }
+}
+
+/* Appends to 's' a string representation of the OpenFlow table number 'table',
+ * either the table number or a name drawn from 'table_map'. */
+void
+ofputil_format_table(uint8_t table, const struct ofputil_table_map *table_map,
+                     struct ds *s)
+{
+    const char *table_name = ofputil_table_map_get_name(table_map, table);
+    if (table_name) {
+        put_name(table_name, s);
+    } else {
+        ds_put_format(s, "%"PRIu8, table);
+    }
+}
+
+/* Puts in the 'bufsize' byte in 'namebuf' a null-terminated string
+ * representation of OpenFlow table number 'table', either the table's number
+ * or a name drawn from 'table_map'. */
+void
+ofputil_table_to_string(uint8_t table,
+                        const struct ofputil_table_map *table_map,
+                        char *namebuf, size_t bufsize)
+{
+    const char *table_name = ofputil_table_map_get_name(table_map, table);
+    if (table_name) {
+        struct ds s = DS_EMPTY_INITIALIZER;
+        put_name(table_name, &s);
+        ovs_strlcpy(namebuf, ds_cstr(&s), bufsize);
+        ds_destroy(&s);
+        return;
+    }
+
+    snprintf(namebuf, bufsize, "%"PRIu8, table);
+}
+
 /* Stores the group id represented by 's' into '*group_idp'.  's' may be an
  * integer or, for reserved group IDs, the standard OpenFlow name for the group
  * (either "ANY" or "ALL").
diff --git a/lib/vconn.c b/lib/vconn.c
index bb56be2d2901..f0d00eec104f 100644
--- a/lib/vconn.c
+++ b/lib/vconn.c
@@ -496,7 +496,7 @@  vcs_recv_hello(struct vconn *vconn)
             ofpbuf_delete(b);
             return;
         } else {
-            char *s = ofp_to_string(b->data, b->size, NULL, 1);
+            char *s = ofp_to_string(b->data, b->size, NULL, NULL, 1);
             VLOG_WARN_RL(&bad_ofmsg_rl,
                          "%s: received message while expecting hello: %s",
                          vconn->name, s);
@@ -642,7 +642,8 @@  do_recv(struct vconn *vconn, struct ofpbuf **msgp)
     if (!retval) {
         COVERAGE_INC(vconn_received);
         if (VLOG_IS_DBG_ENABLED()) {
-            char *s = ofp_to_string((*msgp)->data, (*msgp)->size, NULL, 1);
+            char *s = ofp_to_string((*msgp)->data, (*msgp)->size,
+                                    NULL, NULL, 1);
             VLOG_DBG_RL(&ofmsg_rl, "%s: received: %s", vconn->name, s);
             free(s);
         }
@@ -682,7 +683,7 @@  do_send(struct vconn *vconn, struct ofpbuf *msg)
         COVERAGE_INC(vconn_sent);
         retval = (vconn->vclass->send)(vconn, msg);
     } else {
-        char *s = ofp_to_string(msg->data, msg->size, NULL, 1);
+        char *s = ofp_to_string(msg->data, msg->size, NULL, NULL, 1);
         retval = (vconn->vclass->send)(vconn, msg);
         if (retval != EAGAIN) {
             VLOG_DBG_RL(&ofmsg_rl, "%s: sent (%s): %s",
@@ -958,7 +959,8 @@  recv_flow_stats_reply(struct vconn *vconn, ovs_be32 send_xid,
             error = ofptype_decode(&type, reply->data);
             if (error || type != OFPTYPE_FLOW_STATS_REPLY) {
                 VLOG_WARN_RL(&rl, "received bad reply: %s",
-                             ofp_to_string(reply->data, reply->size, NULL, 1));
+                             ofp_to_string(reply->data, reply->size,
+                                           NULL, NULL, 1));
                 return EPROTO;
             }
         }
diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c
index 6157b70aee60..c7427846c047 100644
--- a/ofproto/ofproto-dpif.c
+++ b/ofproto/ofproto-dpif.c
@@ -1700,11 +1700,9 @@  flush(struct ofproto *ofproto_)
 
 static void
 query_tables(struct ofproto *ofproto,
-             struct ofputil_table_features *features,
+             struct ofputil_table_features *features OVS_UNUSED,
              struct ofputil_table_stats *stats)
 {
-    strcpy(features->name, "classifier");
-
     if (stats) {
         int i;
 
diff --git a/ofproto/ofproto.c b/ofproto/ofproto.c
index 71f449f7ddda..6d3e12976408 100644
--- a/ofproto/ofproto.c
+++ b/ofproto/ofproto.c
@@ -3225,7 +3225,7 @@  query_tables(struct ofproto *ofproto,
         struct ofputil_table_features *f = &features[i];
 
         f->table_id = i;
-        sprintf(f->name, "table%d", i);
+        f->name[0] = '\0';
         f->metadata_match = OVS_BE64_MAX;
         f->metadata_write = OVS_BE64_MAX;
         atomic_read_relaxed(&ofproto->tables[i].miss_config, &f->miss_config);
diff --git a/ovn/controller/ofctrl.c b/ovn/controller/ofctrl.c
index 264667769296..26cf186dcae8 100644
--- a/ovn/controller/ofctrl.c
+++ b/ovn/controller/ofctrl.c
@@ -286,7 +286,7 @@  recv_S_TLV_TABLE_REQUESTED(const struct ofp_header *oh, enum ofptype type,
         VLOG_ERR("switch refused to allocate Geneve option (%s)",
                  ofperr_to_string(ofperr_decode_msg(oh, NULL)));
     } else {
-        char *s = ofp_to_string(oh, ntohs(oh->length), NULL, 1);
+        char *s = ofp_to_string(oh, ntohs(oh->length), NULL, NULL, 1);
         VLOG_ERR("unexpected reply to TLV table request (%s)", s);
         free(s);
     }
@@ -340,7 +340,7 @@  recv_S_TLV_TABLE_MOD_SENT(const struct ofp_header *oh, enum ofptype type,
             goto error;
         }
     } else {
-        char *s = ofp_to_string(oh, ntohs(oh->length), NULL, 1);
+        char *s = ofp_to_string(oh, ntohs(oh->length), NULL, NULL, 1);
         VLOG_ERR("unexpected reply to Geneve option allocation request (%s)",
                  s);
         free(s);
@@ -513,7 +513,7 @@  ofctrl_run(const struct ovsrec_bridge *br_int, struct shash *pending_ct_zones)
                     OVS_NOT_REACHED();
                 }
             } else {
-                char *s = ofp_to_string(oh, ntohs(oh->length), NULL, 1);
+                char *s = ofp_to_string(oh, ntohs(oh->length), NULL, NULL, 1);
                 VLOG_WARN("could not decode OpenFlow message (%s): %s",
                           ofperr_to_string(error), s);
                 free(s);
@@ -573,7 +573,7 @@  log_openflow_rl(struct vlog_rate_limit *rl, enum vlog_level level,
                 const struct ofp_header *oh, const char *title)
 {
     if (!vlog_should_drop(&this_module, level, rl)) {
-        char *s = ofp_to_string(oh, ntohs(oh->length), NULL, 2);
+        char *s = ofp_to_string(oh, ntohs(oh->length), NULL, NULL, 2);
         vlog(&this_module, level, "%s: %s", title, s);
         free(s);
     }
@@ -878,7 +878,7 @@  ofctrl_put(struct hmap *flow_table, struct shash *pending_ct_zones,
 
             error = parse_ofp_group_mod_str(&gm, OFPGC11_ADD,
                                             ds_cstr(&group_string), NULL,
-                                            &usable_protocols);
+                                            NULL, &usable_protocols);
             if (!error) {
                 add_group_mod(&gm, &msgs);
             } else {
@@ -978,7 +978,7 @@  ofctrl_put(struct hmap *flow_table, struct shash *pending_ct_zones,
 
             error = parse_ofp_group_mod_str(&gm, OFPGC11_DELETE,
                                             ds_cstr(&group_string), NULL,
-                                            &usable_protocols);
+                                            NULL, &usable_protocols);
             if (!error) {
                 add_group_mod(&gm, &msgs);
             } else {
diff --git a/ovn/controller/pinctrl.c b/ovn/controller/pinctrl.c
index 7542db3f4854..27bcf3317f99 100644
--- a/ovn/controller/pinctrl.c
+++ b/ovn/controller/pinctrl.c
@@ -1039,7 +1039,7 @@  pinctrl_recv(const struct ofp_header *oh, enum ofptype type,
         if (VLOG_IS_DBG_ENABLED()) {
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(30, 300);
 
-            char *s = ofp_to_string(oh, ntohs(oh->length), NULL, 2);
+            char *s = ofp_to_string(oh, ntohs(oh->length), NULL, NULL, 2);
 
             VLOG_DBG_RL(&rl, "OpenFlow packet ignored: %s", s);
             free(s);
diff --git a/ovn/utilities/ovn-sbctl.c b/ovn/utilities/ovn-sbctl.c
index 0cb8ea480438..dc56d864c986 100644
--- a/ovn/utilities/ovn-sbctl.c
+++ b/ovn/utilities/ovn-sbctl.c
@@ -796,7 +796,7 @@  sbctl_dump_openflow(struct vconn *vconn, const struct uuid *uuid, bool stats)
 
             ds_clear(&s);
             if (stats) {
-                ofp_print_flow_stats(&s, fs, NULL, true);
+                ofp_print_flow_stats(&s, fs, NULL, NULL, true);
             } else {
                 ds_put_format(&s, "%stable=%s%"PRIu8" ",
                               colors.special, colors.end, fs->table_id);
diff --git a/ovn/utilities/ovn-trace.c b/ovn/utilities/ovn-trace.c
index 7ff4a2682557..279303be0c6a 100644
--- a/ovn/utilities/ovn-trace.c
+++ b/ovn/utilities/ovn-trace.c
@@ -1932,7 +1932,7 @@  trace_openflow(const struct ovntrace_flow *f, struct ovs_list *super)
         struct ds s = DS_EMPTY_INITIALIZER;
         for (size_t i = 0; i < n_fses; i++) {
             ds_clear(&s);
-            ofp_print_flow_stats(&s, &fses[i], NULL, true);
+            ofp_print_flow_stats(&s, &fses[i], NULL, NULL, true);
             ovntrace_node_append(super, OVNTRACE_NODE_ACTION,
                                  "%s", ds_cstr(&s));
         }
diff --git a/tests/ofproto.at b/tests/ofproto.at
index b49f546afa67..1b0ba94fd837 100644
--- a/tests/ofproto.at
+++ b/tests/ofproto.at
@@ -2174,7 +2174,7 @@  OVS_VSWITCHD_START
 # Check the default configuration.
 head_table() {
     printf 'OFPST_TABLE reply (xid=0x2):
-  table 0 ("%s"):
+  table 0%s:
     active=0, lookup=0, matched=0
     max_entries=1000000
     matching:
@@ -2191,14 +2191,14 @@  head_table() {
       tcp_src: exact match or wildcard
       tcp_dst: exact match or wildcard
 
-' $1
+' "$1"
 }
 ditto() {
     for i in `seq $1 $2`; do
-        printf '  table %d ("table%d"): ditto\n' $i $i
+        printf '  table %d: ditto\n' $i
     done
 }
-(head_table classifier; ditto 1 253) > expout
+(head_table; ditto 1 253) > expout
 AT_CHECK([ovs-ofctl dump-tables br0], [0], [expout])
 # Change the configuration.
 AT_CHECK(
@@ -2211,12 +2211,12 @@  AT_CHECK(
 <1>
 ])
 # Check that the configuration was updated.
-(head_table main; echo '  table 1 ("table1"):
+(head_table ' ("main")'; echo '  table 1:
     active=0, lookup=0, matched=0
     max_entries=1024
     (same matching)
 
-  table 2 ("table2"):
+  table 2:
     active=0, lookup=0, matched=0
     max_entries=1000000
     (same matching)
@@ -2250,7 +2250,7 @@  AT_CHECK([test `grep '240\.0\.0\.1' stdout | grep -v table_id= | wc -l` -gt 0])
 # Check that dump-tables doesn't count the hidden flows.
 head_table() {
     printf 'OFPST_TABLE reply:
-  table 0 ("%s"):
+  table 0:
     active=0, lookup=0, matched=0
     max_entries=1000000
     matching:
@@ -2267,14 +2267,14 @@  head_table() {
       tcp_src: exact match or wildcard
       tcp_dst: exact match or wildcard
 
-' $1
+'
 }
 ditto() {
     for i in `seq $1 $2`; do
-        printf '  table %d ("table%d"): ditto\n' $i $i
+        printf '  table %d: ditto\n' $i
     done
 }
-(head_table classifier; ditto 1 253) > expout
+(head_table; ditto 1 253) > expout
 AT_CHECK([ovs-ofctl dump-tables br0 | strip_xids], [0], [expout])
 OVS_VSWITCHD_STOP(["/240\.0\.0\.1/d"])
 AT_CLEANUP
@@ -2284,7 +2284,7 @@  OVS_VSWITCHD_START
 # Check the default configuration.
 head_table() {
     printf 'OFPST_TABLE reply (OF1.2) (xid=0x2):
-  table 0 ("%s"):
+  table 0%s:
     active=0, lookup=0, matched=0
     metadata: match=0xffffffffffffffff write=0xffffffffffffffff
     config=controller
@@ -2331,15 +2331,15 @@  head_table() {
       nd_sll: exact match or wildcard
       nd_tll: exact match or wildcard
 
-' $1
+' "$1"
 }
 ditto() {
     for i in `seq $1 $2`; do
-        printf '  table %d ("table%d"): ditto\n' $i $i
+        printf '  table %d: ditto\n' $i
     done
 }
 tail_table() {
-    printf '  table 253 ("table253"):
+    printf '  table 253:
     active=0, lookup=0, matched=0
     metadata: match=0xffffffffffffffff write=0xffffffffffffffff
     config=controller
@@ -2350,7 +2350,7 @@  tail_table() {
     (same matching)
 '
 }
-(head_table classifier; ditto 1 252; tail_table) > expout
+(head_table; ditto 1 252; tail_table) > expout
 AT_CHECK([ovs-ofctl -O OpenFlow12 dump-tables br0], [0], [expout])
 # Change the configuration.
 AT_CHECK(
@@ -2363,7 +2363,7 @@  AT_CHECK(
 <1>
 ])
 # Check that the configuration was updated.
-(head_table main; echo '  table 1 ("table1"):
+(head_table ' ("main")'; echo '  table 1:
     active=0, lookup=0, matched=0
     metadata: match=0xffffffffffffffff write=0xffffffffffffffff
     config=controller
@@ -2371,7 +2371,7 @@  AT_CHECK(
     (same instructions)
     (same matching)
 
-  table 2 ("table2"):
+  table 2:
     active=0, lookup=0, matched=0
     metadata: match=0xffffffffffffffff write=0xffffffffffffffff
     config=controller
@@ -2386,7 +2386,7 @@  AT_CLEANUP
 AT_SETUP([ofproto - table features (OpenFlow 1.3)])
 OVS_VSWITCHD_START
 head_table () {
-    printf '  table 0 ("%s"):
+    printf '  table 0%s:
     metadata: match=0xffffffffffffffff write=0xffffffffffffffff
     max_entries=1000000
     instructions (table miss and others):
@@ -2569,10 +2569,10 @@  metadata in_port in_port_oxm pkt_mark ct_mark ct_label reg0 reg1 reg2 reg3 reg4
       nsh_c4: arbitrary mask
       nsh_ttl: exact match or wildcard
 
-' $1
+' "$1"
 }
 ditto() {
-    printf '  table %d ("%s"):
+    printf '  table %d:
     metadata: match=0xffffffffffffffff write=0xffffffffffffffff
     max_entries=%d
     instructions (table miss and others):
@@ -2581,10 +2581,10 @@  ditto() {
       (same actions)
     (same matching)
 
-' $1 $2 $3 `expr $1 + 1`
+' $1 $2 `expr $1 + 1`
 }
 tail_tables() {
-echo '  table 252 ("table252"):
+echo '  table 252:
     metadata: match=0xffffffffffffffff write=0xffffffffffffffff
     max_entries=1000000
     instructions (table miss and others):
@@ -2593,7 +2593,7 @@  echo '  table 252 ("table252"):
       (same actions)
     (same matching)
 
-  table 253 ("table253"):
+  table 253:
     metadata: match=0xffffffffffffffff write=0xffffffffffffffff
     max_entries=1000000
     instructions (table miss and others):
@@ -2602,9 +2602,9 @@  echo '  table 252 ("table252"):
     (same matching)
 '
 }
-(head_table classifier
+(head_table
  for i in `seq 1 251`; do
-     ditto $i table$i 1000000
+     ditto $i 1000000
  done
  tail_tables) > expout
 AT_CHECK([ovs-ofctl -O OpenFlow13 dump-table-features br0], [0], [expout])
@@ -2619,16 +2619,49 @@  AT_CHECK(
 <1>
 ])
 # Check that the configuration was updated.
-(head_table main
- ditto 1 table1 1024
+(head_table ' ("main")'
+ ditto 1 1024
  for i in `seq 2 251`; do
-     ditto $i table$i 1000000
+     ditto $i 1000000
  done
  tail_tables) > expout
 AT_CHECK([ovs-ofctl -O OpenFlow13 dump-table-features br0], [0], [expout])
 OVS_VSWITCHD_STOP
 AT_CLEANUP
 
+AT_SETUP([ofproto - flow table names])
+OVS_VSWITCHD_START
+add_of_ports br0 1 2
+AT_CHECK(
+  [ovs-vsctl \
+     -- --id=@t0 create Flow_Table name=zero \
+     -- --id=@t1 create Flow_Table name=one \
+     -- --id=@t2 create Flow_Table name=two \
+     -- set bridge br0 'flow_tables={0=@t0,1=@t1,2=@t2}' \
+   | uuidfilt],
+  [0], [<0>
+<1>
+<2>
+])
+AT_DATA([flows.txt], [dnl
+table=zero in_port=p2 actions=p1,resubmit(,one)
+table=one,in_port=p1,ip,actions=ct(table=two)
+table=one,in_port=p1,arp,actions=goto_table(two)
+])
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+AT_CHECK([ovs-ofctl --names --no-stats dump-flows br0], [0], [dnl
+ table=zero, in_port=p2 actions=output:p1,resubmit(,one)
+ table=one, ip,in_port=p1 actions=ct(table=two)
+ table=one, arp,in_port=p1 actions=resubmit(,two)
+])
+AT_CHECK([ovs-ofctl --no-names --no-stats dump-flows br0], [0], [dnl
+ in_port=2 actions=output:1,resubmit(,1)
+ table=1, ip,in_port=1 actions=ct(table=2)
+ table=1, arp,in_port=1 actions=resubmit(,2)
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
 AT_SETUP([ofproto - table description (OpenFlow 1.4)])
 OVS_VSWITCHD_START
 (x=0
diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in
index 95344c7e3872..7649bc5c4c50 100644
--- a/utilities/ovs-ofctl.8.in
+++ b/utilities/ovs-ofctl.8.in
@@ -66,9 +66,12 @@  Prints to the console features for each of the flow tables used by
 \fBdump\-table\-desc \fIswitch\fR
 Prints to the console configuration for each of the flow tables used
 by \fIswitch\fR for OpenFlow 1.4+.
-.IP "\fBmod\-table \fIswitch\fR \fItable_id\fR \fIsetting\fR"
-This command configures flow table settings for OpenFlow table
-\fItable_id\fR within \fIswitch\fR.  The available settings depend on
+.IP "\fBmod\-table \fIswitch\fR \fItable\fR \fIsetting\fR"
+This command configures flow table settings in \fIswitch\fR for
+OpenFlow table \fItable\fR, which may be expressed as a number or
+(unless \fB\-\-no\-names\fR is specified) a name.
+.IP
+The available settings depend on
 the OpenFlow version in use.  In OpenFlow 1.1 and 1.2 (which must be
 enabled with the \fB\-O\fR option) only, \fBmod\-table\fR configures
 behavior when no flow is found when a packet is looked up in a flow
@@ -606,9 +609,11 @@  connection to the switch.  (These could only occur using the
 COMMANDS\fR.)
 .IP "\fB!actions\fR"
 Do not report actions as part of flow updates.
-.IP "\fBtable=\fInumber\fR"
-Limits the monitoring to the table with the given \fInumber\fR between
-0 and 254.  By default, all tables are monitored.
+.IP "\fBtable=\fItable\fR"
+Limits the monitoring to the table with the given \fItable\fR, which
+may be expressed as a number between 0 and 254 or (unless
+\fB\-\-no\-names\fR is specified) a name.  By default, all tables are
+monitored.
 .IP "\fBout_port=\fIport\fR"
 If set, only flows that output to \fIport\fR are monitored.  The
 \fIport\fR may be an OpenFlow port number or keyword
@@ -699,10 +704,11 @@  flows not in normal form.
 them.  In addition to match fields, commands that operate on flows
 accept a few additional key-value pairs:
 .
-.IP \fBtable=\fInumber\fR
-For flow dump commands, limits the flows dumped to those in the table
-with the given \fInumber\fR between 0 and 254.  If not specified (or if
-255 is specified as \fInumber\fR), then flows in all tables are
+.IP \fBtable=\fItable\fR
+For flow dump commands, limits the flows dumped to those in
+\fItable\fR, which may be expressed as a number between 0 and 255 or
+(unless \fB\-\-no\-names\fR is specified) a name.  If not specified
+(or if 255 is specified as \fItable\fR), then flows in all tables are
 dumped.
 .
 .IP
@@ -970,8 +976,8 @@  only known to be implemented by Open vSwitch:
 .IP \fBresubmit\fB:\fIport\fR
 .IQ \fBresubmit\fB(\fR[\fIport\fR]\fB,\fR[\fItable\fR]\fB)
 .IQ \fBresubmit\fB(\fR[\fIport\fR]\fB,\fR[\fItable\fR]\fB,ct)
-Re-searches this OpenFlow flow table (or the table whose number is
-specified by \fItable\fR) with the \fBin_port\fR field replaced by
+Re-searches this OpenFlow flow table (or table \fItable\fR, if
+specified) with the \fBin_port\fR field replaced by
 \fIport\fR (if \fIport\fR is specified) and the packet 5-tuple fields
 swapped with the corresponding conntrack original direction tuple
 fields (if \fBct\fR is specified, see \fBct_nw_src\fR above), and
@@ -980,6 +986,9 @@  in this flow entry.  The \fBin_port\fR and swapped 5-tuple fields are
 restored immediately after the search, before any actions are
 executed.
 .IP
+The \fItable\fR may be expressed as a number between 0 and 254 or
+(unless \fB\-\-no\-names\fR is specified) a name.
+.IP
 The \fBct\fR option requires a valid connection tracking state as a
 match prerequisite in the flow where this action is placed.  Examples
 of valid connection tracking state matches include
@@ -1057,12 +1066,14 @@  existing connection and start a new one in the current direction.
 This flag has no effect if the original direction of the connection is
 already the same as that of the current packet.
 .RE
-.IP \fBtable=\fInumber\fR
+.IP \fBtable=\fItable\fR
 Fork pipeline processing in two. The original instance of the packet will
 continue processing the current actions list as an untracked packet. An
 additional instance of the packet will be sent to the connection tracker, which
 will be re-injected into the OpenFlow pipeline to resume processing in table
-\fInumber\fR, with the \fBct_state\fR and other ct match fields set. If the
+\fInumber\fR (which may be specified as a number between 0 and 254 or,
+unless \fB\-\-no\-names\fR is specified, a name), with the
+\fBct_state\fR and other ct match fields set. If
 \fBtable\fR is not specified, then the packet which is submitted to the
 connection tracker is not re-injected into the OpenFlow pipeline. It is
 strongly recommended to specify a table later than the current table to prevent
@@ -1472,10 +1483,10 @@  flow syntax.
 Adds a \fBfin_timeout\fR action with the specified arguments to the
 new flow.  This feature was added in Open vSwitch 1.5.90.
 .
-.IP \fBtable=\fInumber\fR
+.IP \fBtable=\fItable\fR
 The table in which the new flow should be inserted.  Specify a decimal
-number between 0 and 254.  The default, if \fBtable\fR is unspecified,
-is table 1.
+number between 0 and 254 or (unless \fB\-\-no\-names\fR is specified)
+a name.  The default, if \fBtable\fR is unspecified, is table 1.
 .
 .IP \fBdelete_learned\fR
 This flag enables deletion of the learned flows when the flow with the
@@ -1547,12 +1558,6 @@  Add an \fBoutput\fR action to the new flow's actions, that outputs to
 the OpenFlow port taken from \fIfield\fB[\fIstart\fB..\fIend\fB]\fR,
 which must be an NXM field as described above.
 .RE
-.IP
-For best performance, segregate learned flows into a table (using
-\fBtable=\fInumber\fR) that is not used for any other flows except
-possibly for a lowest-priority ``catch-all'' flow, that is, a flow
-with no match criteria.  (This is why the default \fBtable\fR is 1, to
-keep the learned flows separate from the primary flow table 0.)
 .RE
 .
 .RS
@@ -1675,7 +1680,9 @@  band type. See the description of the \fBMeter Table Commands\fR, above,
 for more details.
 .
 .IP \fBgoto_table\fR:\fItable\fR
-Indicates the next table in the process pipeline.
+Jumps to \fItable\Fr as the next table in the process pipeline.  The
+\fItable\fR may be a number between 0 and 254 or (unless
+\fB\-\-no\-names\fR is specified) a name.
 .
 .IP "\fBfin_timeout(\fIargument\fR[\fB,\fIargument\fR]\fB)"
 This action changes the idle timeout or hard timeout, or both, of this
@@ -2286,21 +2293,24 @@  Uses strict matching when running flow modification commands.
 .
 .IP "\fB\-\-names\fR"
 .IQ "\fB\-\-no\-names\fR"
-Every OpenFlow port has a name and a number.  By default,
-\fBovs\-ofctl\fR commands accept both port names and numbers, and they
-display port names if \fBovs\-ofctl\fR is running on an interactive
-console, port numbers otherwise.  With \fB\-\-names\fR,
-\fBovs\-ofctl\fR commands both accept and display port names; with
-\fB\-\-no\-names\fR, commands neither accept nor display port names.
-.IP
-If a port name contains special characters or might be confused with a
-keyword within a flow, it may be enclosed in double quotes (escaped
-from the shell).  If necessary, JSON-style escape sequences may be
-used inside quotes, as specified in RFC 7159.  When it displays port
-names, \fBovs\-ofctl\fR quotes any name that does not start with a
-letter followed by letters or digits.
-.IP
-These options are new in Open vSwitch 2.8.  Earlier versions always
+Every OpenFlow port has a name and a number, and every OpenFlow flow
+table has a number and sometimes a name.  By default, \fBovs\-ofctl\fR
+commands accept both port and table names and numbers, and they
+display port and table names if \fBovs\-ofctl\fR is running on an
+interactive console, numbers otherwise.  With \fB\-\-names\fR,
+\fBovs\-ofctl\fR commands both accept and display port and table
+names; with \fB\-\-no\-names\fR, commands neither accept nor display
+port and table names.
+.IP
+If a port or table name contains special characters or might be
+confused with a keyword within a flow, it may be enclosed in double
+quotes (escaped from the shell).  If necessary, JSON-style escape
+sequences may be used inside quotes, as specified in RFC 7159.  When
+it displays port and table names, \fBovs\-ofctl\fR quotes any name
+that does not start with a letter followed by letters or digits.
+.IP
+Open vSwitch added support for port names and these options.  Open
+vSwitch 2.10 added support for table names.  Earlier versions always
 behaved as if \fB\-\-no\-names\fR were specified.
 .IP
 Open vSwitch does not place its own limit on the length of port names,
@@ -2312,6 +2322,8 @@  Truncation can also cause long names that are different to appear to
 be the same; when a switch has two ports with the same (truncated)
 name, \fBovs\-ofctl\fR refuses to display or accept the name, using
 the number instead.
+.IP
+OpenFlow and Open vSwitch limit table names to 32 bytes.
 .
 .IP "\fB\-\-stats\fR"
 .IQ "\fB\-\-no\-stats\fR"
diff --git a/utilities/ovs-ofctl.c b/utilities/ovs-ofctl.c
index 56af5a444d95..4f7cdf6d5eee 100644
--- a/utilities/ovs-ofctl.c
+++ b/utilities/ovs-ofctl.c
@@ -125,15 +125,18 @@  struct sort_criterion {
 static struct sort_criterion *criteria;
 static size_t n_criteria, allocated_criteria;
 
-/* --names, --no-names: Show port names in output and accept port numbers in
- * input.  (When neither is specified, the default is to accept port numbers
- * but, for backward compatibility, not to show them unless this is an
- * interactive console session.)  */
-static int use_port_names = -1;
+/* --names, --no-names: Show port and table names in output and accept them in
+ * input.  (When neither is specified, the default is to accept port names but,
+ * for backward compatibility, not to show them unless this is an interactive
+ * console session.)  */
+static int use_names = -1;
 static const struct ofputil_port_map *ports_to_accept(const char *vconn_name);
 static const struct ofputil_port_map *ports_to_show(const char *vconn_name);
-static bool should_accept_ports(void);
-static bool should_show_ports(void);
+static const struct ofputil_table_map *tables_to_accept(
+    const char *vconn_name);
+static const struct ofputil_table_map *tables_to_show(const char *vconn_name);
+static bool should_accept_names(void);
+static bool should_show_names(void);
 
 /* --stats, --no-stats: Show statistics in flow dumps? */
 static int show_stats = 1;
@@ -213,8 +216,8 @@  parse_options(int argc, char *argv[])
         {"timestamp", no_argument, NULL, OPT_TIMESTAMP},
         {"sort", optional_argument, NULL, OPT_SORT},
         {"rsort", optional_argument, NULL, OPT_RSORT},
-        {"names", no_argument, &use_port_names, 1},
-        {"no-names", no_argument, &use_port_names, 0},
+        {"names", no_argument, &use_names, 1},
+        {"no-names", no_argument, &use_names, 0},
         {"stats", no_argument, &show_stats, 1},
         {"no-stats", no_argument, &show_stats, 0},
         {"unixctl",     required_argument, NULL, OPT_UNIXCTL},
@@ -636,7 +639,9 @@  dump_transaction(struct vconn *vconn, struct ofpbuf *request)
                 enum ofpraw raw;
 
                 ofp_print(stdout, reply->data, reply->size,
-                          ports_to_show(vconn_get_name(vconn)), verbosity + 1);
+                          ports_to_show(vconn_get_name(vconn)),
+                          tables_to_show(vconn_get_name(vconn)),
+                          verbosity + 1);
 
                 ofpraw_decode(&raw, reply->data);
                 if (ofptype_from_ofpraw(raw) == OFPTYPE_ERROR) {
@@ -648,6 +653,7 @@  dump_transaction(struct vconn *vconn, struct ofpbuf *request)
                               ofp_to_string(
                                   reply->data, reply->size,
                                   ports_to_show(vconn_get_name(vconn)),
+                                  tables_to_show(vconn_get_name(vconn)),
                                   verbosity + 1));
                 }
             } else {
@@ -662,7 +668,9 @@  dump_transaction(struct vconn *vconn, struct ofpbuf *request)
         run(vconn_transact(vconn, request, &reply), "talking to %s",
             vconn_get_name(vconn));
         ofp_print(stdout, reply->data, reply->size,
-                  ports_to_show(vconn_get_name(vconn)), verbosity + 1);
+                  ports_to_show(vconn_get_name(vconn)),
+                  tables_to_show(vconn_get_name(vconn)),
+                  verbosity + 1);
         ofpbuf_delete(reply);
     }
 }
@@ -693,7 +701,9 @@  transact_multiple_noreply(struct vconn *vconn, struct ovs_list *requests)
         "talking to %s", vconn_get_name(vconn));
     if (reply) {
         ofp_print(stderr, reply->data, reply->size,
-                  ports_to_show(vconn_get_name(vconn)), verbosity + 2);
+                  ports_to_show(vconn_get_name(vconn)),
+                  tables_to_show(vconn_get_name(vconn)),
+                  verbosity + 2);
         exit(1);
     }
     ofpbuf_delete(reply);
@@ -739,7 +749,7 @@  bundle_print_errors(struct ovs_list *errors, struct ovs_list *requests,
             }
             fprintf(stderr, "Error %s for: ", ofperr_get_name(ofperr));
             ofp_print(stderr, ofp_msg, msg_len, ports_to_show(vconn_name),
-                      verbosity + 1);
+                      tables_to_show(vconn_name), verbosity + 1);
         }
         ofpbuf_uninit(&payload);
         ofpbuf_delete(error);
@@ -819,7 +829,7 @@  ofctl_show(struct ovs_cmdl_context *ctx)
     run(vconn_transact(vconn, request, &reply), "talking to %s", vconn_name);
 
     has_ports = ofputil_switch_features_has_ports(reply);
-    ofp_print(stdout, reply->data, reply->size, NULL, verbosity + 1);
+    ofp_print(stdout, reply->data, reply->size, NULL, NULL, verbosity + 1);
     ofpbuf_delete(reply);
 
     if (!has_ports) {
@@ -880,7 +890,7 @@  ofctl_dump_table_features(struct ovs_cmdl_context *ctx)
             if (error) {
                 ovs_fatal(0, "decode error: %s", ofperr_get_name(error));
             } else if (type == OFPTYPE_ERROR) {
-                ofp_print(stdout, reply->data, reply->size, NULL,
+                ofp_print(stdout, reply->data, reply->size, NULL, NULL,
                           verbosity + 1);
                 done = true;
             } else if (type == OFPTYPE_TABLE_FEATURES_STATS_REPLY) {
@@ -900,7 +910,8 @@  ofctl_dump_table_features(struct ovs_cmdl_context *ctx)
 
                     struct ds s = DS_EMPTY_INITIALIZER;
                     ofp_print_table_features(&s, &tf, n ? &prev : NULL,
-                                             NULL, NULL);
+                                             NULL, NULL,
+                                             tables_to_show(ctx->argv[1]));
                     puts(ds_cstr(&s));
                     ds_destroy(&s);
 
@@ -911,6 +922,7 @@  ofctl_dump_table_features(struct ovs_cmdl_context *ctx)
                 ovs_fatal(0, "received bad reply: %s",
                           ofp_to_string(reply->data, reply->size,
                                         ports_to_show(ctx->argv[1]),
+                                        tables_to_show(ctx->argv[1]),
                                         verbosity + 1));
             }
         } else {
@@ -1040,7 +1052,7 @@  port_iterator_next(struct port_iterator *pi, struct ofputil_phy_port *pp)
             } else if (retval != EOF) {
                 ovs_fatal(0, "received bad reply: %s",
                           ofp_to_string(pi->reply->data, pi->reply->size,
-                                        NULL, verbosity + 1));
+                                        NULL, NULL, verbosity + 1));
             }
         }
 
@@ -1062,7 +1074,7 @@  port_iterator_next(struct port_iterator *pi, struct ofputil_phy_port *pp)
             || type != OFPTYPE_PORT_DESC_STATS_REPLY) {
             ovs_fatal(0, "received bad reply: %s",
                       ofp_to_string(pi->reply->data, pi->reply->size, NULL,
-                                    verbosity + 1));
+                                    NULL, verbosity + 1));
         }
 
         pi->more = (ofpmp_flags(oh) & OFPSF_REPLY_MORE) != 0;
@@ -1159,33 +1171,189 @@  get_port_map(const char *vconn_name)
 static const struct ofputil_port_map *
 ports_to_accept(const char *vconn_name)
 {
-    return should_accept_ports() ? get_port_map(vconn_name) : NULL;
+    return should_accept_names() ? get_port_map(vconn_name) : NULL;
 }
 
 static const struct ofputil_port_map *
 ports_to_show(const char *vconn_name)
 {
-    return should_show_ports() ? get_port_map(vconn_name) : NULL;
+    return should_show_names() ? get_port_map(vconn_name) : NULL;
+}
+
+struct table_iterator {
+    struct vconn *vconn;
+
+    enum { TI_STATS, TI_FEATURES } variant;
+    struct ofpbuf *reply;
+    ovs_be32 send_xid;
+    bool more;
+
+    struct ofputil_table_features features;
+};
+
+/* Initializes 'ti' to prepare for iterating through all of the tables on the
+ * OpenFlow switch to which 'vconn' is connected.
+ *
+ * During iteration, the client should not make other use of 'vconn', because
+ * that can cause other messages to be interleaved with the replies used by the
+ * iterator and thus some tables may be missed or a hang can occur. */
+static void
+table_iterator_init(struct table_iterator *ti, struct vconn *vconn)
+{
+    memset(ti, 0, sizeof *ti);
+    ti->vconn = vconn;
+    ti->variant = (vconn_get_version(vconn) < OFP13_VERSION
+                   ? TI_STATS : TI_FEATURES);
+    ti->more = true;
+
+    enum ofpraw raw = (ti->variant == TI_STATS
+                       ? OFPRAW_OFPST_TABLE_REQUEST
+                       : OFPRAW_OFPST13_TABLE_FEATURES_REQUEST);
+    struct ofpbuf *rq = ofpraw_alloc(raw, vconn_get_version(vconn), 0);
+    ti->send_xid = ((struct ofp_header *) rq->data)->xid;
+    send_openflow_buffer(ti->vconn, rq);
+}
+
+/* Obtains the next table from 'ti'.  On success, returns the next table's
+ * features; on failure, returns NULL.  */
+static const struct ofputil_table_features *
+table_iterator_next(struct table_iterator *ti)
+{
+    for (;;) {
+        if (ti->reply) {
+            int retval;
+            if (ti->variant == TI_STATS) {
+                struct ofputil_table_stats ts;
+                retval = ofputil_decode_table_stats_reply(ti->reply,
+                                                          &ts, &ti->features);
+            } else {
+                ovs_assert(ti->variant == TI_FEATURES);
+                retval = ofputil_decode_table_features(ti->reply,
+                                                       &ti->features,
+                                                       true);
+            }
+            if (!retval) {
+                return &ti->features;
+            } else if (retval != EOF) {
+                ovs_fatal(0, "received bad reply: %s",
+                          ofp_to_string(ti->reply->data, ti->reply->size,
+                                        NULL, NULL, verbosity + 1));
+            }
+        }
+
+        if (!ti->more) {
+            return NULL;
+        }
+
+        ovs_be32 recv_xid;
+        do {
+            ofpbuf_delete(ti->reply);
+            run(vconn_recv_block(ti->vconn, &ti->reply),
+                "OpenFlow receive failed");
+            recv_xid = ((struct ofp_header *) ti->reply->data)->xid;
+        } while (ti->send_xid != recv_xid);
+
+        struct ofp_header *oh = ti->reply->data;
+        enum ofptype type;
+        if (ofptype_pull(&type, ti->reply)
+            || type != (ti->variant == TI_STATS
+                        ? OFPTYPE_TABLE_STATS_REPLY
+                        : OFPTYPE_TABLE_FEATURES_STATS_REPLY)) {
+            ovs_fatal(0, "received bad reply: %s",
+                      ofp_to_string(ti->reply->data, ti->reply->size, NULL,
+                                    NULL, verbosity + 1));
+        }
+
+        ti->more = (ofpmp_flags(oh) & OFPSF_REPLY_MORE) != 0;
+    }
+}
+
+/* Destroys iterator 'ti'. */
+static void
+table_iterator_destroy(struct table_iterator *ti)
+{
+    if (ti) {
+        while (ti->more) {
+            /* Drain vconn's queue of any other replies for this request. */
+            table_iterator_next(ti);
+        }
+
+        ofpbuf_delete(ti->reply);
+    }
+}
+
+static const struct ofputil_table_map *
+get_table_map(const char *vconn_name)
+{
+    static struct shash table_maps = SHASH_INITIALIZER(&table_maps);
+    struct ofputil_table_map *map = shash_find_data(&table_maps, vconn_name);
+    if (!map) {
+        map = xmalloc(sizeof *map);
+        ofputil_table_map_init(map);
+        shash_add(&table_maps, vconn_name, map);
+
+        if (!strchr(vconn_name, ':') || !vconn_verify_name(vconn_name)) {
+            /* For an active vconn (which includes a vconn constructed from a
+             * bridge name), connect to it and pull down the port name-number
+             * mapping. */
+            struct vconn *vconn;
+            open_vconn(vconn_name, &vconn);
+
+            struct table_iterator ti;
+            table_iterator_init(&ti, vconn);
+            for (;;) {
+                const struct ofputil_table_features *tf
+                    = table_iterator_next(&ti);
+                if (!tf) {
+                    break;
+                }
+                if (tf->name[0]) {
+                    ofputil_table_map_put(map, tf->table_id, tf->name);
+                }
+            }
+            table_iterator_destroy(&ti);
+
+            vconn_close(vconn);
+        } else {
+            /* Don't bother with passive vconns, since it could take a long
+             * time for the remote to try to connect to us.  Don't bother with
+             * invalid vconn names either. */
+        }
+    }
+    return map;
+}
+
+static const struct ofputil_table_map *
+tables_to_accept(const char *vconn_name)
+{
+    return should_accept_names() ? get_table_map(vconn_name) : NULL;
+}
+
+static const struct ofputil_table_map *
+tables_to_show(const char *vconn_name)
+{
+    return should_show_names() ? get_table_map(vconn_name) : NULL;
 }
 
-/* We accept port names unless the feature is turned off explicitly. */
+/* We accept port and table names unless the feature is turned off
+ * explicitly. */
 static bool
-should_accept_ports(void)
+should_accept_names(void)
 {
-    return use_port_names != 0;
+    return use_names != 0;
 }
 
-/* We show port names only if the feature is turned on explicitly, or if we're
- * interacting with a user on the console. */
+/* We show port and table names only if the feature is turned on explicitly, or
+ * if we're interacting with a user on the console. */
 static bool
-should_show_ports(void)
+should_show_names(void)
 {
     static int interactive = -1;
     if (interactive == -1) {
         interactive = isatty(STDOUT_FILENO);
     }
 
-    return use_port_names > 0 || (use_port_names == -1 && interactive);
+    return use_names > 0 || (use_names == -1 && interactive);
 }
 
 /* Returns the port number corresponding to 'port_name' (which may be a port
@@ -1218,7 +1386,7 @@  try_set_protocol(struct vconn *vconn, enum ofputil_protocol want,
         run(vconn_transact_noreply(vconn, request, &reply),
             "talking to %s", vconn_get_name(vconn));
         if (reply) {
-            char *s = ofp_to_string(reply->data, reply->size, NULL, 2);
+            char *s = ofp_to_string(reply->data, reply->size, NULL, NULL, 2);
             VLOG_DBG("%s: failed to set protocol, switch replied: %s",
                      vconn_get_name(vconn), s);
             free(s);
@@ -1270,8 +1438,11 @@  prepare_dump_flows(int argc, char *argv[], bool aggregate,
     const char *match = argc > 2 ? argv[2] : "";
     const struct ofputil_port_map *port_map
         = *match ? ports_to_accept(vconn_name) : NULL;
+    const struct ofputil_table_map *table_map
+        = *match ? tables_to_accept(vconn_name) : NULL;
     error = parse_ofp_flow_stats_request_str(fsr, aggregate, match,
-                                             port_map, &usable_protocols);
+                                             port_map, table_map,
+                                             &usable_protocols);
     if (error) {
         ovs_fatal(0, "%s", error);
     }
@@ -1362,7 +1533,7 @@  compare_flows(const void *afs_, const void *bfs_)
 static void
 ofctl_dump_flows(struct ovs_cmdl_context *ctx)
 {
-    if (!n_criteria && !should_show_ports() && show_stats) {
+    if (!n_criteria && !should_show_names() && show_stats) {
         ofctl_dump_flows__(ctx->argc, ctx->argv, false);
         return;
     } else {
@@ -1384,7 +1555,7 @@  ofctl_dump_flows(struct ovs_cmdl_context *ctx)
         for (size_t i = 0; i < n_fses; i++) {
             ds_clear(&s);
             ofp_print_flow_stats(&s, &fses[i], ports_to_show(ctx->argv[1]),
-                                 show_stats);
+                                 tables_to_show(ctx->argv[1]), show_stats);
             printf(" %s\n", ds_cstr(&s));
         }
         ds_destroy(&s);
@@ -1575,7 +1746,8 @@  ofctl_flow_mod_file(int argc OVS_UNUSED, char *argv[], int command)
          * this is backwards compatible. */
         command = -2;
     }
-    error = parse_ofp_flow_mod_file(argv[2], ports_to_accept(argv[1]), command,
+    error = parse_ofp_flow_mod_file(argv[2], ports_to_accept(argv[1]),
+                                    tables_to_accept(argv[1]), command,
                                     &fms, &n_fms, &usable_protocols);
     if (error) {
         ovs_fatal(0, "%s", error);
@@ -1595,7 +1767,8 @@  ofctl_flow_mod(int argc, char *argv[], uint16_t command)
         enum ofputil_protocol usable_protocols;
 
         error = parse_ofp_flow_mod_str(&fm, argc > 2 ? argv[2] : "",
-                                       ports_to_accept(argv[1]), command,
+                                       ports_to_accept(argv[1]),
+                                       tables_to_accept(argv[1]), command,
                                        &usable_protocols);
         if (error) {
             ovs_fatal(0, "%s", error);
@@ -1645,7 +1818,7 @@  set_packet_in_format(struct vconn *vconn,
         run(vconn_transact_noreply(vconn, spif, &reply),
             "talking to %s", vconn_get_name(vconn));
         if (reply) {
-            char *s = ofp_to_string(reply->data, reply->size, NULL, 2);
+            char *s = ofp_to_string(reply->data, reply->size, NULL, NULL, 2);
             VLOG_DBG("%s: failed to set packet in format to nx_packet_in, "
                      "controller replied: %s.",
                      vconn_get_name(vconn), s);
@@ -1741,7 +1914,8 @@  ofctl_send(struct unixctl_conn *conn, int argc,
 
         fprintf(stderr, "send: ");
         ofp_print(stderr, msg->data, msg->size,
-                  ports_to_show(vconn_get_name(vconn)), verbosity);
+                  ports_to_show(vconn_get_name(vconn)),
+                  tables_to_show(vconn_get_name(vconn)), verbosity);
 
         error = vconn_send_block(vconn, msg);
         if (error) {
@@ -1777,7 +1951,7 @@  unixctl_packet_out(struct unixctl_conn *conn, int OVS_UNUSED argc,
 
     error_msg = parse_ofp_packet_out_str(
         &po, argv[1], ports_to_accept(vconn_get_name(vconn)),
-        &usable_protocols);
+        tables_to_accept(vconn_get_name(vconn)), &usable_protocols);
     if (error_msg) {
         ds_put_format(&reply, "%s\n", error_msg);
         free(error_msg);
@@ -1793,7 +1967,8 @@  unixctl_packet_out(struct unixctl_conn *conn, int OVS_UNUSED argc,
         struct ofpbuf *msg = ofputil_encode_packet_out(&po, protocol);
 
         ofp_print(stderr, msg->data, msg->size,
-                  ports_to_show(vconn_get_name(vconn)), verbosity);
+                  ports_to_show(vconn_get_name(vconn)),
+                  tables_to_show(vconn_get_name(vconn)), verbosity);
 
         int error = vconn_send_block(vconn, msg);
         if (error) {
@@ -1957,7 +2132,8 @@  monitor_vconn(struct vconn *vconn, bool reply_to_echo_requests,
 
             ofptype_decode(&type, b->data);
             ofp_print(stderr, b->data, b->size,
-                      ports_to_show(vconn_get_name(vconn)), verbosity + 2);
+                      ports_to_show(vconn_get_name(vconn)),
+                      tables_to_show(vconn_get_name(vconn)), verbosity + 2);
             fflush(stderr);
 
             switch ((int) type) {
@@ -2000,6 +2176,7 @@  monitor_vconn(struct vconn *vconn, bool reply_to_echo_requests,
                         fprintf(stderr, "send: ");
                         ofp_print(stderr, reply->data, reply->size,
                                   ports_to_show(vconn_get_name(vconn)),
+                                  tables_to_show(vconn_get_name(vconn)),
                                   verbosity + 2);
                         fflush(stderr);
 
@@ -2079,6 +2256,7 @@  ofctl_monitor(struct ovs_cmdl_context *ctx)
 
             error = parse_flow_monitor_request(&fmr, arg + 6,
                                                ports_to_accept(ctx->argv[1]),
+                                               tables_to_accept(ctx->argv[1]),
                                                &usable_protocols);
             if (error) {
                 ovs_fatal(0, "%s", error);
@@ -2196,6 +2374,7 @@  ofctl_packet_out(struct ovs_cmdl_context *ctx)
         ofpbuf_init(&ofpacts, 64);
         struct ofpact_parse_params pp = {
             .port_map = ports_to_accept(ctx->argv[1]),
+            .table_map = tables_to_accept(ctx->argv[1]),
             .ofpacts = &ofpacts,
             .usable_protocols = &usable_protocols
         };
@@ -2233,6 +2412,7 @@  ofctl_packet_out(struct ovs_cmdl_context *ctx)
     } else if (ctx->argc == 3) {
         error = parse_ofp_packet_out_str(&po, ctx->argv[2],
                                          ports_to_accept(ctx->argv[1]),
+                                         tables_to_accept(ctx->argv[1]),
                                          &usable_protocols);
         if (error) {
             ovs_fatal(0, "%s", error);
@@ -2339,7 +2519,7 @@  fetch_table_desc(struct vconn *vconn, struct ofputil_table_mod *tm,
             if (ofptype_pull(&type, &b)
                 || type != OFPTYPE_TABLE_DESC_REPLY) {
                 ovs_fatal(0, "received bad reply: %s",
-                          ofp_to_string(reply->data, reply->size, NULL,
+                          ofp_to_string(reply->data, reply->size, NULL, NULL,
                                         verbosity + 1));
             }
             uint16_t flags = ofpmp_flags(oh);
@@ -2382,6 +2562,7 @@  ofctl_mod_table(struct ovs_cmdl_context *ctx)
     int i;
 
     error = parse_ofp_table_mod(&tm, ctx->argv[2], ctx->argv[3],
+                                tables_to_accept(ctx->argv[1]),
                                 &usable_versions);
     if (error) {
         ovs_fatal(0, "%s", error);
@@ -2507,7 +2688,7 @@  ofctl_ofp_parse(struct ovs_cmdl_context *ctx)
             ovs_fatal(0, "%s: unexpected end of file mid-message", filename);
         }
 
-        ofp_print(stdout, b.data, b.size, NULL, verbosity + 2);
+        ofp_print(stdout, b.data, b.size, NULL, NULL, verbosity + 2);
     }
     ofpbuf_uninit(&b);
 
@@ -2595,7 +2776,7 @@  ofctl_ofp_parse_pcap(struct ovs_cmdl_context *ctx)
                            IP_ARGS(flow.nw_src), ntohs(flow.tp_src),
                            IP_ARGS(flow.nw_dst), ntohs(flow.tp_dst));
                     ofp_print(stdout, dp_packet_data(payload), length,
-                              NULL, verbosity + 1);
+                              NULL, NULL, verbosity + 1);
                     dp_packet_pull(payload, length);
                 }
             }
@@ -2640,9 +2821,10 @@  ofctl_ping(struct ovs_cmdl_context *ctx)
             || reply->size != payload
             || memcmp(request->msg, reply->msg, payload)) {
             printf("Reply does not match request.  Request:\n");
-            ofp_print(stdout, request, request->size, NULL, verbosity + 2);
+            ofp_print(stdout, request, request->size, NULL, NULL,
+                      verbosity + 2);
             printf("Reply:\n");
-            ofp_print(stdout, reply, reply->size, NULL, verbosity + 2);
+            ofp_print(stdout, reply, reply->size, NULL, NULL, verbosity + 2);
         }
         printf("%"PRIu32" bytes from %s: xid=%08"PRIx32" time=%.1f ms\n",
                reply->size, ctx->argv[1], ntohl(rpy_hdr->xid),
@@ -2805,6 +2987,7 @@  ofctl_group_mod_file(int argc OVS_UNUSED, char *argv[], int command)
         command = -2;
     }
     error = parse_ofp_group_mod_file(argv[2], ports_to_accept(argv[1]),
+                                     tables_to_accept(argv[1]),
                                      command, &gms, &n_gms, &usable_protocols);
     if (error) {
         ovs_fatal(0, "%s", error);
@@ -2825,6 +3008,7 @@  ofctl_group_mod(int argc, char *argv[], uint16_t command)
 
         error = parse_ofp_group_mod_str(&gm, command, argc > 2 ? argv[2] : "",
                                         ports_to_accept(argv[1]),
+                                        tables_to_accept(argv[1]),
                                         &usable_protocols);
         if (error) {
             ovs_fatal(0, "%s", error);
@@ -2885,6 +3069,7 @@  ofctl_dump_group_stats(struct ovs_cmdl_context *ctx)
     error = parse_ofp_group_mod_str(&gm, OFPGC11_DELETE,
                                     ctx->argc > 2 ? ctx->argv[2] : "",
                                     ports_to_accept(ctx->argv[1]),
+                                    tables_to_accept(ctx->argv[1]),
                                     &usable_protocols);
     if (error) {
         ovs_fatal(0, "%s", error);
@@ -2950,6 +3135,7 @@  ofctl_bundle(struct ovs_cmdl_context *ctx)
     char *error;
 
     error = parse_ofp_bundle_file(ctx->argv[2], ports_to_accept(ctx->argv[1]),
+                                  tables_to_accept(ctx->argv[1]),
                                   &bms, &n_bms, &usable_protocols);
     if (error) {
         ovs_fatal(0, "%s", error);
@@ -3122,9 +3308,11 @@  struct fte_state {
     /* The final metadata table that we have constructed. */
     struct tun_table *tun_tab;
 
-    /* Port map.  There is only one port map, not one per source, because it
-     * only makes sense to display a single name for a given port number. */
+    /* Port and table map.  There is only one of each, not one per source,
+     * because it only makes sense to display a single name for a given port
+     * or table number. */
     const struct ofputil_port_map *port_map;
+    const struct ofputil_table_map *table_map;
 };
 
 /* Frees 'version' and the data that it owns. */
@@ -3358,6 +3546,7 @@  fte_state_init(struct fte_state *state)
     ovs_list_init(&state->fte_pending_list);
     state->tun_tab = NULL;
     state->port_map = NULL;
+    state->table_map = NULL;
 }
 
 static void
@@ -3443,7 +3632,7 @@  read_flows_from_file(const char *filename, struct fte_state *state, int index)
         enum ofputil_protocol usable;
 
         error = parse_ofp_str(&fm, OFPFC_ADD, ds_cstr(&s), state->port_map,
-                              &usable);
+                              state->table_map, &usable);
         if (error) {
             ovs_fatal(0, "%s:%d: %s", filename, line_number, error);
         }
@@ -3564,6 +3753,7 @@  ofctl_replace_flows(struct ovs_cmdl_context *ctx)
 
     fte_state_init(&fte_state);
     fte_state.port_map = ports_to_accept(ctx->argv[1]);
+    fte_state.table_map = tables_to_accept(ctx->argv[1]);
     usable_protocols = read_flows_from_file(ctx->argv[2], &fte_state, FILE_IDX);
 
     protocol = open_vconn(ctx->argv[1], &vconn);
@@ -3815,7 +4005,7 @@  ofctl_parse_flows__(struct ofputil_flow_mod *fms, size_t n_fms,
         struct ofpbuf *msg;
 
         msg = ofputil_encode_flow_mod(fm, protocol);
-        ofp_print(stdout, msg->data, msg->size, NULL, verbosity);
+        ofp_print(stdout, msg->data, msg->size, NULL, NULL, verbosity);
         ofpbuf_delete(msg);
 
         free(CONST_CAST(struct ofpact *, fm->ofpacts));
@@ -3831,7 +4021,7 @@  ofctl_parse_flow(struct ovs_cmdl_context *ctx)
     struct ofputil_flow_mod fm;
     char *error;
 
-    error = parse_ofp_flow_mod_str(&fm, ctx->argv[1], NULL,
+    error = parse_ofp_flow_mod_str(&fm, ctx->argv[1], NULL, NULL,
                                    OFPFC_ADD, &usable_protocols);
     if (error) {
         ovs_fatal(0, "%s", error);
@@ -3849,7 +4039,7 @@  ofctl_parse_flows(struct ovs_cmdl_context *ctx)
     size_t n_fms = 0;
     char *error;
 
-    error = parse_ofp_flow_mod_file(ctx->argv[1], NULL, OFPFC_ADD,
+    error = parse_ofp_flow_mod_file(ctx->argv[1], NULL, NULL, OFPFC_ADD,
                                     &fms, &n_fms, &usable_protocols);
     if (error) {
         ovs_fatal(0, "%s", error);
@@ -4291,7 +4481,7 @@  ofctl_check_vlan(struct ovs_cmdl_context *ctx)
     string_s = match_to_string(&match, NULL, OFP_DEFAULT_PRIORITY);
     printf("%s -> ", string_s);
     fflush(stdout);
-    error_s = parse_ofp_str(&fm, -1, string_s, NULL, &usable_protocols);
+    error_s = parse_ofp_str(&fm, -1, string_s, NULL, NULL, &usable_protocols);
     if (error_s) {
         ovs_fatal(0, "%s", error_s);
     }
@@ -4460,7 +4650,7 @@  ofctl_ofp_print(struct ovs_cmdl_context *ctx)
     if (ofpbuf_put_hex(&packet, buffer, NULL)[0] != '\0') {
         ovs_fatal(0, "trailing garbage following hex bytes");
     }
-    ofp_print(stdout, packet.data, packet.size, NULL, verbosity);
+    ofp_print(stdout, packet.data, packet.size, NULL, NULL, verbosity);
     ofpbuf_uninit(&packet);
     ds_destroy(&line);
 }
@@ -4475,7 +4665,7 @@  ofctl_encode_hello(struct ovs_cmdl_context *ctx)
 
     hello = ofputil_encode_hello(bitmap);
     ovs_hex_dump(stdout, hello->data, hello->size, 0, false);
-    ofp_print(stdout, hello->data, hello->size, NULL, verbosity);
+    ofp_print(stdout, hello->data, hello->size, NULL, NULL, verbosity);
     ofpbuf_delete(hello);
 }
 
diff --git a/utilities/ovs-testcontroller.c b/utilities/ovs-testcontroller.c
index 8f8fc2bb13a1..571af0c7f971 100644
--- a/utilities/ovs-testcontroller.c
+++ b/utilities/ovs-testcontroller.c
@@ -335,7 +335,7 @@  parse_options(int argc, char *argv[])
             break;
 
         case OPT_WITH_FLOWS:
-            error = parse_ofp_flow_mod_file(optarg, NULL, OFPFC_ADD,
+            error = parse_ofp_flow_mod_file(optarg, NULL, NULL, OFPFC_ADD,
                                             &default_flows, &n_default_flows,
                                             &usable_protocols);
             if (error) {