diff mbox

[ovs-dev,13/13] ofproto: Support group mods in bundles.

Message ID 1468577959-98487-13-git-send-email-jarno@ovn.org
State Superseded
Headers show

Commit Message

Jarno Rajahalme July 15, 2016, 10:19 a.m. UTC
Allow adding group mods in OpenFlow bundles.  Group mods are executed
atomically with any flow mods in the same bundle.  Mods are executed
in order, so that groups appearing in flow actions need to be inserted
in to the bundle before the dependent flow mods.

ovs-ofctl is enhanced to allow the '--bundle' option with group mod
commands.  add-groups file format is enhanced to allow each line to be
preceded by one of the keywords "add", "modify", "delete",
"add_or_mod", "insert_bucket", or "remove_bucket".

ovs-ofctl also has a new "bundle" command that reads a file in which
each line contains one flow mod or group mod, and then executes them
all as a single atomic bundle transaction.

Signed-off-by: Jarno Rajahalme <jarno@ovn.org>
---
 NEWS                            |   7 +-
 include/openvswitch/ofp-parse.h |  10 +-
 include/openvswitch/ofp-util.h  |  16 +-
 lib/ofp-parse.c                 | 152 +++++++++++++++-
 lib/ofp-util.c                  |  33 +++-
 ofproto/bundles.h               |   8 +-
 ofproto/ofproto.c               | 149 ++++++++++-----
 tests/ofproto-macros.at         |   2 +
 tests/ofproto.at                | 395 +++++++++++++++++++++++++++++++++++++++-
 utilities/ovs-ofctl.8.in        |  56 ++++--
 utilities/ovs-ofctl.c           | 101 ++++++++--
 11 files changed, 849 insertions(+), 80 deletions(-)
diff mbox

Patch

diff --git a/NEWS b/NEWS
index 6496dc1..a190292 100644
--- a/NEWS
+++ b/NEWS
@@ -3,6 +3,9 @@  Post-v2.5.0
    - ovsdb-server:
      * New "monitor2" and "update2" extensions to RFC 7047.
    - OpenFlow:
+     * OpenFlow 1.3+ bundles are now supported for group mods as well as
+       flow mods and port mods.  Both 'atomic' and 'ordered' bundle
+       flags are supported for group mods as well as flow mods.
      * OpenFlow 1.1+ OFPT_QUEUE_GET_CONFIG_REQUEST now supports OFPP_ANY.
      * OpenFlow 1.4+ OFPMP_QUEUE_DESC is now supported.
      * OpenFlow 1.4+ OFPT_TABLE_STATUS is now supported.
@@ -26,7 +29,9 @@  Post-v2.5.0
        properly translated to OpenFlow 1.0.
    - ovs-ofctl:
      * queue-get-config command now allows a queue ID to be specified.
-     * '--bundle' option can now be used with OpenFlow 1.3.
+     * '--bundle' option can now be used with OpenFlow 1.3 and with group mods.
+     * New "bundle" command allows executing a mixture of flow and group mods
+       as a single atomic transaction.
      * New option "--color" to produce colorized output for some commands.
      * New option '--may-create' to use OFPGC_ADD_OR_MOD in mod-group command.
    - IPFIX:
diff --git a/include/openvswitch/ofp-parse.h b/include/openvswitch/ofp-parse.h
index 1ab5095..76a8369 100644
--- a/include/openvswitch/ofp-parse.h
+++ b/include/openvswitch/ofp-parse.h
@@ -33,6 +33,7 @@  struct ofputil_flow_stats_request;
 struct ofputil_group_mod;
 struct ofputil_meter_mod;
 struct ofputil_table_mod;
+struct ofputil_bundle_msg;
 struct ofputil_tlv_table_mod;
 struct simap;
 enum ofputil_protocol;
@@ -74,16 +75,21 @@  char *parse_flow_monitor_request(struct ofputil_flow_monitor_request *,
                                  enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
 
-char *parse_ofp_group_mod_file(const char *file_name, uint16_t command,
+char *parse_ofp_group_mod_file(const char *file_name, int command,
                                struct ofputil_group_mod **gms, size_t *n_gms,
                                enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
 
-char *parse_ofp_group_mod_str(struct ofputil_group_mod *, uint16_t command,
+char *parse_ofp_group_mod_str(struct ofputil_group_mod *, int command,
                               const char *string,
                               enum ofputil_protocol *usable_protocols)
     OVS_WARN_UNUSED_RESULT;
 
+char *parse_ofp_bundle_file(const char *file_name,
+                            struct ofputil_bundle_msg **, size_t *n_bms,
+                            enum ofputil_protocol *)
+    OVS_WARN_UNUSED_RESULT;
+
 char *parse_ofp_tlv_table_mod_str(struct ofputil_tlv_table_mod *,
                                      uint16_t command, const char *string,
                                      enum ofputil_protocol *usable_protocols)
diff --git a/include/openvswitch/ofp-util.h b/include/openvswitch/ofp-util.h
index 7a5c905..28d11d3 100644
--- a/include/openvswitch/ofp-util.h
+++ b/include/openvswitch/ofp-util.h
@@ -27,6 +27,7 @@ 
 #include "openvswitch/netdev.h"
 #include "openflow/netronome-ext.h"
 #include "openflow/nicira-ext.h"
+#include "openvswitch/ofp-msgs.h"
 #include "openvswitch/ofpbuf.h"
 #include "openvswitch/types.h"
 #include "openvswitch/type-props.h"
@@ -1324,8 +1325,6 @@  struct ofputil_bundle_add_msg {
     const struct ofp_header   *msg;
 };
 
-enum ofptype;
-
 enum ofperr ofputil_decode_bundle_ctrl(const struct ofp_header *,
                                        struct ofputil_bundle_ctrl_msg *);
 
@@ -1341,6 +1340,19 @@  enum ofperr ofputil_decode_bundle_add(const struct ofp_header *,
                                       struct ofputil_bundle_add_msg *,
                                       enum ofptype *type);
 
+struct ofputil_bundle_msg {
+    enum ofptype type;
+    union {
+        struct ofputil_flow_mod fm;
+        struct ofputil_group_mod gm;
+    };
+};
+
+/* Destroys 'bms'. */
+void ofputil_encode_bundle_msgs(struct ofputil_bundle_msg *bms, size_t n_bms,
+                                struct ovs_list *requests,
+                                enum ofputil_protocol);
+
 struct ofputil_tlv_map {
     struct ovs_list list_node;
 
diff --git a/lib/ofp-parse.c b/lib/ofp-parse.c
index 4af6d9b..da52f36 100644
--- a/lib/ofp-parse.c
+++ b/lib/ofp-parse.c
@@ -1350,7 +1350,7 @@  parse_select_group_field(char *s, struct field_array *fa,
 }
 
 static char * OVS_WARN_UNUSED_RESULT
-parse_ofp_group_mod_str__(struct ofputil_group_mod *gm, uint16_t command,
+parse_ofp_group_mod_str__(struct ofputil_group_mod *gm, int command,
                           char *string,
                           enum ofputil_protocol *usable_protocols)
 {
@@ -1367,6 +1367,31 @@  parse_ofp_group_mod_str__(struct ofputil_group_mod *gm, uint16_t command,
 
     *usable_protocols = OFPUTIL_P_OF11_UP;
 
+    if (command == -2) {
+        size_t len;
+
+        string += strspn(string, " \t\r\n");   /* Skip white space. */
+        len = strcspn(string, ", \t\r\n"); /* Get length of the first token. */
+
+        if (!strncmp(string, "add", len)) {
+            command = OFPGC11_ADD;
+        } else if (!strncmp(string, "delete", len)) {
+            command = OFPGC11_DELETE;
+        } else if (!strncmp(string, "modify", len)) {
+            command = OFPGC11_MODIFY;
+        } else if (!strncmp(string, "add_or_mod", len)) {
+            command = OFPGC11_ADD_OR_MOD;
+        } else if (!strncmp(string, "insert_bucket", len)) {
+            command = OFPGC15_INSERT_BUCKET;
+        } else if (!strncmp(string, "remove_bucket", len)) {
+            command = OFPGC15_REMOVE_BUCKET;
+        } else {
+            len = 0;
+            command = OFPGC11_ADD;
+        }
+        string += len;
+    }
+
     switch (command) {
     case OFPGC11_ADD:
         fields = F_GROUP_TYPE | F_BUCKETS;
@@ -1592,8 +1617,12 @@  parse_ofp_group_mod_str__(struct ofputil_group_mod *gm, uint16_t command,
     return error;
 }
 
+/* If 'command' is given as -2, each line may start with a command name ("add",
+ * "modify", "add_or_mod", "delete", "insert_bucket", or "remove_bucket").  A
+ * missing command name is treated as "add".
+ */
 char * OVS_WARN_UNUSED_RESULT
-parse_ofp_group_mod_str(struct ofputil_group_mod *gm, uint16_t command,
+parse_ofp_group_mod_str(struct ofputil_group_mod *gm, int command,
                         const char *str_,
                         enum ofputil_protocol *usable_protocols)
 {
@@ -1608,8 +1637,12 @@  parse_ofp_group_mod_str(struct ofputil_group_mod *gm, uint16_t command,
     return error;
 }
 
+/* If 'command' is given as -2, each line may start with a command name ("add",
+ * "modify", "add_or_mod", "delete", "insert_bucket", or "remove_bucket").  A
+ * missing command name is treated as "add".
+ */
 char * OVS_WARN_UNUSED_RESULT
-parse_ofp_group_mod_file(const char *file_name, uint16_t command,
+parse_ofp_group_mod_file(const char *file_name, int command,
                          struct ofputil_group_mod **gms, size_t *n_gms,
                          enum ofputil_protocol *usable_protocols)
 {
@@ -1675,6 +1708,119 @@  parse_ofp_group_mod_file(const char *file_name, uint16_t command,
     return NULL;
 }
 
+static void
+free_bundle_msgs(struct ofputil_bundle_msg **bms, size_t *n_bms)
+{
+    for (size_t i = 0; i < *n_bms; i++) {
+        switch ((int)(*bms)[i].type) {
+        case OFPTYPE_FLOW_MOD:
+            free(CONST_CAST(struct ofpact *, (*bms)[i].fm.ofpacts));
+            break;
+        case OFPTYPE_GROUP_MOD:
+            ofputil_bucket_list_destroy(&(*bms)[i].gm.buckets);
+            break;
+        default:
+            break;
+        }
+    }
+    free(*bms);
+    *bms = NULL;
+    *n_bms = 0;
+}
+
+/* Opens file 'file_name' and reads each line as a flow_mod or a group_mod,
+ * depending on the first keyword on each line.  Stores each flow and group
+ * mods in '*bms', an array allocated on the caller's behalf, and the number of
+ * messages in '*n_bms'.
+ *
+ * Returns NULL if successful, otherwise a malloc()'d string describing the
+ * error.  The caller is responsible for freeing the returned string. */
+char * OVS_WARN_UNUSED_RESULT
+parse_ofp_bundle_file(const char *file_name,
+                      struct ofputil_bundle_msg **bms, size_t *n_bms,
+                      enum ofputil_protocol *usable_protocols)
+{
+    size_t allocated_bms;
+    char *error = NULL;
+    int line_number;
+    FILE *stream;
+    struct ds ds;
+
+    *usable_protocols = OFPUTIL_P_ANY;
+
+    *bms = NULL;
+    *n_bms = 0;
+
+    stream = !strcmp(file_name, "-") ? stdin : fopen(file_name, "r");
+    if (stream == NULL) {
+        return xasprintf("%s: open failed (%s)",
+                         file_name, ovs_strerror(errno));
+    }
+
+    allocated_bms = *n_bms;
+    ds_init(&ds);
+    line_number = 0;
+    while (!ds_get_preprocessed_line(&ds, stream, &line_number)) {
+        enum ofputil_protocol usable;
+        char *s = ds_cstr(&ds);
+        size_t len;
+
+        if (*n_bms >= allocated_bms) {
+            struct ofputil_bundle_msg *new_bms;
+
+            new_bms = x2nrealloc(*bms, &allocated_bms, sizeof **bms);
+            for (size_t i = 0; i < *n_bms; i++) {
+                if (new_bms[i].type == OFPTYPE_GROUP_MOD) {
+                    ovs_list_moved(&new_bms[i].gm.buckets,
+                                   &(*bms)[i].gm.buckets);
+                }
+            }
+            *bms = new_bms;
+        }
+
+        s += strspn(s, " \t\r\n");   /* Skip white space. */
+        len = strcspn(s, ", \t\r\n"); /* Get length of the first token. */
+
+        if (!strncmp(s, "flow", len)) {
+            s += len;
+            error = parse_ofp_flow_mod_str(&(*bms)[*n_bms].fm, s, -2, &usable);
+            if (error) {
+                break;
+            }
+            (*bms)[*n_bms].type = OFPTYPE_FLOW_MOD;
+        } else if (!strncmp(s, "group", len)) {
+            s += len;
+            error = parse_ofp_group_mod_str(&(*bms)[*n_bms].gm, -2, s,
+                                            &usable);
+            if (error) {
+                break;
+            }
+            (*bms)[*n_bms].type = OFPTYPE_GROUP_MOD;
+        } else {
+            error = xasprintf("Unsupported bundle message type: %.*s",
+                              (int)len, s);
+            break;
+        }
+
+        *usable_protocols &= usable; /* Each line can narrow the set. */
+        *n_bms += 1;
+    }
+
+    ds_destroy(&ds);
+    if (stream != stdin) {
+        fclose(stream);
+    }
+
+    if (error) {
+        char *err_msg = xasprintf("%s:%d: %s", file_name, line_number, error);
+        free(error);
+
+        free_bundle_msgs(bms, n_bms);
+        return err_msg;
+    }
+    return NULL;
+}
+
 char * OVS_WARN_UNUSED_RESULT
 parse_ofp_tlv_table_mod_str(struct ofputil_tlv_table_mod *ttm,
                                uint16_t command, const char *s,
diff --git a/lib/ofp-util.c b/lib/ofp-util.c
index 40fd2b1..27d6fd1 100644
--- a/lib/ofp-util.c
+++ b/lib/ofp-util.c
@@ -9480,6 +9480,36 @@  ofputil_decode_group_mod(const struct ofp_header *oh,
     return 0;
 }
 
+/* Destroys 'bms'. */
+void
+ofputil_encode_bundle_msgs(struct ofputil_bundle_msg *bms, size_t n_bms,
+                           struct ovs_list *requests,
+                           enum ofputil_protocol protocol)
+{
+    enum ofp_version version = ofputil_protocol_to_ofp_version(protocol);
+
+    for (size_t i = 0; i < n_bms; i++) {
+        struct ofpbuf *request = NULL;
+
+        switch ((int)bms[i].type) {
+        case OFPTYPE_FLOW_MOD:
+            request = ofputil_encode_flow_mod(&bms[i].fm, protocol);
+            free(CONST_CAST(struct ofpact *, bms[i].fm.ofpacts));
+            break;
+        case OFPTYPE_GROUP_MOD:
+            request = ofputil_encode_group_mod(version, &bms[i].gm);
+            ofputil_bucket_list_destroy(&bms[i].gm.buckets);
+            break;
+        default:
+            break;
+        }
+        if (request) {
+            ovs_list_push_back(requests, &request->list_node);
+        }
+    }
+    free(bms);
+}
+
 /* Parse a queue status request message into 'oqsr'.
  * Returns 0 if successful, otherwise an OFPERR_* number. */
 enum ofperr
@@ -9863,11 +9893,12 @@  ofputil_is_bundlable(enum ofptype type)
         /* Minimum required by OpenFlow 1.4. */
     case OFPTYPE_PORT_MOD:
     case OFPTYPE_FLOW_MOD:
+        /* Other supported types. */
+    case OFPTYPE_GROUP_MOD:
         return true;
 
         /* Nice to have later. */
     case OFPTYPE_FLOW_MOD_TABLE_ID:
-    case OFPTYPE_GROUP_MOD:
     case OFPTYPE_TABLE_MOD:
     case OFPTYPE_METER_MOD:
     case OFPTYPE_PACKET_OUT:
diff --git a/ofproto/bundles.h b/ofproto/bundles.h
index d045031..a7a45b1 100644
--- a/ofproto/bundles.h
+++ b/ofproto/bundles.h
@@ -1,7 +1,7 @@ 
 /*
  * Copyright (c) 2013, 2014 Alexandru Copot <alex.mihai.c@gmail.com>, with support from IXIA.
  * Copyright (c) 2013, 2014 Daniel Baluta <dbaluta@ixiacom.com>
- * Copyright (c) 2014, 2015 Nicira, Inc.
+ * Copyright (c) 2014, 2015, 2016 Nicira, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -33,10 +33,12 @@  extern "C" {
 
 struct ofp_bundle_entry {
     struct ovs_list   node;
-    enum ofptype      type;  /* OFPTYPE_FLOW_MOD or OFPTYPE_PORT_MOD. */
+    enum ofptype      type;  /* OFPTYPE_FLOW_MOD, OFPTYPE_PORT_MOD, or
+                              * OFPTYPE_GROUP_MOD. */
     union {
         struct ofproto_flow_mod ofm;   /* ofm.fm.ofpacts must be malloced. */
         struct ofproto_port_mod opm;
+        struct ofproto_group_mod ogm;
     };
 
     /* OpenFlow header and some of the message contents for error reporting. */
@@ -89,6 +91,8 @@  ofp_bundle_entry_free(struct ofp_bundle_entry *entry)
     if (entry) {
         if (entry->type == OFPTYPE_FLOW_MOD) {
             free(entry->ofm.fm.ofpacts);
+        } else if (entry->type == OFPTYPE_GROUP_MOD) {
+            ofputil_bucket_list_destroy(&entry->ogm.gm.buckets);
         }
         free(entry);
     }
diff --git a/ofproto/ofproto.c b/ofproto/ofproto.c
index efebbf6..aa2f2f9 100644
--- a/ofproto/ofproto.c
+++ b/ofproto/ofproto.c
@@ -5289,6 +5289,27 @@  delete_flows_start__(struct ofproto *ofproto, ovs_version_t version,
 }
 
 static void
+delete_flows_revert__(struct ofproto *ofproto,
+                      const struct rule_collection *rules)
+    OVS_REQUIRES(ofproto_mutex)
+{
+    struct rule *rule;
+
+    RULE_COLLECTION_FOR_EACH (rule, rules) {
+        struct oftable *table = &ofproto->tables[rule->table_id];
+
+        /* Add rule back to ofproto data structures. */
+        ofproto_rule_insert__(ofproto, rule);
+
+        /* Restore table's rule count. */
+        table->n_flows++;
+
+        /* Restore the original visibility of the rule. */
+        cls_rule_restore_visibility(&rule->cr);
+    }
+}
+
+static void
 delete_flows_finish__(struct ofproto *ofproto,
                       struct rule_collection *rules,
                       enum ofp_flow_removed_reason reason,
@@ -5368,22 +5389,8 @@  static void
 delete_flows_revert(struct ofproto *ofproto, struct ofproto_flow_mod *ofm)
     OVS_REQUIRES(ofproto_mutex)
 {
-    struct rule_collection *rules = &ofm->old_rules;
-    struct rule *rule;
-
-    RULE_COLLECTION_FOR_EACH (rule, rules) {
-        struct oftable *table = &ofproto->tables[rule->table_id];
-
-        /* Add rule back to ofproto data structures. */
-        ofproto_rule_insert__(ofproto, rule);
-
-        /* Restore table's rule count. */
-        table->n_flows++;
-
-        /* Restore the original visibility of the rule. */
-        cls_rule_restore_visibility(&rule->cr);
-    }
-    rule_collection_destroy(rules);
+    delete_flows_revert__(ofproto, &ofm->old_rules);
+    rule_collection_destroy(&ofm->old_rules);
 }
 
 static void
@@ -6872,6 +6879,40 @@  ofproto_group_mod_start(struct ofproto *ofproto, struct ofproto_group_mod *ogm)
 }
 
 static void
+ofproto_group_mod_revert(struct ofproto *ofproto,
+                         struct ofproto_group_mod *ogm)
+    OVS_REQUIRES(ofproto_mutex)
+{
+    struct ofgroup *new_group = ogm->new_group;
+    struct ofgroup *old_group;
+
+    /* Restore replaced or deleted groups. */
+    GROUP_COLLECTION_FOR_EACH (old_group, &ogm->old_groups) {
+        ofproto->n_groups[old_group->type]++;
+        if (new_group) {
+            ovs_assert(group_collection_n(&ogm->old_groups) == 1);
+            /* Transfer rules back. */
+            rule_collection_move(&old_group->rules, &new_group->rules);
+        } else {
+            old_group->being_deleted = false;
+            /* Revert rule deletion. */
+            delete_flows_revert__(ofproto, &old_group->rules);
+        }
+        /* Restore visibility. */
+        versions_set_remove_version(&old_group->versions,
+                                    OVS_VERSION_NOT_REMOVED);
+    }
+    if (new_group) {
+        /* Remove the new group immediately.  It was never visible to
+         * lookups. */
+        cmap_remove(&ofproto->groups, &new_group->cmap_node,
+                    hash_int(new_group->group_id, 0));
+        ofproto->n_groups[new_group->type]--;
+        ofproto_group_unref(new_group);
+    }
+}
+
+static void
 ofproto_group_mod_finish(struct ofproto *ofproto,
                          struct ofproto_group_mod *ogm,
                          const struct openflow_mod_requester *req)
@@ -7200,20 +7241,27 @@  do_bundle_commit(struct ofconn *ofconn, uint32_t id, uint16_t flags)
                     prev_is_port_mod = true;
                     error = port_mod_start(ofconn, &be->opm.pm, &be->opm.port);
                 }
-            } else if (be->type == OFPTYPE_FLOW_MOD) {
-                /* Flow mods between port mods are applied as a single
-                 * version, but the versions are published only after
-                 * we know the commit is successful. */
+            } else {
+                /* Flow & group mods between port mods are applied as a single
+                 * version, but the versions are published only after we know
+                 * the commit is successful. */
                 if (prev_is_port_mod) {
+                    prev_is_port_mod = false;
                     ++version;
                 }
-                prev_is_port_mod = false;
-                /* Store the version in which the changes should take
-                 * effect. */
-                be->ofm.version = version;
-                error = ofproto_flow_mod_start(ofproto, &be->ofm);
-            } else {
-                OVS_NOT_REACHED();
+                if (be->type == OFPTYPE_FLOW_MOD) {
+                    /* Store the version in which the changes should take
+                     * effect. */
+                    be->ofm.version = version;
+                    error = ofproto_flow_mod_start(ofproto, &be->ofm);
+                } else if (be->type == OFPTYPE_GROUP_MOD) {
+                    /* Store the version in which the changes should take
+                     * effect. */
+                    be->ogm.version = version;
+                    error = ofproto_group_mod_start(ofproto, &be->ogm);
+                } else {
+                    OVS_NOT_REACHED();
+                }
             }
             if (error) {
                 break;
@@ -7231,27 +7279,16 @@  do_bundle_commit(struct ofconn *ofconn, uint32_t id, uint16_t flags)
             LIST_FOR_EACH_REVERSE_CONTINUE(be, node, &bundle->msg_list) {
                 if (be->type == OFPTYPE_FLOW_MOD) {
                     ofproto_flow_mod_revert(ofproto, &be->ofm);
+                } else if (be->type == OFPTYPE_GROUP_MOD) {
+                    ofproto_group_mod_revert(ofproto, &be->ogm);
                 }
+
                 /* Nothing needs to be reverted for a port mod. */
             }
         } else {
             /* 4. Finish. */
             LIST_FOR_EACH (be, node, &bundle->msg_list) {
-                if (be->type == OFPTYPE_FLOW_MOD) {
-                    struct openflow_mod_requester req = { ofconn,
-                                                          be->ofp_msg };
-
-                    /* Bump the lookup version to the one of the current
-                     * message.  This makes all the changes in the bundle at
-                     * this version visible to lookups at once. */
-                    if (ofproto->tables_version < be->ofm.version) {
-                        ofproto->tables_version = be->ofm.version;
-                        ofproto->ofproto_class->set_tables_version(
-                            ofproto, ofproto->tables_version);
-                    }
-
-                    ofproto_flow_mod_finish(ofproto, &be->ofm, &req);
-                } else if (be->type == OFPTYPE_PORT_MOD) {
+                if (be->type == OFPTYPE_PORT_MOD) {
                     /* Perform the actual port mod. This is not atomic, i.e.,
                      * the effects will be immediately seen by upcall
                      * processing regardless of the lookup version.  It should
@@ -7259,6 +7296,32 @@  do_bundle_commit(struct ofconn *ofconn, uint32_t id, uint16_t flags)
                      * also from OVSDB changes asynchronously to all upcall
                      * processing. */
                     port_mod_finish(ofconn, &be->opm.pm, be->opm.port);
+                } else {
+                    struct openflow_mod_requester req = { ofconn,
+                                                          be->ofp_msg };
+                    if (be->type == OFPTYPE_FLOW_MOD) {
+                        /* Bump the lookup version to the one of the current
+                         * message.  This makes all the changes in the bundle
+                         * at this version visible to lookups at once. */
+                        if (ofproto->tables_version < be->ofm.version) {
+                            ofproto->tables_version = be->ofm.version;
+                            ofproto->ofproto_class->set_tables_version(
+                                ofproto, ofproto->tables_version);
+                        }
+
+                        ofproto_flow_mod_finish(ofproto, &be->ofm, &req);
+                    } else if (be->type == OFPTYPE_GROUP_MOD) {
+                        /* Bump the lookup version to the one of the current
+                         * message.  This makes all the changes in the bundle
+                         * at this version visible to lookups at once. */
+                        if (ofproto->tables_version < be->ogm.version) {
+                            ofproto->tables_version = be->ogm.version;
+                            ofproto->ofproto_class->set_tables_version(
+                                ofproto, ofproto->tables_version);
+                        }
+
+                        ofproto_group_mod_finish(ofproto, &be->ogm, &req);
+                    }
                 }
             }
         }
@@ -7362,6 +7425,8 @@  handle_bundle_add(struct ofconn *ofconn, const struct ofp_header *oh)
                                         ofproto->n_tables);
         /* Move actions to heap. */
         bmsg->ofm.fm.ofpacts = ofpbuf_steal_data(&ofpacts);
+    } else if (type == OFPTYPE_GROUP_MOD) {
+        error = ofputil_decode_group_mod(badd.msg, &bmsg->ogm.gm);
     } else {
         OVS_NOT_REACHED();
     }
diff --git a/tests/ofproto-macros.at b/tests/ofproto-macros.at
index da49eb2..8234341 100644
--- a/tests/ofproto-macros.at
+++ b/tests/ofproto-macros.at
@@ -14,6 +14,8 @@  s/ idle_age=[0-9]*,//
 s/ hard_age=[0-9]*,//
 s/dp_hash=0x[0-9a-f]*\//dp_hash=0x0\//
 s/recirc_id=0x[0-9a-f]*,/recirc_id=0x0,/
+s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z|//
+s/dir\/[0-9]*\/br0.mgmt/dir\/XXXX\/br0.mgmt/
 '
 }
 
diff --git a/tests/ofproto.at b/tests/ofproto.at
index e5fb54c..95753d4 100644
--- a/tests/ofproto.at
+++ b/tests/ofproto.at
@@ -393,7 +393,7 @@  AT_CHECK([ovs-ofctl -O OpenFlow11 -vwarn add-group br0 'group_id=1236,type=indir
 OVS_VSWITCHD_STOP
 AT_CLEANUP
 
-AT_SETUP([ofproto - group_mod with mod and add_or_mod command])
+AT_SETUP([ofproto - group mod with mod and add_or_mod command])
 OVS_VSWITCHD_START
 dnl Check that mod-group for non-existing group fails without --may-create
 AT_DATA([stderr], [dnl
@@ -500,7 +500,7 @@  AT_CLEANUP
 dnl This is really bare-bones.
 dnl It at least checks request and reply serialization and deserialization.
 dnl Actions definition listed in both supported formats (w/ actions=)
-AT_SETUP([ofproto - insert buckets])
+AT_SETUP([ofproto - insert group buckets])
 OVS_VSWITCHD_START
 # Add group with no buckets.
 AT_DATA([groups.txt], [dnl
@@ -596,7 +596,7 @@  AT_CLEANUP
 dnl This is really bare-bones.
 dnl It at least checks request and reply serialization and deserialization.
 dnl Actions definition listed in both supported formats (w/ actions=)
-AT_SETUP([ofproto - remove buckets])
+AT_SETUP([ofproto - remove group buckets])
 OVS_VSWITCHD_START
 AT_DATA([groups.txt], [dnl
 group_id=1234,type=all,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11,bucket=bucket_id:12,actions=output:12,bucket=bucket_id:13,actions=output:13,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15,bucket=bucket_id:16,actions=output:16
@@ -650,6 +650,315 @@  AT_CHECK([ovs-ofctl -O OpenFlow11 -vwarn remove-buckets br0 group_id=1234,comman
 OVS_VSWITCHD_STOP
 AT_CLEANUP
 
+AT_SETUP([ofproto - bundle del group (OpenFlow 1.3)])
+OVS_VSWITCHD_START
+AT_DATA([groups.txt], [dnl
+group_id=1234,type=all,bucket=output:10
+group_id=1235,type=all,bucket=actions=output:10
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow13 -vwarn add-groups br0 groups.txt])
+AT_CHECK([ovs-ofctl -O OpenFlow13 -vwarn dump-groups br0 ], [0], [stdout])
+AT_CHECK([strip_xids < stdout | sort], [0], [dnl
+ group_id=1234,type=all,bucket=actions=output:10
+ group_id=1235,type=all,bucket=actions=output:10
+OFPST_GROUP_DESC reply (OF1.3):
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow13 -vwarn del-groups br0 group_id=1234])
+AT_CHECK([ovs-ofctl -O OpenFlow13 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.3):
+ group_id=1235,type=all,bucket=actions=output:10
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow13 -vwarn del-groups br0 group_id=1234])
+AT_CHECK([ovs-ofctl -O OpenFlow13 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.3):
+ group_id=1235,type=all,bucket=actions=output:10
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow13 -vwarn del-groups br0], [0])
+AT_CHECK([ovs-ofctl -O OpenFlow13 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.3):
+])
+
+# Negative test.
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow13 -vwarn del-groups br0 group_id=0xfffffff0],
+  [1], [], [ovs-ofctl: invalid group id 4294967280
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([ofproto - bundle add indirect group])
+OVS_VSWITCHD_START
+dnl indirect group must have exactly one bucket
+AT_DATA([stderr], [dnl
+OFPT_ERROR (OF1.4) (xid=0x2): OFPGMFC_INVALID_GROUP
+OFPT_BUNDLE_ADD_MESSAGE (OF1.4) (xid=0x2):
+ bundle_id=0 flags=atomic ordered
+OFPT_GROUP_MOD (OF1.4) (xid=0x2): ***decode error: OFPGMFC_INVALID_GROUP***
+])
+AT_CHECK([ovs-ofctl --bundle -vwarn add-group br0 'group_id=1234,type=indirect'], [1], , [stderr])
+AT_CHECK([ovs-ofctl --bundle -vwarn add-group br0 'group_id=1235,type=indirect,bucket=output:10'])
+AT_CHECK([ovs-ofctl --bundle -vwarn add-group br0 'group_id=1236,type=indirect,bucket=output:10,bucket=output:11'], [1], , [stderr])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([ofproto - bundle group mod with mod and add_or_mod command])
+OVS_VSWITCHD_START
+dnl Check that mod-group for non-existing group fails without --may-create
+AT_DATA([stderr], [dnl
+OFPT_ERROR (OF1.4) (xid=0x2): OFPGMFC_UNKNOWN_GROUP
+OFPT_GROUP_MOD (OF1.4) (xid=0x2):
+ MOD group_id=1234,type=indirect,bucket=actions=output:2
+])
+AT_CHECK([ovs-ofctl --bundle -vwarn mod-group br0 'group_id=1234,type=indirect,bucket=actions=2'], [1], , [stderr])
+dnl Check that mod-group for non-existing group succeeds with --may-create
+AT_CHECK([ovs-ofctl --bundle -vwarn --may-create mod-group br0 'group_id=1234,type=indirect,bucket=actions=2'])
+AT_CHECK([ovs-ofctl -O OpenFlow14 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.4):
+ group_id=1234,type=indirect,bucket=actions=output:2
+])
+dnl Check that mod-group for existing group succeeds with --may-create
+AT_CHECK([ovs-ofctl --bundle -vwarn --may-create mod-group br0 'group_id=1234,type=indirect,bucket=actions=3'])
+AT_CHECK([ovs-ofctl -O OpenFlow14 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.4):
+ group_id=1234,type=indirect,bucket=actions=output:3
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([ofproto - bundle del group (OpenFlow 1.5)])
+OVS_VSWITCHD_START
+AT_DATA([groups.txt], [dnl
+group_id=1233,type=select,selection_method=hash,bucket=output:10,bucket=output:11
+group_id=1234,type=select,selection_method=hash,fields(eth_dst,ip_dst,tcp_dst),bucket=output:10,bucket=output:11
+group_id=1235,type=all,bucket=actions=output:12,bucket=bucket_id:0,actions=output:10,bucket=bucket_id:1,actions=output:11
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn add-groups br0 groups.txt])
+AT_CHECK([ovs-ofctl -F OXM-OpenFlow15 -O OpenFlow15 -vwarn dump-groups br0 1234], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=select,selection_method=hash,fields(eth_dst,ip_dst,tcp_dst),bucket=bucket_id:0,actions=output:10,bucket=bucket_id:1,actions=output:11
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn del-groups br0 group_id=1234])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout | sort], [0], [dnl
+ group_id=1233,type=select,selection_method=hash,bucket=bucket_id:0,actions=output:10,bucket=bucket_id:1,actions=output:11
+ group_id=1235,type=all,bucket=bucket_id:2,actions=output:12,bucket=bucket_id:0,actions=output:10,bucket=bucket_id:1,actions=output:11
+OFPST_GROUP_DESC reply (OF1.5):
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn del-groups br0 group_id=1234])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout | sort], [0], [dnl
+ group_id=1233,type=select,selection_method=hash,bucket=bucket_id:0,actions=output:10,bucket=bucket_id:1,actions=output:11
+ group_id=1235,type=all,bucket=bucket_id:2,actions=output:12,bucket=bucket_id:0,actions=output:10,bucket=bucket_id:1,actions=output:11
+OFPST_GROUP_DESC reply (OF1.5):
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn del-groups br0], [0])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([ofproto - bundle del group deletes flows])
+OVS_VSWITCHD_START
+AT_DATA([groups.txt], [dnl
+group_id=1234,type=all,bucket=output:10
+group_id=1235,type=all,bucket=output:10
+])
+AT_CHECK([ovs-ofctl --bundle -vwarn add-groups br0 groups.txt])
+AT_DATA([flows.txt], [dnl
+tcp actions=group:1234
+table=2 udp actions=group:1235
+])
+AT_CHECK([ovs-ofctl --bundle -vwarn add-flows br0 flows.txt])
+AT_CHECK([ovs-ofctl -O OpenFlow14 -vwarn dump-flows br0 | ofctl_strip | sort],
+[0], [dnl
+ table=2, udp actions=group:1235
+ tcp actions=group:1234
+OFPST_FLOW reply (OF1.4):
+])
+AT_CHECK([ovs-ofctl --bundle -vwarn del-groups br0 group_id=1234])
+AT_CHECK([ovs-ofctl -O OpenFlow14 -vwarn dump-flows br0 | ofctl_strip | sort],
+[0], [dnl
+ table=2, udp actions=group:1235
+OFPST_FLOW reply (OF1.4):
+])
+AT_CHECK([ovs-ofctl --bundle -vwarn del-groups br0 group_id=1234])
+AT_CHECK([ovs-ofctl -O OpenFlow14 -vwarn dump-flows br0 | ofctl_strip | sort],
+[0], [dnl
+ table=2, udp actions=group:1235
+OFPST_FLOW reply (OF1.4):
+])
+AT_CHECK([ovs-ofctl --bundle -vwarn del-groups br0])
+AT_CHECK([ovs-ofctl -O OpenFlow14 -vwarn dump-flows br0 | ofctl_strip | sort],
+[0], [dnl
+OFPST_FLOW reply (OF1.4):
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+dnl This is really bare-bones.
+dnl It at least checks request and reply serialization and deserialization.
+dnl Actions definition listed in both supported formats (w/ actions=)
+AT_SETUP([ofproto - bundle insert group buckets])
+OVS_VSWITCHD_START
+# Add group with no buckets.
+AT_DATA([groups.txt], [dnl
+group_id=1234,type=all
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn add-groups br0 groups.txt])
+AT_CHECK([ovs-ofctl -F OXM-OpenFlow15 -O OpenFlow15 -vwarn dump-groups br0 1234], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all
+])
+
+# Add two buckets, using "last".
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn insert-buckets br0 group_id=1234,command_bucket_id=last,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11])
+AT_CHECK([ovs-ofctl -F OXM-OpenFlow15 -O OpenFlow15 -vwarn dump-groups br0 1234], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11
+])
+
+# Start over again, then add two buckets using "first".
+AT_DATA([groups.txt], [dnl
+delete group_id=1234
+add group_id=1234,type=all
+insert_bucket group_id=1234,command_bucket_id=first,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn add-groups br0 groups.txt])
+AT_CHECK([ovs-ofctl -F OXM-OpenFlow15 -O OpenFlow15 -vwarn dump-groups br0 1234], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11
+])
+
+# Add two more buckets before the existing ones.
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn insert-buckets br0 group_id=1234,command_bucket_id=first,bucket=bucket_id:0,actions=output:0,bucket=bucket_id:1,actions=output:1])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:0,actions=output:0,bucket=bucket_id:1,actions=output:1,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11
+])
+
+# Add another bucket at the end.
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn insert-buckets br0 group_id=1234,command_bucket_id=last,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:0,actions=output:0,bucket=bucket_id:1,actions=output:1,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15
+])
+
+# Verify that duplicate bucket IDs are rejected.
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn insert-buckets br0 group_id=1234,command_bucket_id=last,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15], [1], [], [stderr])
+AT_CHECK([strip_xids < stderr | sed '/truncated/,$d'], [0], [dnl
+OFPT_ERROR (OF1.5): OFPGMFC_BUCKET_EXISTS
+OFPT_GROUP_MOD (OF1.5):
+])
+
+# Add another bucket just before bucket 15.
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn insert-buckets br0 group_id=1234,command_bucket_id=15])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:0,actions=output:0,bucket=bucket_id:1,actions=output:1,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15
+])
+
+# Add two more buckets just before bucket 11,
+# getting the command from a file.
+AT_DATA([buckets.txt], [dnl
+group_id=1234,command_bucket_id=11,bucket=bucket_id:12,actions=output:12,bucket=bucket_id:13,actions=output:13
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn insert-buckets br0 - < buckets.txt])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:0,actions=output:0,bucket=bucket_id:1,actions=output:1,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11,bucket=bucket_id:12,actions=output:12,bucket=bucket_id:13,actions=output:13,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15
+])
+
+# Add yet two more buckets.
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn insert-buckets br0 group_id=1234,command_bucket_id=15,bucket=bucket_id:20,actions=output:20,bucket=bucket_id:21,actions=output:21])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:0,actions=output:0,bucket=bucket_id:1,actions=output:1,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11,bucket=bucket_id:12,actions=output:12,bucket=bucket_id:13,actions=output:13,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15,bucket=bucket_id:20,actions=output:20,bucket=bucket_id:21,actions=output:21
+])
+
+# Negative tests.
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn insert-buckets br0 group_id=1234,command_bucket_id=0xffffff01,bucket=bucket_id:0,actions=output:0,bucket=bucket_id:1,actions=output:1], [1], [],
+  [ovs-ofctl: invalid command bucket id 4294967041
+])
+AT_CHECK([ovs-ofctl --bundle -vwarn insert-buckets br0 group_id=1234,command_bucket_id=first,bucket=bucket_id:0,actions=output:0,bucket=bucket_id:1,actions=output:1], [1], [],
+  [ovs-ofctl: insert-bucket needs OpenFlow 1.5 or later ('-O OpenFlow15')
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([ofproto - bundle remove group buckets])
+OVS_VSWITCHD_START
+AT_DATA([groups.txt], [dnl
+group_id=1234,type=all,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11,bucket=bucket_id:12,actions=output:12,bucket=bucket_id:13,actions=output:13,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15,bucket=bucket_id:16,actions=output:16
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn add-groups br0 groups.txt])
+AT_CHECK([ovs-ofctl -F OXM-OpenFlow15 -O OpenFlow15 -vwarn dump-groups br0 1234], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:10,actions=output:10,bucket=bucket_id:11,actions=output:11,bucket=bucket_id:12,actions=output:12,bucket=bucket_id:13,actions=output:13,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15,bucket=bucket_id:16,actions=output:16
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn remove-buckets br0 group_id=1234,command_bucket_id=first])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:11,actions=output:11,bucket=bucket_id:12,actions=output:12,bucket=bucket_id:13,actions=output:13,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15,bucket=bucket_id:16,actions=output:16
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn remove-buckets br0 group_id=1234,command_bucket_id=last])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:11,actions=output:11,bucket=bucket_id:12,actions=output:12,bucket=bucket_id:13,actions=output:13,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn remove-buckets br0 group_id=1234,command_bucket_id=13])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all,bucket=bucket_id:11,actions=output:11,bucket=bucket_id:12,actions=output:12,bucket=bucket_id:14,actions=output:14,bucket=bucket_id:15,actions=output:15
+])
+AT_DATA([buckets.txt], [dnl
+group_id=1234
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn remove-buckets br0 - < buckets.txt])
+AT_CHECK([ovs-ofctl -O OpenFlow15 -vwarn dump-groups br0], [0], [stdout])
+AT_CHECK([strip_xids < stdout], [0], [dnl
+OFPST_GROUP_DESC reply (OF1.5):
+ group_id=1234,type=all
+])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn remove-buckets br0 group_id=1234,command_bucket_id=first])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn remove-buckets br0 group_id=1234,command_bucket_id=last])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn remove-buckets br0 group_id=1234,command_bucket_id=all])
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow15 -vwarn remove-buckets br0 group_id=1234,command_bucket_id=1], [1], [], [stderr])
+AT_CHECK([ofctl_strip < stderr], [0], [dnl
+OFPT_ERROR (OF1.5): OFPGMFC_UNKNOWN_BUCKET
+OFPT_GROUP_MOD (OF1.5):
+ REMOVE_BUCKET command_bucket_id:1,group_id=1234
+OFPT_ERROR (OF1.5): OFPBFC_MSG_FAILED
+OFPT_BUNDLE_CONTROL (OF1.5):
+ bundle_id=0 type=COMMIT_REQUEST flags=atomic ordered
+00001|vconn|WARN|Bundle commit failed (OFPBFC_MSG_FAILED).
+ovs-ofctl: talking to unix:/home/jrajahalme/openvswitch/tests/testsuite.dir/XXXX/br0.mgmt (Protocol error)
+])
+# Negative test.
+AT_CHECK([ovs-ofctl --bundle -O OpenFlow11 -vwarn remove-buckets br0 group_id=1234,command_bucket_id=last], [1], [],
+  [ovs-ofctl: remove-bucket needs OpenFlow 1.5 or later ('-O OpenFlow15')
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
 dnl This is really bare-bones.
 dnl It at least checks request and reply serialization and deserialization.
 AT_SETUP([ofproto - flow mod checks group availability])
@@ -678,6 +987,36 @@  OFPT_FLOW_MOD (OF1.1):
 OVS_VSWITCHD_STOP
 AT_CLEANUP
 
+AT_SETUP([ofproto - bundle flow mod checks group availability])
+OVS_VSWITCHD_START
+AT_DATA([bundle.txt], [dnl
+group add group_id=1234,type=all,bucket=output:10
+flow add tcp actions=group:1234
+flow add udp actions=group:1235
+])
+AT_CHECK([ovs-ofctl -vwarn bundle br0 bundle.txt], [1], [], [stderr])
+
+# The output should look like this:
+#
+# 00000000  02 0e 00 98 00 00 00 02-00 00 00 00 00 00 00 00 |................|
+# 00000010  00 00 00 00 00 00 00 00-ff 00 00 00 00 00 80 00 |................|
+# 00000020  ff ff ff ff ff ff ff ff-ff ff ff ff 00 00 00 00 |................|
+# 00000030  00 00 00 58 00 00 00 00-00 00 03 d7 00 00 00 00 |...X............|
+#
+# This 'sed' command captures the error message but drops details.
+AT_CHECK([sed '/truncated/d
+/^000000.0/d' stderr | ofctl_strip], [0],
+  [OFPT_ERROR (OF1.4): OFPBAC_BAD_OUT_GROUP
+OFPT_FLOW_MOD (OF1.4):
+OFPT_ERROR (OF1.4): OFPBFC_MSG_FAILED
+OFPT_BUNDLE_CONTROL (OF1.4):
+ bundle_id=0 type=COMMIT_REQUEST flags=atomic ordered
+00001|vconn|WARN|Bundle commit failed (OFPBFC_MSG_FAILED).
+ovs-ofctl: talking to unix:/home/jrajahalme/openvswitch/tests/testsuite.dir/XXXX/br0.mgmt (Protocol error)
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
 dnl This is really bare-bones.
 dnl It at least checks request and reply serialization and deserialization.
 AT_SETUP([ofproto - group description])
@@ -1580,6 +1919,56 @@  AT_CHECK([ovs-ofctl -O OpenFlow11 dump-flows br0 | ofctl_strip | sort], [0],
 OVS_VSWITCHD_STOP
 AT_CLEANUP
 
+AT_SETUP([ofproto - bundle flow_mod with out group matching (OpenFlow 1.4)])
+OVS_VSWITCHD_START
+AT_DATA([bundle.txt], [dnl
+group group_id=1,type=all,bucket=output:10
+group group_id=2,type=all,bucket=output:10
+group group_id=3,type=all,bucket=output:10
+flow in_port=1 actions=group:2,output:5
+flow in_port=2 actions=group:1,write_actions(group:2,group:3,output:6)
+flow in_port=3 actions=group:3,group:1,write_actions(group:2,output:3)
+flow in_port=4 actions=output:4
+flow in_port=5 actions=write_actions(output:4,group:3)
+flow in_port=6 actions=group:1
+])
+AT_CHECK([ovs-ofctl bundle br0 bundle.txt])
+
+# for checking
+AT_DATA([flows.txt], [dnl
+ in_port=1 actions=group:2,output:5
+ in_port=2 actions=group:1,write_actions(group:2,group:3,output:6)
+ in_port=3 actions=group:3,group:1,write_actions(group:2,output:3)
+ in_port=4 actions=output:4
+ in_port=5 actions=write_actions(output:4,group:3)
+ in_port=6 actions=group:1
+])
+
+(cat flows.txt; echo 'OFPST_FLOW reply (OF1.4):') > expout
+AT_CHECK([ovs-ofctl -O OpenFlow14 dump-flows br0 | ofctl_strip | sort], [0],
+  [expout])
+
+(grep 'output:3' flows.txt | grep 'group:2'; echo 'OFPST_FLOW reply (OF1.4):') > expout
+AT_CHECK([ovs-ofctl -O OpenFlow14 dump-flows br0 out_port=3,out_group=2 | ofctl_strip | sort], [0], [expout])
+
+AT_CHECK([ovs-ofctl -O OpenFlow14 del-flows br0 out_group=2])
+(grep -v 'group:2' flows.txt; echo 'OFPST_FLOW reply (OF1.4):') > expout
+AT_CHECK([ovs-ofctl -O OpenFlow14 dump-flows br0 | ofctl_strip | sort], [0],
+  [expout])
+
+AT_CHECK([ovs-ofctl -O OpenFlow14 del-flows br0 out_group=3])
+(grep -v 'group:[[23]]' flows.txt; echo 'OFPST_FLOW reply (OF1.4):') > expout
+AT_CHECK([ovs-ofctl -O OpenFlow14 dump-flows br0 | ofctl_strip | sort], [0],
+  [expout])
+
+AT_CHECK([ovs-ofctl -O OpenFlow14 del-flows br0 out_group=1])
+(grep -v 'group:[[123]]' flows.txt; echo 'OFPST_FLOW reply (OF1.4):') > expout
+AT_CHECK([ovs-ofctl -O OpenFlow14 dump-flows br0 | ofctl_strip | sort], [0],
+  [expout])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
 AT_SETUP([ofproto - flow table configuration (OpenFlow 1.0)])
 OVS_VSWITCHD_START
 # Check the default configuration.
diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in
index 6114f0f..38923ae 100644
--- a/utilities/ovs-ofctl.8.in
+++ b/utilities/ovs-ofctl.8.in
@@ -424,15 +424,25 @@  was generated by the switch itself.
 These commands manage the group table in an OpenFlow switch.  In each
 case, \fIgroup\fR specifies a group entry in the format described in
 \fBGroup Syntax\fR, below, and \fIfile\fR is a text file that contains
-zero or more groups in the same syntax, one per line.
+zero or more groups in the same syntax, one per line, and the optional
+\fB\-\-bundle\fR option operates the command as a single atomic
+transation, see option \fB\-\-bundle\fR, below.
 
-.IP "\fBadd\-group \fIswitch group\fR"
-.IQ "\fBadd\-group \fIswitch \fB\- < \fIfile\fR"
-.IQ "\fBadd\-groups \fIswitch file\fR"
+.IP "[\fB\-\-bundle\fR] \fBadd\-group \fIswitch group\fR"
+.IQ "[\fB\-\-bundle\fR] \fBadd\-group \fIswitch \fB\- < \fIfile\fR"
+.IQ "[\fB\-\-bundle\fR] \fBadd\-groups \fIswitch file\fR"
 Add each group entry to \fIswitch\fR's tables.
 .
-.IP "[\fB\-\-may\-create\fR] \fBmod\-group \fIswitch group\fR"
-.IQ "[\fB\-\-may\-create\fR] \fBmod\-group \fIswitch \fB\- < \fIfile\fR"
+Each group specification (e.g., each line in \fIfile\fR) may start
+with \fBadd\fR, \fBmodify\fR, \fBadd_or_mod\fR, \fBdelete\fR,
+\fBinsert_bucket\fR, or \fBremove_bucket\fR keyword to specify whether
+a flow is to be added, modified, or deleted, or whether a group bucket
+is to be added or removed.  For backwards compatibility a group
+specification without one of these keywords is treated as a group add.
+All group mods are executed in the order specified.
+.
+.IP "[\fB\-\-bundle\fR] [\fB\-\-may\-create\fR] \fBmod\-group \fIswitch group\fR"
+.IQ "[\fB\-\-bundle\fR] [\fB\-\-may\-create\fR] \fBmod\-group \fIswitch \fB\- < \fIfile\fR"
 Modify the action buckets in entries from \fIswitch\fR's tables for
 each group entry.  If a specified group does not already exist, then
 without \fB\-\-may\-create\fR, this command has no effect; with
@@ -440,25 +450,45 @@  without \fB\-\-may\-create\fR, this command has no effect; with
 \fB\-\-may\-create\fR option uses an Open vSwitch extension to
 OpenFlow only implemented in Open vSwitch 2.6 and later.
 .
-.IP "\fBdel\-groups \fIswitch\fR"
-.IQ "\fBdel\-groups \fIswitch \fR[\fIgroup\fR]"
-.IQ "\fBdel\-groups \fIswitch \fB\- < \fIfile\fR"
+.IP "[\fB\-\-bundle\fR] \fBdel\-groups \fIswitch\fR"
+.IQ "[\fB\-\-bundle\fR] \fBdel\-groups \fIswitch \fR[\fIgroup\fR]"
+.IQ "[\fB\-\-bundle\fR] \fBdel\-groups \fIswitch \fB\- < \fIfile\fR"
 Deletes entries from \fIswitch\fR's group table.  With only a
 \fIswitch\fR argument, deletes all groups.  Otherwise, deletes the group
 for each group entry.
 .
-.IP "\fBinsert\-buckets \fIswitch group\fR"
-.IQ "\fBinsert\-buckets \fIswitch \fB\- < \fIfile\fR"
+.IP "[\fB\-\-bundle\fR] \fBinsert\-buckets \fIswitch group\fR"
+.IQ "[\fB\-\-bundle\fR] \fBinsert\-buckets \fIswitch \fB\- < \fIfile\fR"
 Add buckets to an existing group present in the \fIswitch\fR's group table.
 If no \fIcommand_bucket_id\fR is present in the group specification then all
 buckets of the group are removed.
 .
-.IP "\fBremove\-buckets \fIswitch group\fR"
-.IQ "\fBremove\-buckets \fIswitch \fB\- < \fIfile\fR"
+.IP "[\fB\-\-bundle\fR] \fBremove\-buckets \fIswitch group\fR"
+.IQ "[\fB\-\-bundle\fR] \fBremove\-buckets \fIswitch \fB\- < \fIfile\fR"
 Remove buckets to an existing group present in the \fIswitch\fR's group table.
 If no \fIcommand_bucket_id\fR is present in the group specification then all
 buckets of the group are removed.
 .
+.SS OpenFlow Switch Bundle Command
+.
+Transactional updates to both flow and group tables can be made with
+the \fBbundle\fR command.  \fIfile\fR is a text file that contains
+zero or more flows and groups in either \fBFlow Syntax\fR or \fBGroup
+Syntax\fR, each line preceded by either \fBflow\fR or \fBgroup\fR
+keyword.  The \fBflow\fR keyword may be optionally followed by one of
+the keywords \fBadd\fR, \fBmodify\fR, \fBmodify_strict\fR,
+\fBdelete\fR, or \fBdelete_strict\fR, of which the \fBadd\fR is
+assumed if a bare \fBflow\fR is given.  Similarly, the \fBgroup\fR
+keyword may be optionally followed by one of the keywords \fBadd\fR,
+\fBmodify\fR, \fBadd_or_mod\fR, \fBdelete\fR, \fBinsert_bucket\fR, or
+\fBremove_bucket\fR, of which the \fBadd\fR is assumed if a bare
+\fBgroup\fR is given.
+.
+.IP "\fBbundle \fIswitch file\fR"
+Execute all flow and group mods in \fIfile\fR as a single atomic
+transaction against \fIswitch\fR's tables.  All bundled mods are
+executed in the order specified.
+.
 .SS "OpenFlow Switch Tunnel TLV Table Commands"
 .
 Open vSwitch maintains a mapping table between tunnel option TLVs (defined
diff --git a/utilities/ovs-ofctl.c b/utilities/ovs-ofctl.c
index b04c063..36797fd 100644
--- a/utilities/ovs-ofctl.c
+++ b/utilities/ovs-ofctl.c
@@ -2470,6 +2470,36 @@  ofctl_dump_ipfix_flow(struct ovs_cmdl_context *ctx)
 }
 
 static void
+bundle_group_mod__(const char *remote, struct ofputil_group_mod *gms,
+                   size_t n_gms, enum ofputil_protocol usable_protocols)
+{
+    enum ofputil_protocol protocol;
+    enum ofp_version version;
+    struct vconn *vconn;
+    struct ovs_list requests;
+    size_t i;
+
+    ovs_list_init(&requests);
+
+    /* Bundles need OpenFlow 1.3+. */
+    usable_protocols &= OFPUTIL_P_OF13_UP;
+    protocol = open_vconn_for_flow_mod(remote, &vconn, usable_protocols);
+    version = ofputil_protocol_to_ofp_version(protocol);
+
+    for (i = 0; i < n_gms; i++) {
+        struct ofputil_group_mod *gm = &gms[i];
+        struct ofpbuf *request = ofputil_encode_group_mod(version, gm);
+
+        ovs_list_push_back(&requests, &request->list_node);
+        ofputil_bucket_list_destroy(&gm->buckets);
+    }
+
+    bundle_transact(vconn, &requests, OFPBF_ORDERED | OFPBF_ATOMIC);
+    ofpbuf_list_delete(&requests);
+    vconn_close(vconn);
+}
+
+static void
 ofctl_group_mod__(const char *remote, struct ofputil_group_mod *gms,
                   size_t n_gms, enum ofputil_protocol usable_protocols)
 {
@@ -2481,40 +2511,44 @@  ofctl_group_mod__(const char *remote, struct ofputil_group_mod *gms,
     struct vconn *vconn;
     size_t i;
 
+    if (bundle) {
+        bundle_group_mod__(remote, gms, n_gms, usable_protocols);
+        return;
+    }
+
     protocol = open_vconn_for_flow_mod(remote, &vconn, usable_protocols);
     version = ofputil_protocol_to_ofp_version(protocol);
 
     for (i = 0; i < n_gms; i++) {
         gm = &gms[i];
         request = ofputil_encode_group_mod(version, gm);
-        if (request) {
-            transact_noreply(vconn, request);
-        }
+        transact_noreply(vconn, request);
+        ofputil_bucket_list_destroy(&gm->buckets);
     }
 
     vconn_close(vconn);
-
 }
 
-
 static void
-ofctl_group_mod_file(int argc OVS_UNUSED, char *argv[], uint16_t command)
+ofctl_group_mod_file(int argc OVS_UNUSED, char *argv[], int command)
 {
     struct ofputil_group_mod *gms = NULL;
     enum ofputil_protocol usable_protocols;
     size_t n_gms = 0;
     char *error;
-    int i;
 
+    if (command == OFPGC11_ADD) {
+        /* Allow the file to specify a mix of commands.  If none specified at
+         * the beginning of any given line, then the default is OFPGC11_ADD, so
+         * this is backwards compatible. */
+        command = -2;
+    }
     error = parse_ofp_group_mod_file(argv[2], command, &gms, &n_gms,
                                      &usable_protocols);
     if (error) {
         ovs_fatal(0, "%s", error);
     }
     ofctl_group_mod__(argv[1], gms, n_gms, usable_protocols);
-    for (i = 0; i < n_gms; i++) {
-        ofputil_bucket_list_destroy(&gms[i].buckets);
-    }
     free(gms);
 }
 
@@ -2534,7 +2568,6 @@  ofctl_group_mod(int argc, char *argv[], uint16_t command)
             ovs_fatal(0, "%s", error);
         }
         ofctl_group_mod__(argv[1], &gm, 1, usable_protocols);
-        ofputil_bucket_list_destroy(&gm.buckets);
     }
 }
 
@@ -2644,6 +2677,48 @@  ofctl_dump_group_features(struct ovs_cmdl_context *ctx)
 }
 
 static void
+ofctl_bundle(struct ovs_cmdl_context *ctx)
+{
+    enum ofputil_protocol protocol, usable_protocols;
+    struct ofputil_bundle_msg *bms;
+    struct ovs_list requests;
+    struct vconn *vconn;
+    size_t n_bms;
+    char *error;
+
+    error = parse_ofp_bundle_file(ctx->argv[2], &bms, &n_bms,
+                                  &usable_protocols);
+    if (error) {
+        ovs_fatal(0, "%s", error);
+    }
+
+    /* Implicit OpenFlow 1.4. */
+    if (!(get_allowed_ofp_versions() &
+          ofputil_protocols_to_version_bitmap(OFPUTIL_P_OF13_UP))) {
+
+        /* Add implicit allowance for OpenFlow 1.4. */
+        add_allowed_ofp_versions(ofputil_protocols_to_version_bitmap(
+                                     OFPUTIL_P_OF14_OXM));
+        /* Remove all versions that do not support bundles. */
+        mask_allowed_ofp_versions(ofputil_protocols_to_version_bitmap(
+                                     OFPUTIL_P_OF13_UP));
+        allowed_protocols = ofputil_protocols_from_version_bitmap(
+                                     get_allowed_ofp_versions());
+    }
+
+    /* Bundles need OpenFlow 1.3+. */
+    usable_protocols &= OFPUTIL_P_OF13_UP;
+    protocol = open_vconn_for_flow_mod(ctx->argv[1], &vconn, usable_protocols);
+
+    ovs_list_init(&requests);
+    ofputil_encode_bundle_msgs(bms, n_bms, &requests, protocol);
+    bundle_transact(vconn, &requests, OFPBF_ORDERED | OFPBF_ATOMIC);
+    ofpbuf_list_delete(&requests);
+
+    vconn_close(vconn);
+}
+
+static void
 ofctl_tlv_mod(struct ovs_cmdl_context *ctx, uint16_t command)
 {
     enum ofputil_protocol usable_protocols;
@@ -4109,6 +4184,10 @@  static const struct ovs_cmdl_command all_commands[] = {
       1, 2, ofctl_dump_group_stats },
     { "dump-group-features", "switch",
       1, 1, ofctl_dump_group_features },
+
+    { "bundle", "switch file",
+      2, 2, ofctl_bundle },
+
     { "add-tlv-map", "switch map",
       2, 2, ofctl_add_tlv_map },
     { "del-tlv-map", "switch [map]",