]> git.proxmox.com Git - mirror_ovs.git/commitdiff
ovn: Change strategy for tunnel keys.
authorBen Pfaff <blp@nicira.com>
Mon, 3 Aug 2015 23:38:12 +0000 (16:38 -0700)
committerBen Pfaff <blp@nicira.com>
Mon, 3 Aug 2015 23:39:02 +0000 (16:39 -0700)
Until now, OVN has used "flat" tunnel keys, in which the STT tunnel key or
Geneve VNI contains a logical port number.  Logical port numbers are unique
within an OVN deployment.

Flat tunnel keys have the advantage of simplicity.  However, for packets
that are destined to logical ports on multiple hypervisors, they require
sending one packet per destination logical port rather than one packet per
hypervisor.  They also make it hard to integrate with VXLAN-based hardware
switches, which use VNIs to designate logical networks instead of logical
ports.

This commit switches OVN to a different scheme.  In this scheme, in Geneve
the VNI designates a logical network and a Geneve option specifies the
logical input and output ports, which are now scoped within the logical
network rather than globally unique.  In STT, all three identifiers are
encoded in the tunnel key.

To allow for the reduced amount of traffic for packets destined to logical
ports on multiple hypervisors, this commit also introduces the concept
of a logical multicast group.  The membership of these groups can be set
using a new Multicast_Group table in the southbound database (and
ovn-northd does use it starting in this commit).

With multicast groups alone, it would be difficult to implement ACLs,
because an ACL might disallow only some of the packets being sent to
a remote hypervisor.  Thus, this commit also splits the OVN logical
pipeline into two pipelines: the "ingress" pipeline, which makes the
decision about the logical destination of a packet as a set of logical
ports or multicast groups, and the "egress" pipeline, which runs on the
destination hypervisor with the multicast group destination exploded into
individual ports and makes a final decision on whether to deliver the
packet.  The "egress" pipeline can efficiently apply ACLs.

Until now, the OVN logical and physical pipeline implementation was not
adequately documented.  This commit adds extensive documentation to
the OVN manpages to cover these issues.

Signed-off-by: Ben Pfaff <blp@nicira.com>
Acked-by: Justin Pettit <jpettit@nicira.com>
ovn/TODO
ovn/controller/lflow.c
ovn/controller/lflow.h
ovn/controller/ovn-controller.c
ovn/controller/physical.c
ovn/controller/physical.h
ovn/northd/ovn-northd.c
ovn/ovn-architecture.7.xml
ovn/ovn-nb.xml
ovn/ovn-sb.ovsschema
ovn/ovn-sb.xml

index e46a7bcea6d1e6c50bac2e7dcd8272881df0b750..509e5d00445abe1b729984c8f47473401e3fff0f 100644 (file)
--- a/ovn/TODO
+++ b/ovn/TODO
@@ -1,24 +1,5 @@
 * ovn-controller
 
-*** Determine how to split logical pipeline across physical nodes.
-
-    From the original OVN architecture document:
-
-    The pipeline processing is split between the ingress and egress
-    transport nodes.  In particular, the logical egress processing may
-    occur at either hypervisor.  Processing the logical egress on the
-    ingress hypervisor requires more state about the egress vif's
-    policies, but reduces traffic on the wire that would eventually be
-    dropped.  Whereas, processing on the egress hypervisor can reduce
-    broadcast traffic on the wire by doing local replication.  We
-    initially plan to process logical egress on the egress hypervisor
-    so that less state needs to be replicated.  However, we may change
-    this behavior once we gain some experience writing the logical
-    flows.
-
-    The split pipeline processing split will influence how tunnel keys
-    are encoded.
-
 ** ovn-controller parameters and configuration.
 
 *** SSL configuration.
index 2793293c5862610b82b94b56fcbf1d6a71f36ec5..9246e61de0dc9bd84ab5d1fa19135d6bc8b34a1f 100644 (file)
@@ -33,6 +33,15 @@ VLOG_DEFINE_THIS_MODULE(lflow);
 /* Contains "struct expr_symbol"s for fields supported by OVN lflows. */
 static struct shash symtab;
 
+static void
+add_logical_register(struct shash *symtab, enum mf_field_id id)
+{
+    char name[8];
+
+    snprintf(name, sizeof name, "reg%d", id - MFF_REG0);
+    expr_symtab_add_field(symtab, name, id, NULL, false);
+}
+
 static void
 symtab_init(void)
 {
@@ -44,16 +53,10 @@ symtab_init(void)
     expr_symtab_add_string(&symtab, "inport", MFF_LOG_INPORT, NULL);
     expr_symtab_add_string(&symtab, "outport", MFF_LOG_OUTPORT, NULL);
 
-    /* Registers.  We omit the registers that would otherwise overlap the
-     * reserved fields. */
-    for (enum mf_field_id id = MFF_REG0; id < MFF_REG0 + FLOW_N_REGS; id++) {
-        if (id != MFF_LOG_INPORT && id != MFF_LOG_OUTPORT) {
-            char name[8];
-
-            snprintf(name, sizeof name, "reg%d", id - MFF_REG0);
-            expr_symtab_add_field(&symtab, name, id, NULL, false);
-        }
-    }
+    /* Logical registers. */
+#define MFF_LOG_REG(ID) add_logical_register(&symtab, ID);
+    MFF_LOG_REGS;
+#undef MFF_LOG_REG
 
     /* Data fields. */
     expr_symtab_add_field(&symtab, "eth.src", MFF_ETH_SRC, NULL, false);
@@ -134,68 +137,56 @@ symtab_init(void)
 /* Logical datapaths and logical port numbers. */
 
 /* A logical datapath.
- *
- * 'uuid' is the UUID that represents the logical datapath in the OVN_SB
- * database.
- *
- * 'integer' represents the logical datapath as an integer value that is unique
- * only within the local hypervisor.  Because of its size, this value is more
- * practical for use in an OpenFlow flow table than a UUID.
  *
  * 'ports' maps 'logical_port' names to 'tunnel_key' values in the OVN_SB
  * Port_Binding table within the logical datapath. */
 struct logical_datapath {
     struct hmap_node hmap_node; /* Indexed on 'uuid'. */
-    struct uuid uuid;           /* The logical_datapath's UUID. */
-    uint32_t integer;           /* Locally unique among logical datapaths. */
+    struct uuid uuid;           /* UUID from Datapath_Binding row. */
+    uint32_t tunnel_key;        /* 'tunnel_key' from Datapath_Binding row. */
     struct simap ports;         /* Logical port name to port number. */
 };
 
 /* Contains "struct logical_datapath"s. */
 static struct hmap logical_datapaths = HMAP_INITIALIZER(&logical_datapaths);
 
-/* Finds and returns the logical_datapath with the given 'uuid', or NULL if
- * no such logical_datapath exists. */
+/* Finds and returns the logical_datapath for 'binding', or NULL if no such
+ * logical_datapath exists. */
 static struct logical_datapath *
-ldp_lookup(const struct uuid *uuid)
+ldp_lookup(const struct sbrec_datapath_binding *binding)
 {
     struct logical_datapath *ldp;
-    HMAP_FOR_EACH_IN_BUCKET (ldp, hmap_node, uuid_hash(uuid),
+    HMAP_FOR_EACH_IN_BUCKET (ldp, hmap_node, uuid_hash(&binding->header_.uuid),
                              &logical_datapaths) {
-        if (uuid_equals(&ldp->uuid, uuid)) {
+        if (uuid_equals(&ldp->uuid, &binding->header_.uuid)) {
             return ldp;
         }
     }
     return NULL;
 }
 
-/* Finds and returns the integer value corresponding to the given 'uuid', or 0
- * if no such logical datapath exists. */
-uint32_t
-ldp_to_integer(const struct uuid *logical_datapath)
-{
-    const struct logical_datapath *ldp = ldp_lookup(logical_datapath);
-    return ldp ? ldp->integer : 0;
-}
-
-/* Creates a new logical_datapath with the given 'uuid'. */
+/* Creates a new logical_datapath for the given 'binding'. */
 static struct logical_datapath *
-ldp_create(const struct uuid *uuid)
+ldp_create(const struct sbrec_datapath_binding *binding)
 {
-    static uint32_t next_integer = 1;
     struct logical_datapath *ldp;
 
-    /* We don't handle the case where the logical datapaths wrap around. */
-    ovs_assert(next_integer);
-
     ldp = xmalloc(sizeof *ldp);
-    hmap_insert(&logical_datapaths, &ldp->hmap_node, uuid_hash(uuid));
-    ldp->uuid = *uuid;
-    ldp->integer = next_integer++;
+    hmap_insert(&logical_datapaths, &ldp->hmap_node,
+                uuid_hash(&binding->header_.uuid));
+    ldp->uuid = binding->header_.uuid;
+    ldp->tunnel_key = binding->tunnel_key;
     simap_init(&ldp->ports);
     return ldp;
 }
 
+static struct logical_datapath *
+ldp_lookup_or_create(const struct sbrec_datapath_binding *binding)
+{
+    struct logical_datapath *ldp = ldp_lookup(binding);
+    return ldp ? ldp : ldp_create(binding);
+}
+
 static void
 ldp_free(struct logical_datapath *ldp)
 {
@@ -205,7 +196,8 @@ ldp_free(struct logical_datapath *ldp)
 }
 
 /* Iterates through all of the records in the Port_Binding table, updating the
- * table of logical_datapaths to match the values found in active Bindings. */
+ * table of logical_datapaths to match the values found in active
+ * Port_Bindings. */
 static void
 ldp_run(struct controller_ctx *ctx)
 {
@@ -216,16 +208,17 @@ ldp_run(struct controller_ctx *ctx)
 
     const struct sbrec_port_binding *binding;
     SBREC_PORT_BINDING_FOR_EACH (binding, ctx->ovnsb_idl) {
-        struct logical_datapath *ldp;
-
-        ldp = ldp_lookup(&binding->logical_datapath);
-        if (!ldp) {
-            ldp = ldp_create(&binding->logical_datapath);
-        }
+        struct logical_datapath *ldp = ldp_lookup_or_create(binding->datapath);
 
         simap_put(&ldp->ports, binding->logical_port, binding->tunnel_key);
     }
 
+    const struct sbrec_multicast_group *mc;
+    SBREC_MULTICAST_GROUP_FOR_EACH (mc, ctx->ovnsb_idl) {
+        struct logical_datapath *ldp = ldp_lookup_or_create(mc->datapath);
+        simap_put(&ldp->ports, mc->name, mc->tunnel_key);
+    }
+
     struct logical_datapath *next_ldp;
     HMAP_FOR_EACH_SAFE (ldp, next_ldp, hmap_node, &logical_datapaths) {
         if (simap_is_empty(&ldp->ports)) {
@@ -250,9 +243,7 @@ lflow_init(void)
 }
 
 /* Translates logical flows in the Logical_Flow table in the OVN_SB database
- * into OpenFlow flows, adding the OpenFlow flows to 'flow_table'.
- *
- * We put the logical flows into OpenFlow tables 16 through 47 (inclusive). */
+ * into OpenFlow flows.  See ovn-architecture(7) for more information. */
 void
 lflow_run(struct controller_ctx *ctx, struct hmap *flow_table)
 {
@@ -268,22 +259,33 @@ lflow_run(struct controller_ctx *ctx, struct hmap *flow_table)
          * no logical ports are bound to that logical datapath, so there's no
          * point in maintaining any flows for it anyway, so skip it. */
         const struct logical_datapath *ldp;
-        ldp = ldp_lookup(&lflow->logical_datapath);
+        ldp = ldp_lookup(lflow->logical_datapath);
         if (!ldp) {
             continue;
         }
 
-        /* Translate OVN actions into OpenFlow actions. */
+        /* Translate logical table ID to physical table ID. */
+        bool ingress = !strcmp(lflow->pipeline, "ingress");
+        uint8_t phys_table = lflow->table_id + (ingress
+                                                ? OFTABLE_LOG_INGRESS_PIPELINE
+                                                : OFTABLE_LOG_EGRESS_PIPELINE);
+        uint8_t next_phys_table
+            = lflow->table_id + 1 < LOG_PIPELINE_LEN ? phys_table + 1 : 0;
+        uint8_t output_phys_table = (ingress
+                                     ? OFTABLE_REMOTE_OUTPUT
+                                     : OFTABLE_LOG_TO_PHY);
+        /* Translate OVN actions into OpenFlow actions.
+         *
+         * XXX Deny changes to 'outport' in egress pipeline. */
         uint64_t ofpacts_stub[64 / 8];
         struct ofpbuf ofpacts;
         struct expr *prereqs;
-        uint8_t next_table_id;
         char *error;
 
         ofpbuf_use_stub(&ofpacts, ofpacts_stub, sizeof ofpacts_stub);
-        next_table_id = lflow->table_id < 31 ? lflow->table_id + 17 : 0;
         error = actions_parse_string(lflow->actions, &symtab, &ldp->ports,
-                                     next_table_id, 64, &ofpacts, &prereqs);
+                                     next_phys_table, output_phys_table,
+                                     &ofpacts, &prereqs);
         if (error) {
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
             VLOG_WARN_RL(&rl, "error parsing actions \"%s\": %s",
@@ -322,13 +324,13 @@ lflow_run(struct controller_ctx *ctx, struct hmap *flow_table)
         /* Prepare the OpenFlow matches for adding to the flow table. */
         struct expr_match *m;
         HMAP_FOR_EACH (m, hmap_node, &matches) {
-            match_set_metadata(&m->match, htonll(ldp->integer));
+            match_set_metadata(&m->match, htonll(ldp->tunnel_key));
             if (m->match.wc.masks.conj_id) {
                 m->match.flow.conj_id += conj_id_ofs;
             }
             if (!m->n) {
-                ofctrl_add_flow(flow_table, lflow->table_id + 16,
-                                lflow->priority, &m->match, &ofpacts);
+                ofctrl_add_flow(flow_table, phys_table, lflow->priority,
+                                &m->match, &ofpacts);
             } else {
                 uint64_t conj_stubs[64 / 8];
                 struct ofpbuf conj;
@@ -343,8 +345,8 @@ lflow_run(struct controller_ctx *ctx, struct hmap *flow_table)
                     dst->clause = src->clause;
                     dst->n_clauses = src->n_clauses;
                 }
-                ofctrl_add_flow(flow_table, lflow->table_id + 16,
-                                lflow->priority, &m->match, &conj);
+                ofctrl_add_flow(flow_table, phys_table, lflow->priority,
+                                &m->match, &conj);
                 ofpbuf_uninit(&conj);
             }
         }
index 4ee4b1f918671066d8f67b6fe6f07cf179b50122..59fe5599a348be6faa6f696be43075b4832c47fe 100644 (file)
@@ -38,15 +38,39 @@ struct controller_ctx;
 struct hmap;
 struct uuid;
 
+/* OpenFlow table numbers.
+ *
+ * These are heavily documented in ovn-architecture(7), please update it if
+ * you make any changes. */
+#define OFTABLE_PHY_TO_LOG            0
+#define OFTABLE_LOG_INGRESS_PIPELINE 16 /* First of LOG_PIPELINE_LEN tables. */
+#define OFTABLE_REMOTE_OUTPUT        32
+#define OFTABLE_LOCAL_OUTPUT         33
+#define OFTABLE_DROP_LOOPBACK        34
+#define OFTABLE_LOG_EGRESS_PIPELINE  48 /* First of LOG_PIPELINE_LEN tables. */
+#define OFTABLE_LOG_TO_PHY           64
+
+/* The number of tables for the ingress and egress pipelines. */
+#define LOG_PIPELINE_LEN 16
+
 /* Logical fields. */
 #define MFF_LOG_DATAPATH MFF_METADATA /* Logical datapath (64 bits). */
 #define MFF_LOG_INPORT   MFF_REG6     /* Logical input port (32 bits). */
 #define MFF_LOG_OUTPORT  MFF_REG7     /* Logical output port (32 bits). */
 
+/* Logical registers.
+ *
+ * Make sure these don't overlap with the logical fields! */
+#define MFF_LOG_REGS \
+    MFF_LOG_REG(MFF_REG0) \
+    MFF_LOG_REG(MFF_REG1) \
+    MFF_LOG_REG(MFF_REG2) \
+    MFF_LOG_REG(MFF_REG3) \
+    MFF_LOG_REG(MFF_REG4) \
+    MFF_LOG_REG(MFF_REG5)
+
 void lflow_init(void);
 void lflow_run(struct controller_ctx *, struct hmap *flow_table);
 void lflow_destroy(void);
 
-uint32_t ldp_to_integer(const struct uuid *logical_datapath);
-
 #endif /* ovn/lflow.h */
index 81d5812d879fdfc894eda6108db5666bf914aaf2..2c4248f8c1553d1393fa36a17c78cc125a14d83d 100644 (file)
@@ -279,12 +279,13 @@ main(int argc, char *argv[])
         }
 
         if (br_int) {
-            ofctrl_run(br_int);
+            enum mf_field_id mff_ovn_geneve = ofctrl_run(br_int);
 
             struct hmap flow_table = HMAP_INITIALIZER(&flow_table);
             lflow_run(&ctx, &flow_table);
             if (chassis_id) {
-                physical_run(&ctx, br_int, chassis_id, &flow_table);
+                physical_run(&ctx, mff_ovn_geneve,
+                             br_int, chassis_id, &flow_table);
             }
             ofctrl_put(&flow_table);
             hmap_destroy(&flow_table);
index 5f3466cf406170957b52bfdd9e95c9cd9af21624..ff6cf578ba1da13ffae34b5738c91a0d521f1f73 100644 (file)
 #include "ofpbuf.h"
 #include "ovn-controller.h"
 #include "ovn/lib/ovn-sb-idl.h"
+#include "openvswitch/vlog.h"
 #include "simap.h"
+#include "sset.h"
 #include "vswitch-idl.h"
 
+VLOG_DEFINE_THIS_MODULE(physical);
+
 void
 physical_register_ovs_idl(struct ovsdb_idl *ovs_idl)
 {
@@ -42,12 +46,98 @@ physical_register_ovs_idl(struct ovsdb_idl *ovs_idl)
     ovsdb_idl_add_column(ovs_idl, &ovsrec_interface_col_external_ids);
 }
 
+/* Maps from a chassis to the OpenFlow port number of the tunnel that can be
+ * used to reach that chassis. */
+struct chassis_tunnel {
+    struct hmap_node hmap_node;
+    const char *chassis_id;
+    ofp_port_t ofport;
+    enum chassis_tunnel_type { GENEVE, STT } type;
+};
+
+static struct chassis_tunnel *
+chassis_tunnel_find(struct hmap *tunnels, const char *chassis_id)
+{
+    struct chassis_tunnel *tun;
+    HMAP_FOR_EACH_WITH_HASH (tun, hmap_node, hash_string(chassis_id, 0),
+                             tunnels) {
+        if (!strcmp(tun->chassis_id, chassis_id)) {
+            return tun;
+        }
+    }
+    return NULL;
+}
+
+static void
+put_load(uint64_t value, enum mf_field_id dst, int ofs, int n_bits,
+         struct ofpbuf *ofpacts)
+{
+    struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ofpacts);
+    sf->field = mf_from_id(dst);
+    sf->flow_has_vlan = false;
+
+    ovs_be64 n_value = htonll(value);
+    bitwise_copy(&n_value, 8, 0, &sf->value, sf->field->n_bytes, ofs, n_bits);
+    bitwise_one(&sf->mask, sf->field->n_bytes, ofs, n_bits);
+}
+
+static void
+put_move(enum mf_field_id src, int src_ofs,
+         enum mf_field_id dst, int dst_ofs,
+         int n_bits,
+         struct ofpbuf *ofpacts)
+{
+    struct ofpact_reg_move *move = ofpact_put_REG_MOVE(ofpacts);
+    move->src.field = mf_from_id(src);
+    move->src.ofs = src_ofs;
+    move->src.n_bits = n_bits;
+    move->dst.field = mf_from_id(dst);
+    move->dst.ofs = dst_ofs;
+    move->dst.n_bits = n_bits;
+}
+
+static void
+put_resubmit(uint8_t table_id, struct ofpbuf *ofpacts)
+{
+    struct ofpact_resubmit *resubmit = ofpact_put_RESUBMIT(ofpacts);
+    resubmit->in_port = OFPP_IN_PORT;
+    resubmit->table_id = table_id;
+}
+
+static void
+put_encapsulation(enum mf_field_id mff_ovn_geneve,
+                  const struct chassis_tunnel *tun,
+                  const struct sbrec_datapath_binding *datapath,
+                  uint16_t outport, struct ofpbuf *ofpacts)
+{
+    if (tun->type == GENEVE) {
+        put_load(datapath->tunnel_key, MFF_TUN_ID, 0, 24, ofpacts);
+        put_load(outport, mff_ovn_geneve, 0, 32, ofpacts);
+        put_move(MFF_LOG_INPORT, 0, mff_ovn_geneve, 16, 15, ofpacts);
+    } else if (tun->type == STT) {
+        put_load(datapath->tunnel_key | (outport << 24), MFF_TUN_ID, 0, 64,
+                 ofpacts);
+        put_move(MFF_LOG_INPORT, 0, MFF_TUN_ID, 40, 15, ofpacts);
+    } else {
+        OVS_NOT_REACHED();
+    }
+}
+
+static void
+put_stack(enum mf_field_id field, struct ofpact_stack *stack)
+{
+    stack->subfield.field = mf_from_id(field);
+    stack->subfield.ofs = 0;
+    stack->subfield.n_bits = stack->subfield.field->n_bits;
+}
+
 void
-physical_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
-             const char *this_chassis_id, struct hmap *flow_table)
+physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
+             const struct ovsrec_bridge *br_int, const char *this_chassis_id,
+             struct hmap *flow_table)
 {
     struct simap lport_to_ofport = SIMAP_INITIALIZER(&lport_to_ofport);
-    struct simap chassis_to_ofport = SIMAP_INITIALIZER(&chassis_to_ofport);
+    struct hmap tunnels = HMAP_INITIALIZER(&tunnels);
     for (int i = 0; i < br_int->n_ports; i++) {
         const struct ovsrec_port *port_rec = br_int->ports[i];
         if (!strcmp(port_rec->name, br_int->name)) {
@@ -74,7 +164,24 @@ physical_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
 
             /* Record as chassis or local logical port. */
             if (chassis_id) {
-                simap_put(&chassis_to_ofport, chassis_id, ofport);
+                enum chassis_tunnel_type tunnel_type;
+                if (!strcmp(iface_rec->type, "geneve")) {
+                    tunnel_type = GENEVE;
+                    if (!mff_ovn_geneve) {
+                        continue;
+                    }
+                } else if (!strcmp(iface_rec->type, "stt")) {
+                    tunnel_type = STT;
+                } else {
+                    continue;
+                }
+
+                struct chassis_tunnel *tun = xmalloc(sizeof *tun);
+                hmap_insert(&tunnels, &tun->hmap_node,
+                            hash_string(chassis_id, 0));
+                tun->chassis_id = chassis_id;
+                tun->ofport = u16_to_ofp(ofport);
+                tun->type = tunnel_type;
                 break;
             } else {
                 const char *iface_id = smap_get(&iface_rec->external_ids,
@@ -114,27 +221,20 @@ physical_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
                                           binding->logical_port));
         }
 
-        bool local = ofport != 0;
-        if (!local) {
+        const struct chassis_tunnel *tun = NULL;
+        if (!ofport) {
             if (!binding->chassis) {
                 continue;
             }
-            ofport = u16_to_ofp(simap_get(&chassis_to_ofport,
-                                          binding->chassis->name));
-            if (!ofport) {
+            tun = chassis_tunnel_find(&tunnels, binding->chassis->name);
+            if (!tun) {
                 continue;
             }
-        }
-
-        /* Translate the logical datapath into the form we use in
-         * MFF_LOG_DATAPATH. */
-        uint32_t ldp = ldp_to_integer(&binding->logical_datapath);
-        if (!ldp) {
-            continue;
+            ofport = tun->ofport;
         }
 
         struct match match;
-        if (local) {
+        if (!tun) {
             /* Packets that arrive from a vif can belong to a VM or
              * to a container located inside that VM. Packets that
              * arrive from containers have a tag (vlan) associated with them.
@@ -149,7 +249,8 @@ physical_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
              *
              * For both types of traffic: set MFF_LOG_INPORT to the logical
              * input port, MFF_LOG_DATAPATH to the logical datapath, and
-             * resubmit into the logical pipeline starting at table 16. */
+             * resubmit into the logical ingress pipeline starting at table
+             * 16. */
             match_init_catchall(&match);
             ofpbuf_clear(&ofpacts);
             match_set_in_port(&match, ofport);
@@ -157,120 +258,252 @@ physical_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
                 match_set_dl_vlan(&match, htons(tag));
             }
 
-            /* Set MFF_LOG_DATAPATH. */
-            struct ofpact_set_field *sf = ofpact_put_SET_FIELD(&ofpacts);
-            sf->field = mf_from_id(MFF_LOG_DATAPATH);
-            sf->value.be64 = htonll(ldp);
-            sf->mask.be64 = OVS_BE64_MAX;
-
-            /* Set MFF_LOG_INPORT. */
-            sf = ofpact_put_SET_FIELD(&ofpacts);
-            sf->field = mf_from_id(MFF_LOG_INPORT);
-            sf->value.be32 = htonl(binding->tunnel_key);
-            sf->mask.be32 = OVS_BE32_MAX;
+            /* Set MFF_LOG_DATAPATH and MFF_LOG_INPORT. */
+            put_load(binding->datapath->tunnel_key, MFF_LOG_DATAPATH, 0, 64,
+                     &ofpacts);
+            put_load(binding->tunnel_key, MFF_LOG_INPORT, 0, 32, &ofpacts);
 
             /* Strip vlans. */
             if (tag) {
                 ofpact_put_STRIP_VLAN(&ofpacts);
             }
 
-            /* Resubmit to first logical pipeline table. */
-            struct ofpact_resubmit *resubmit = ofpact_put_RESUBMIT(&ofpacts);
-            resubmit->in_port = OFPP_IN_PORT;
-            resubmit->table_id = 16;
-            ofctrl_add_flow(flow_table, 0, tag ? 150 : 100, &match, &ofpacts);
+            /* Resubmit to first logical ingress pipeline table. */
+            put_resubmit(OFTABLE_LOG_INGRESS_PIPELINE, &ofpacts);
+            ofctrl_add_flow(flow_table, OFTABLE_PHY_TO_LOG, tag ? 150 : 100,
+                            &match, &ofpacts);
+
+            /* Table 33, priority 100.
+             * =======================
+             *
+             * Implements output to local hypervisor.  Each flow matches a
+             * logical output port on the local hypervisor, and resubmits to
+             * table 34.
+             */
+
+            match_init_catchall(&match);
+            ofpbuf_clear(&ofpacts);
+
+            /* Match MFF_LOG_DATAPATH, MFF_LOG_OUTPORT. */
+            match_set_metadata(&match, htonll(binding->datapath->tunnel_key));
+            match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0,
+                          binding->tunnel_key);
 
-            /* Table 0, Priority 50.
-             * =====================
+            /* Resubmit to table 34. */
+            put_resubmit(OFTABLE_DROP_LOOPBACK, &ofpacts);
+            ofctrl_add_flow(flow_table, OFTABLE_LOCAL_OUTPUT, 100, &match,
+                            &ofpacts);
+
+            /* Table 64, Priority 100.
+             * =======================
              *
-             * For packets that arrive from a remote node destined to this
-             * local vif: deliver directly to the vif. If the destination
-             * is a container sitting behind a vif, tag the packets. */
+             * Deliver the packet to the local vif. */
             match_init_catchall(&match);
             ofpbuf_clear(&ofpacts);
-            match_set_tun_id(&match, htonll(binding->tunnel_key));
+            match_set_metadata(&match, htonll(binding->datapath->tunnel_key));
+            match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0,
+                          binding->tunnel_key);
             if (tag) {
+                /* For containers sitting behind a local vif, tag the packets
+                 * before delivering them. */
                 struct ofpact_vlan_vid *vlan_vid;
                 vlan_vid = ofpact_put_SET_VLAN_VID(&ofpacts);
                 vlan_vid->vlan_vid = tag;
                 vlan_vid->push_vlan_if_needed = true;
+
+                /* A packet might need to hair-pin back into its ingress
+                 * OpenFlow port (to a different logical port, which we already
+                 * checked back in table 34), so set the in_port to zero. */
+                put_stack(MFF_IN_PORT, ofpact_put_STACK_PUSH(&ofpacts));
+                put_load(0, MFF_IN_PORT, 0, 16, &ofpacts);
             }
             ofpact_put_OUTPUT(&ofpacts)->port = ofport;
-            ofctrl_add_flow(flow_table, 0, 50, &match, &ofpacts);
+            if (tag) {
+                /* Revert the tag added to the packets headed to containers
+                 * in the previous step. If we don't do this, the packets
+                 * that are to be broadcasted to a VM in the same logical
+                 * switch will also contain the tag. Also revert the zero'd
+                 * in_port. */
+                ofpact_put_STRIP_VLAN(&ofpacts);
+                put_stack(MFF_IN_PORT, ofpact_put_STACK_POP(&ofpacts));
+            }
+            ofctrl_add_flow(flow_table, OFTABLE_LOG_TO_PHY, 100,
+                            &match, &ofpacts);
+        } else {
+            /* Table 32, priority 100.
+             * =======================
+             *
+             * Implements output to remote hypervisors.  Each flow matches an
+             * output port that includes a logical port on a remote hypervisor,
+             * and tunnels the packet to that hypervisor.
+             */
+
+            match_init_catchall(&match);
+            ofpbuf_clear(&ofpacts);
+
+            /* Match MFF_LOG_DATAPATH, MFF_LOG_OUTPORT. */
+            match_set_metadata(&match, htonll(binding->datapath->tunnel_key));
+            match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0,
+                          binding->tunnel_key);
+
+            put_encapsulation(mff_ovn_geneve, tun, binding->datapath,
+                              binding->tunnel_key, &ofpacts);
+
+            /* Output to tunnel. */
+            ofpact_put_OUTPUT(&ofpacts)->port = ofport;
+            ofctrl_add_flow(flow_table, OFTABLE_REMOTE_OUTPUT, 100,
+                            &match, &ofpacts);
         }
 
-        /* Table 64, Priority 100.
+        /* Table 34, Priority 100.
          * =======================
          *
          * Drop packets whose logical inport and outport are the same. */
         match_init_catchall(&match);
         ofpbuf_clear(&ofpacts);
+        match_set_metadata(&match, htonll(binding->datapath->tunnel_key));
         match_set_reg(&match, MFF_LOG_INPORT - MFF_REG0, binding->tunnel_key);
         match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0, binding->tunnel_key);
-        ofctrl_add_flow(flow_table, 64, 100, &match, &ofpacts);
+        ofctrl_add_flow(flow_table, OFTABLE_DROP_LOOPBACK, 100,
+                        &match, &ofpacts);
+    }
+
+    /* Handle output to multicast groups, in tables 32 and 33. */
+    const struct sbrec_multicast_group *mc;
+    SBREC_MULTICAST_GROUP_FOR_EACH (mc, ctx->ovnsb_idl) {
+        struct sset remote_chassis = SSET_INITIALIZER(&remote_chassis);
+        struct match match;
+
+        match_init_catchall(&match);
+        match_set_metadata(&match, htonll(mc->datapath->tunnel_key));
+        match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0, mc->tunnel_key);
 
-        /* Table 64, Priority 50.
-         * ======================
+        /* Go through all of the ports in the multicast group:
          *
-         * For packets to remote machines, send them over a tunnel to the
-         * remote chassis.
+         *    - For local ports, add actions to 'ofpacts' to set the output
+         *      port and resubmit.
          *
-         * For packets to local vifs, deliver them directly. */
-        match_init_catchall(&match);
+         *    - For remote ports, add the chassis to 'remote_chassis'. */
         ofpbuf_clear(&ofpacts);
-        match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0, binding->tunnel_key);
-        if (!local) {
-            /* Set MFF_TUN_ID. */
-            struct ofpact_set_field *sf = ofpact_put_SET_FIELD(&ofpacts);
-            sf->field = mf_from_id(MFF_TUN_ID);
-            sf->value.be64 = htonll(binding->tunnel_key);
-            sf->mask.be64 = OVS_BE64_MAX;
+        for (size_t i = 0; i < mc->n_ports; i++) {
+            struct sbrec_port_binding *port = mc->ports[i];
+
+            if (port->datapath != mc->datapath) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, UUID_FMT": multicast group contains ports "
+                             "in wrong datapath",
+                             UUID_ARGS(&mc->header_.uuid));
+                continue;
+            }
+
+            if (simap_contains(&lport_to_ofport, port->logical_port)) {
+                put_load(port->tunnel_key, MFF_LOG_OUTPORT, 0, 32, &ofpacts);
+                put_resubmit(OFTABLE_DROP_LOOPBACK, &ofpacts);
+            } else if (port->chassis) {
+                sset_add(&remote_chassis, port->chassis->name);
+            }
+        }
+
+        /* Table 33, priority 100.
+         * =======================
+         *
+         * Handle output to the local logical ports in the multicast group, if
+         * any. */
+        bool local_ports = ofpacts.size > 0;
+        if (local_ports) {
+            ofctrl_add_flow(flow_table, OFTABLE_LOCAL_OUTPUT, 100,
+                            &match, &ofpacts);
         }
-        if (tag) {
-            /* For containers sitting behind a local vif, tag the packets
-             * before delivering them. Since there is a possibility of
-             * packets needing to hair-pin back into the same vif from
-             * which it came, push the in_port to stack and make the
-             * in_port as zero. */
-            struct ofpact_vlan_vid *vlan_vid;
-            vlan_vid = ofpact_put_SET_VLAN_VID(&ofpacts);
-            vlan_vid->vlan_vid = tag;
-            vlan_vid->push_vlan_if_needed = true;
-
-            struct ofpact_stack *stack_action;
-            const struct mf_field *field;
-            stack_action = ofpact_put_STACK_PUSH(&ofpacts);
-            field = mf_from_id(MFF_IN_PORT);
-            stack_action->subfield.field = field;
-            stack_action->subfield.ofs = 0;
-            stack_action->subfield.n_bits = field->n_bits;
-
-            struct ofpact_set_field *sf = ofpact_put_SET_FIELD(&ofpacts);
-            sf->field = mf_from_id(MFF_IN_PORT);
-            sf->value.be16 = 0;
-            sf->mask.be16 = OVS_BE16_MAX;
+
+        /* Table 32, priority 100.
+         * =======================
+         *
+         * Handle output to the remote chassis in the multicast group, if
+         * any. */
+        if (!sset_is_empty(&remote_chassis)) {
+            ofpbuf_clear(&ofpacts);
+
+            const char *chassis;
+            const struct chassis_tunnel *prev = NULL;
+            SSET_FOR_EACH (chassis, &remote_chassis) {
+                const struct chassis_tunnel *tun
+                    = chassis_tunnel_find(&tunnels, chassis);
+                if (!tun) {
+                    continue;
+                }
+
+                if (!prev || tun->type != prev->type) {
+                    put_encapsulation(mff_ovn_geneve, tun,
+                                      mc->datapath, mc->tunnel_key, &ofpacts);
+                    prev = tun;
+                }
+                ofpact_put_OUTPUT(&ofpacts)->port = tun->ofport;
+            }
+
+            if (ofpacts.size) {
+                if (local_ports) {
+                    put_resubmit(OFTABLE_LOCAL_OUTPUT, &ofpacts);
+                }
+                ofctrl_add_flow(flow_table, OFTABLE_REMOTE_OUTPUT, 100,
+                                &match, &ofpacts);
+            }
         }
-        ofpact_put_OUTPUT(&ofpacts)->port = ofport;
-        if (tag) {
-            /* Revert the tag added to the packets headed to containers
-             * in the previous step. If we don't do this, the packets
-             * that are to be broadcasted to a VM in the same logical
-             * switch will also contain the tag. Also revert the zero'd
-             * in_port. */
-            ofpact_put_STRIP_VLAN(&ofpacts);
-
-            struct ofpact_stack *stack_action;
-            const struct mf_field *field;
-            stack_action = ofpact_put_STACK_POP(&ofpacts);
-            field = mf_from_id(MFF_IN_PORT);
-            stack_action->subfield.field = field;
-            stack_action->subfield.ofs = 0;
-            stack_action->subfield.n_bits = field->n_bits;
+        sset_destroy(&remote_chassis);
+    }
+
+    /* Table 0, priority 100.
+     * ======================
+     *
+     * For packets that arrive from a remote hypervisor (by matching a tunnel
+     * in_port), set MFF_LOG_DATAPATH, MFF_LOG_INPORT, and MFF_LOG_OUTPORT from
+     * the tunnel key data, then resubmit to table 33 to handle packets to the
+     * local hypervisor. */
+
+    struct chassis_tunnel *tun;
+    HMAP_FOR_EACH (tun, hmap_node, &tunnels) {
+        struct match match = MATCH_CATCHALL_INITIALIZER;
+        match_set_in_port(&match, tun->ofport);
+
+        ofpbuf_clear(&ofpacts);
+        if (tun->type == GENEVE) {
+            put_move(MFF_TUN_ID, 0,  MFF_LOG_DATAPATH, 0, 24, &ofpacts);
+            put_move(mff_ovn_geneve, 16, MFF_LOG_INPORT, 0, 15,
+                     &ofpacts);
+            put_move(mff_ovn_geneve, 0, MFF_LOG_OUTPORT, 0, 16,
+                     &ofpacts);
+        } else if (tun->type == STT) {
+            put_move(MFF_TUN_ID, 40, MFF_LOG_INPORT,   0, 15, &ofpacts);
+            put_move(MFF_TUN_ID, 24, MFF_LOG_OUTPORT,  0, 16, &ofpacts);
+            put_move(MFF_TUN_ID,  0, MFF_LOG_DATAPATH, 0, 24, &ofpacts);
+        } else {
+            OVS_NOT_REACHED();
         }
-        ofctrl_add_flow(flow_table, 64, 50, &match, &ofpacts);
+        put_resubmit(OFTABLE_LOCAL_OUTPUT, &ofpacts);
+
+        ofctrl_add_flow(flow_table, OFTABLE_PHY_TO_LOG, 100, &match, &ofpacts);
     }
 
+    /* Table 34, Priority 0.
+     * =======================
+     *
+     * Resubmit packets that don't output to the ingress port (already checked
+     * in table 33) to the logical egress pipeline, clearing the logical
+     * registers (for consistent behavior with packets that get tunneled). */
+    struct match match;
+    match_init_catchall(&match);
+    ofpbuf_clear(&ofpacts);
+#define MFF_LOG_REG(ID) put_load(0, ID, 0, 32, &ofpacts);
+    MFF_LOG_REGS;
+#undef MFF_LOG_REGS
+    put_resubmit(OFTABLE_LOG_EGRESS_PIPELINE, &ofpacts);
+    ofctrl_add_flow(flow_table, OFTABLE_DROP_LOOPBACK, 0, &match, &ofpacts);
+
     ofpbuf_uninit(&ofpacts);
     simap_destroy(&lport_to_ofport);
-    simap_destroy(&chassis_to_ofport);
+    struct chassis_tunnel *tun_next;
+    HMAP_FOR_EACH_SAFE (tun, tun_next, hmap_node, &tunnels) {
+        hmap_remove(&tunnels, &tun->hmap_node);
+        free(tun);
+    }
+    hmap_destroy(&tunnels);
 }
index 95e5a20098520e64405d3ac2f81cc9a17532409b..c79e97bca737bb780b86d3f40395f95711bcf6df 100644 (file)
  * ============================
  *
  * This module implements physical-to-logical and logical-to-physical
- * translation as separate OpenFlow tables that run before and after,
- * respectively, the logical pipeline OpenFlow tables.
+ * translation as separate OpenFlow tables that run before the ingress pipeline
+ * and after the egress pipeline, respectively, as well as to connect the
+ * two pipelines.
  */
 
+#include "meta-flow.h"
+
 struct controller_ctx;
 struct hmap;
 struct ovsdb_idl;
@@ -39,7 +42,8 @@ struct ovsrec_bridge;
 #define OVN_GENEVE_LEN 4
 
 void physical_register_ovs_idl(struct ovsdb_idl *);
-void physical_run(struct controller_ctx *, const struct ovsrec_bridge *br_int,
-                  const char *chassis_id, struct hmap *flow_table);
+void physical_run(struct controller_ctx *, enum mf_field_id mff_ovn_geneve,
+                  const struct ovsrec_bridge *br_int, const char *chassis_id,
+                  struct hmap *flow_table);
 
 #endif /* ovn/physical.h */
index aa58134b9f383338f5afc7a94dc7792eca8047cd..cf8e222fba1721f6ecf972fa77ad798bd584dbdb 100644 (file)
@@ -30,6 +30,7 @@
 #include "ovn/lib/ovn-nb-idl.h"
 #include "ovn/lib/ovn-sb-idl.h"
 #include "poll-loop.h"
+#include "smap.h"
 #include "stream.h"
 #include "stream-ssl.h"
 #include "unixctl.h"
@@ -74,50 +75,473 @@ Options:\n\
     stream_usage("database", true, true, false);
 }
 \f
-static int
-compare_strings(const void *a_, const void *b_)
+struct tnlid_node {
+    struct hmap_node hmap_node;
+    uint32_t tnlid;
+};
+
+static void
+destroy_tnlids(struct hmap *tnlids)
 {
-    char *const *a = a_;
-    char *const *b = b_;
-    return strcmp(*a, *b);
+    struct tnlid_node *node, *next;
+    HMAP_FOR_EACH_SAFE (node, next, hmap_node, tnlids) {
+        hmap_remove(tnlids, &node->hmap_node);
+        free(node);
+    }
+    hmap_destroy(tnlids);
+}
+
+static void
+add_tnlid(struct hmap *set, uint32_t tnlid)
+{
+    struct tnlid_node *node = xmalloc(sizeof *node);
+    hmap_insert(set, &node->hmap_node, hash_int(tnlid, 0));
+    node->tnlid = tnlid;
 }
 
-/*
- * Determine whether 2 arrays of MAC addresses are the same.  It's possible that
- * the lists could be *very* long and this check is being done a lot (every
- * time the OVN_Northbound database changes).
- */
 static bool
-macs_equal(char **binding_macs_, size_t b_n_macs,
-           char **lport_macs_, size_t l_n_macs)
+tnlid_in_use(const struct hmap *set, uint32_t tnlid)
 {
-    char **binding_macs, **lport_macs;
-    size_t bytes, i;
+    const struct tnlid_node *node;
+    HMAP_FOR_EACH_IN_BUCKET (node, hmap_node, hash_int(tnlid, 0), set) {
+        if (node->tnlid == tnlid) {
+            return true;
+        }
+    }
+    return false;
+}
 
-    if (b_n_macs != l_n_macs) {
-        return false;
+static uint32_t
+allocate_tnlid(struct hmap *set, const char *name, uint32_t max,
+               uint32_t *hint)
+{
+    for (uint32_t tnlid = *hint + 1; tnlid != *hint;
+         tnlid = tnlid + 1 <= max ? tnlid + 1 : 1) {
+        if (!tnlid_in_use(set, tnlid)) {
+            add_tnlid(set, tnlid);
+            *hint = tnlid;
+            return tnlid;
+        }
     }
 
-    bytes = b_n_macs * sizeof binding_macs_[0];
-    binding_macs = xmalloc(bytes);
-    lport_macs = xmalloc(bytes);
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+    VLOG_WARN_RL(&rl, "all %s tunnel ids exhausted", name);
+    return 0;
+}
+\f
+/* The 'key' comes from nb->header_.uuid or sb->external_ids:logical-switch. */
+struct ovn_datapath {
+    struct hmap_node key_node;  /* Index on 'key'. */
+    struct uuid key;            /* nb->header_.uuid. */
 
-    memcpy(binding_macs, binding_macs_, bytes);
-    memcpy(lport_macs, lport_macs_, bytes);
+    const struct nbrec_logical_switch *nb;   /* May be NULL. */
+    const struct sbrec_datapath_binding *sb; /* May be NULL. */
 
-    qsort(binding_macs, b_n_macs, sizeof binding_macs[0], compare_strings);
-    qsort(lport_macs, l_n_macs, sizeof lport_macs[0], compare_strings);
+    struct ovs_list list;       /* In list of similar records. */
 
-    for (i = 0; i < b_n_macs; i++) {
-        if (strcmp(binding_macs[i], lport_macs[i])) {
-            break;
+    struct hmap port_tnlids;
+    uint32_t port_key_hint;
+
+    bool has_unknown;
+};
+
+static struct ovn_datapath *
+ovn_datapath_create(struct hmap *datapaths, const struct uuid *key,
+                    const struct nbrec_logical_switch *nb,
+                    const struct sbrec_datapath_binding *sb)
+{
+    struct ovn_datapath *od = xzalloc(sizeof *od);
+    od->key = *key;
+    od->sb = sb;
+    od->nb = nb;
+    hmap_init(&od->port_tnlids);
+    od->port_key_hint = 0;
+    hmap_insert(datapaths, &od->key_node, uuid_hash(&od->key));
+    return od;
+}
+
+static void
+ovn_datapath_destroy(struct hmap *datapaths, struct ovn_datapath *od)
+{
+    if (od) {
+        /* Don't remove od->list.  It is used within build_datapaths() as a
+         * private list and once we've exited that function it is not safe to
+         * use it. */
+        hmap_remove(datapaths, &od->key_node);
+        destroy_tnlids(&od->port_tnlids);
+        free(od);
+    }
+}
+
+static struct ovn_datapath *
+ovn_datapath_find(struct hmap *datapaths, const struct uuid *uuid)
+{
+    struct ovn_datapath *od;
+
+    HMAP_FOR_EACH_WITH_HASH (od, key_node, uuid_hash(uuid), datapaths) {
+        if (uuid_equals(uuid, &od->key)) {
+            return od;
+        }
+    }
+    return NULL;
+}
+
+static struct ovn_datapath *
+ovn_datapath_from_sbrec(struct hmap *datapaths,
+                        const struct sbrec_datapath_binding *sb)
+{
+    struct uuid key;
+
+    if (!smap_get_uuid(&sb->external_ids, "logical-switch", &key)) {
+        return NULL;
+    }
+    return ovn_datapath_find(datapaths, &key);
+}
+
+static void
+join_datapaths(struct northd_context *ctx, struct hmap *datapaths,
+               struct ovs_list *sb_only, struct ovs_list *nb_only,
+               struct ovs_list *both)
+{
+    hmap_init(datapaths);
+    list_init(sb_only);
+    list_init(nb_only);
+    list_init(both);
+
+    const struct sbrec_datapath_binding *sb, *sb_next;
+    SBREC_DATAPATH_BINDING_FOR_EACH_SAFE (sb, sb_next, ctx->ovnsb_idl) {
+        struct uuid key;
+        if (!smap_get_uuid(&sb->external_ids, "logical-switch", &key)) {
+            ovsdb_idl_txn_add_comment(ctx->ovnsb_txn,
+                                      "deleting Datapath_Binding "UUID_FMT" that "
+                                      "lacks external-ids:logical-switch",
+                         UUID_ARGS(&sb->header_.uuid));
+            sbrec_datapath_binding_delete(sb);
+            continue;
+        }
+
+        if (ovn_datapath_find(datapaths, &key)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_INFO_RL(&rl, "deleting Datapath_Binding "UUID_FMT" with "
+                         "duplicate external-ids:logical-switch "UUID_FMT,
+                         UUID_ARGS(&sb->header_.uuid), UUID_ARGS(&key));
+            sbrec_datapath_binding_delete(sb);
+            continue;
+        }
+
+        struct ovn_datapath *od = ovn_datapath_create(datapaths, &key,
+                                                      NULL, sb);
+        list_push_back(sb_only, &od->list);
+    }
+
+    const struct nbrec_logical_switch *nb;
+    NBREC_LOGICAL_SWITCH_FOR_EACH (nb, ctx->ovnnb_idl) {
+        struct ovn_datapath *od = ovn_datapath_find(datapaths,
+                                                    &nb->header_.uuid);
+        if (od) {
+            od->nb = nb;
+            list_remove(&od->list);
+            list_push_back(both, &od->list);
+        } else {
+            od = ovn_datapath_create(datapaths, &nb->header_.uuid, nb, NULL);
+            list_push_back(nb_only, &od->list);
+        }
+    }
+}
+
+static uint32_t
+ovn_datapath_allocate_key(struct hmap *dp_tnlids)
+{
+    static uint32_t hint;
+    return allocate_tnlid(dp_tnlids, "datapath", (1u << 24) - 1, &hint);
+}
+
+static void
+build_datapaths(struct northd_context *ctx, struct hmap *datapaths)
+{
+    struct ovs_list sb_only, nb_only, both;
+
+    join_datapaths(ctx, datapaths, &sb_only, &nb_only, &both);
+
+    if (!list_is_empty(&nb_only)) {
+        /* First index the in-use datapath tunnel IDs. */
+        struct hmap dp_tnlids = HMAP_INITIALIZER(&dp_tnlids);
+        struct ovn_datapath *od;
+        LIST_FOR_EACH (od, list, &both) {
+            add_tnlid(&dp_tnlids, od->sb->tunnel_key);
+        }
+
+        /* Add southbound record for each unmatched northbound record. */
+        LIST_FOR_EACH (od, list, &nb_only) {
+            uint16_t tunnel_key = ovn_datapath_allocate_key(&dp_tnlids);
+            if (!tunnel_key) {
+                break;
+            }
+
+            od->sb = sbrec_datapath_binding_insert(ctx->ovnsb_txn);
+
+            struct smap external_ids = SMAP_INITIALIZER(&external_ids);
+            char uuid_s[UUID_LEN + 1];
+            sprintf(uuid_s, UUID_FMT, UUID_ARGS(&od->nb->header_.uuid));
+            smap_add(&external_ids, "logical-switch", uuid_s);
+            sbrec_datapath_binding_set_external_ids(od->sb, &external_ids);
+            smap_destroy(&external_ids);
+
+            sbrec_datapath_binding_set_tunnel_key(od->sb, tunnel_key);
+        }
+        destroy_tnlids(&dp_tnlids);
+    }
+
+    /* Delete southbound records without northbound matches. */
+    struct ovn_datapath *od, *next;
+    LIST_FOR_EACH_SAFE (od, next, list, &sb_only) {
+        list_remove(&od->list);
+        sbrec_datapath_binding_delete(od->sb);
+        ovn_datapath_destroy(datapaths, od);
+    }
+}
+\f
+struct ovn_port {
+    struct hmap_node key_node;  /* Index on 'key'. */
+    const char *key;            /* nb->name and sb->logical_port */
+
+    const struct nbrec_logical_port *nb; /* May be NULL. */
+    const struct sbrec_port_binding *sb; /* May be NULL. */
+
+    struct ovn_datapath *od;
+
+    struct ovs_list list;       /* In list of similar records. */
+};
+
+static struct ovn_port *
+ovn_port_create(struct hmap *ports, const char *key,
+                const struct nbrec_logical_port *nb,
+                const struct sbrec_port_binding *sb)
+{
+    struct ovn_port *op = xzalloc(sizeof *op);
+    op->key = key;
+    op->sb = sb;
+    op->nb = nb;
+    hmap_insert(ports, &op->key_node, hash_string(op->key, 0));
+    return op;
+}
+
+static void
+ovn_port_destroy(struct hmap *ports, struct ovn_port *port)
+{
+    if (port) {
+        /* Don't remove port->list.  It is used within build_ports() as a
+         * private list and once we've exited that function it is not safe to
+         * use it. */
+        hmap_remove(ports, &port->key_node);
+        free(port);
+    }
+}
+
+static struct ovn_port *
+ovn_port_find(struct hmap *ports, const char *name)
+{
+    struct ovn_port *op;
+
+    HMAP_FOR_EACH_WITH_HASH (op, key_node, hash_string(name, 0), ports) {
+        if (!strcmp(op->key, name)) {
+            return op;
+        }
+    }
+    return NULL;
+}
+
+static uint32_t
+ovn_port_allocate_key(struct ovn_datapath *od)
+{
+    return allocate_tnlid(&od->port_tnlids, "port",
+                          (1u << 15) - 1, &od->port_key_hint);
+}
+
+static void
+join_logical_ports(struct northd_context *ctx,
+                   struct hmap *datapaths, struct hmap *ports,
+                   struct ovs_list *sb_only, struct ovs_list *nb_only,
+                   struct ovs_list *both)
+{
+    hmap_init(ports);
+    list_init(sb_only);
+    list_init(nb_only);
+    list_init(both);
+
+    const struct sbrec_port_binding *sb;
+    SBREC_PORT_BINDING_FOR_EACH (sb, ctx->ovnsb_idl) {
+        struct ovn_port *op = ovn_port_create(ports, sb->logical_port,
+                                              NULL, sb);
+        list_push_back(sb_only, &op->list);
+    }
+
+    struct ovn_datapath *od;
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        for (size_t i = 0; i < od->nb->n_ports; i++) {
+            const struct nbrec_logical_port *nb = od->nb->ports[i];
+            struct ovn_port *op = ovn_port_find(ports, nb->name);
+            if (op) {
+                op->nb = nb;
+                list_remove(&op->list);
+                list_push_back(both, &op->list);
+            } else {
+                op = ovn_port_create(ports, nb->name, nb, NULL);
+                list_push_back(nb_only, &op->list);
+            }
+            op->od = od;
+        }
+    }
+}
+
+static void
+ovn_port_update_sbrec(const struct ovn_port *op)
+{
+    sbrec_port_binding_set_datapath(op->sb, op->od->sb);
+    sbrec_port_binding_set_parent_port(op->sb, op->nb->parent_name);
+    sbrec_port_binding_set_tag(op->sb, op->nb->tag, op->nb->n_tag);
+    sbrec_port_binding_set_mac(op->sb, (const char **) op->nb->macs,
+                               op->nb->n_macs);
+}
+
+static void
+build_ports(struct northd_context *ctx, struct hmap *datapaths,
+            struct hmap *ports)
+{
+    struct ovs_list sb_only, nb_only, both;
+
+    join_logical_ports(ctx, datapaths, ports, &sb_only, &nb_only, &both);
+
+    /* For logical ports that are in both databases, update the southbound
+     * record based on northbound data.  Also index the in-use tunnel_keys. */
+    struct ovn_port *op, *next;
+    LIST_FOR_EACH_SAFE (op, next, list, &both) {
+        ovn_port_update_sbrec(op);
+
+        add_tnlid(&op->od->port_tnlids, op->sb->tunnel_key);
+        if (op->sb->tunnel_key > op->od->port_key_hint) {
+            op->od->port_key_hint = op->sb->tunnel_key;
+        }
+    }
+
+    /* Add southbound record for each unmatched northbound record. */
+    LIST_FOR_EACH_SAFE (op, next, list, &nb_only) {
+        uint16_t tunnel_key = ovn_port_allocate_key(op->od);
+        if (!tunnel_key) {
+            continue;
+        }
+
+        op->sb = sbrec_port_binding_insert(ctx->ovnsb_txn);
+        ovn_port_update_sbrec(op);
+
+        sbrec_port_binding_set_logical_port(op->sb, op->key);
+        sbrec_port_binding_set_tunnel_key(op->sb, tunnel_key);
+    }
+
+    /* Delete southbound records without northbound matches. */
+    LIST_FOR_EACH_SAFE(op, next, list, &sb_only) {
+        list_remove(&op->list);
+        sbrec_port_binding_delete(op->sb);
+        ovn_port_destroy(ports, op);
+    }
+}
+\f
+#define OVN_MIN_MULTICAST 32768
+#define OVN_MAX_MULTICAST 65535
+
+struct multicast_group {
+    const char *name;
+    uint16_t key;               /* OVN_MIN_MULTICAST...OVN_MAX_MULTICAST. */
+};
+
+#define MC_FLOOD "_MC_flood"
+static const struct multicast_group mc_flood = { MC_FLOOD, 65535 };
+
+#define MC_UNKNOWN "_MC_unknown"
+static const struct multicast_group mc_unknown = { MC_UNKNOWN, 65534 };
+
+static bool
+multicast_group_equal(const struct multicast_group *a,
+                      const struct multicast_group *b)
+{
+    return !strcmp(a->name, b->name) && a->key == b->key;
+}
+
+/* Multicast group entry. */
+struct ovn_multicast {
+    struct hmap_node hmap_node; /* Index on 'datapath' and 'key'. */
+    struct ovn_datapath *datapath;
+    const struct multicast_group *group;
+
+    struct ovn_port **ports;
+    size_t n_ports, allocated_ports;
+};
+
+static uint32_t
+ovn_multicast_hash(const struct ovn_datapath *datapath,
+                   const struct multicast_group *group)
+{
+    return hash_pointer(datapath, group->key);
+}
+
+static struct ovn_multicast *
+ovn_multicast_find(struct hmap *mcgroups, struct ovn_datapath *datapath,
+                   const struct multicast_group *group)
+{
+    struct ovn_multicast *mc;
+
+    HMAP_FOR_EACH_WITH_HASH (mc, hmap_node,
+                             ovn_multicast_hash(datapath, group), mcgroups) {
+        if (mc->datapath == datapath
+            && multicast_group_equal(mc->group, group)) {
+            return mc;
         }
     }
+    return NULL;
+}
+
+static void
+ovn_multicast_add(struct hmap *mcgroups, const struct multicast_group *group,
+                  struct ovn_port *port)
+{
+    struct ovn_datapath *od = port->od;
+    struct ovn_multicast *mc = ovn_multicast_find(mcgroups, od, group);
+    if (!mc) {
+        mc = xmalloc(sizeof *mc);
+        hmap_insert(mcgroups, &mc->hmap_node, ovn_multicast_hash(od, group));
+        mc->datapath = od;
+        mc->group = group;
+        mc->n_ports = 0;
+        mc->allocated_ports = 4;
+        mc->ports = xmalloc(mc->allocated_ports * sizeof *mc->ports);
+    }
+    if (mc->n_ports >= mc->allocated_ports) {
+        mc->ports = x2nrealloc(mc->ports, &mc->allocated_ports,
+                               sizeof *mc->ports);
+    }
+    mc->ports[mc->n_ports++] = port;
+}
 
-    free(binding_macs);
-    free(lport_macs);
+static void
+ovn_multicast_destroy(struct hmap *mcgroups, struct ovn_multicast *mc)
+{
+    if (mc) {
+        hmap_remove(mcgroups, &mc->hmap_node);
+        free(mc->ports);
+        free(mc);
+    }
+}
 
-    return (i == b_n_macs) ? true : false;
+static void
+ovn_multicast_update_sbrec(const struct ovn_multicast *mc,
+                           const struct sbrec_multicast_group *sb)
+{
+    struct sbrec_port_binding **ports = xmalloc(mc->n_ports * sizeof *ports);
+    for (size_t i = 0; i < mc->n_ports; i++) {
+        ports[i] = CONST_CAST(struct sbrec_port_binding *, mc->ports[i]->sb);
+    }
+    sbrec_multicast_group_set_ports(sb, ports, mc->n_ports);
+    free(ports);
 }
 \f
 /* Logical flow generation.
@@ -126,83 +550,90 @@ macs_equal(char **binding_macs_, size_t b_n_macs,
  * function of most of the northbound database.
  */
 
-/* Enough context to add a Logical_Flow row, using lflow_add(). */
-struct lflow_ctx {
-    /* From northd_context. */
-    struct ovsdb_idl *ovnsb_idl;
-    struct ovsdb_idl_txn *ovnsb_txn;
-
-    /* Contains "struct lflow_hash_node"s.  Used to figure out what existing
-     * Logical_Flow rows should be deleted: we index all of the Logical_Flow
-     * rows into this data structure, then as existing rows are generated we
-     * remove them.  After generating all the rows, any remaining in
-     * 'lflow_hmap' must be deleted from the database. */
-    struct hmap lflow_hmap;
-};
+struct ovn_lflow {
+    struct hmap_node hmap_node;
 
-/* A row in the Logical_Flow table, indexed by its full contents, */
-struct lflow_hash_node {
-    struct hmap_node node;
-    const struct sbrec_logical_flow *lflow;
+    struct ovn_datapath *od;
+    enum ovn_pipeline { P_IN, P_OUT } pipeline;
+    uint8_t table_id;
+    uint16_t priority;
+    char *match;
+    char *actions;
 };
 
 static size_t
-lflow_hash(const struct uuid *logical_datapath, uint8_t table_id,
-          uint16_t priority, const char *match, const char *actions)
+ovn_lflow_hash(const struct ovn_lflow *lflow)
 {
-    size_t hash = uuid_hash(logical_datapath);
-    hash = hash_2words((table_id << 16) | priority, hash);
-    hash = hash_string(match, hash);
-    return hash_string(actions, hash);
+    size_t hash = uuid_hash(&lflow->od->key);
+    hash = hash_2words((lflow->table_id << 16) | lflow->priority, hash);
+    hash = hash_string(lflow->match, hash);
+    return hash_string(lflow->actions, hash);
 }
 
-static size_t
-lflow_hash_rec(const struct sbrec_logical_flow *lflow)
+static bool
+ovn_lflow_equal(const struct ovn_lflow *a, const struct ovn_lflow *b)
 {
-    return lflow_hash(&lflow->logical_datapath, lflow->table_id,
-                      lflow->priority, lflow->match,
-                      lflow->actions);
+    return (a->od == b->od
+            && a->pipeline == b->pipeline
+            && a->table_id == b->table_id
+            && a->priority == b->priority
+            && !strcmp(a->match, b->match)
+            && !strcmp(a->actions, b->actions));
+}
+
+static void
+ovn_lflow_init(struct ovn_lflow *lflow, struct ovn_datapath *od,
+              enum ovn_pipeline pipeline, uint8_t table_id, uint16_t priority,
+              char *match, char *actions)
+{
+    lflow->od = od;
+    lflow->pipeline = pipeline;
+    lflow->table_id = table_id;
+    lflow->priority = priority;
+    lflow->match = match;
+    lflow->actions = actions;
 }
 
 /* Adds a row with the specified contents to the Logical_Flow table. */
 static void
-lflow_add(struct lflow_ctx *ctx,
-          const struct nbrec_logical_switch *logical_datapath,
-          uint8_t table_id,
-          uint16_t priority,
-          const char *match,
-          const char *actions)
-{
-    struct lflow_hash_node *hash_node;
-
-    /* Check whether such a row already exists in the Logical_Flow table.  If
-     * so, remove it from 'ctx->lflow_hmap' and we're done. */
-    HMAP_FOR_EACH_WITH_HASH (hash_node, node,
-                             lflow_hash(&logical_datapath->header_.uuid,
-                                        table_id, priority, match, actions),
-                             &ctx->lflow_hmap) {
-        const struct sbrec_logical_flow *lflow = hash_node->lflow;
-        if (uuid_equals(&lflow->logical_datapath,
-                        &logical_datapath->header_.uuid)
-            && lflow->table_id == table_id
-            && lflow->priority == priority
-            && !strcmp(lflow->match, match)
-            && !strcmp(lflow->actions, actions)) {
-            hmap_remove(&ctx->lflow_hmap, &hash_node->node);
-            free(hash_node);
-            return;
-        }
-    }
-
-    /* No such Logical_Flow row.  Add one. */
-    const struct sbrec_logical_flow *lflow;
-    lflow = sbrec_logical_flow_insert(ctx->ovnsb_txn);
-    sbrec_logical_flow_set_logical_datapath(lflow,
-                                            logical_datapath->header_.uuid);
-    sbrec_logical_flow_set_table_id(lflow, table_id);
-    sbrec_logical_flow_set_priority(lflow, priority);
-    sbrec_logical_flow_set_match(lflow, match);
-    sbrec_logical_flow_set_actions(lflow, actions);
+ovn_lflow_add(struct hmap *lflow_map, struct ovn_datapath *od,
+              enum ovn_pipeline pipeline, uint8_t table_id, uint16_t priority,
+              const char *match, const char *actions)
+{
+    struct ovn_lflow *lflow = xmalloc(sizeof *lflow);
+    ovn_lflow_init(lflow, od, pipeline, table_id, priority,
+                   xstrdup(match), xstrdup(actions));
+    hmap_insert(lflow_map, &lflow->hmap_node, ovn_lflow_hash(lflow));
+}
+
+static struct ovn_lflow *
+ovn_lflow_find(struct hmap *lflows, struct ovn_datapath *od,
+               enum ovn_pipeline pipeline, uint8_t table_id, uint16_t priority,
+               const char *match, const char *actions)
+{
+    struct ovn_lflow target;
+    ovn_lflow_init(&target, od, pipeline, table_id, priority,
+                   CONST_CAST(char *, match), CONST_CAST(char *, actions));
+
+    struct ovn_lflow *lflow;
+    HMAP_FOR_EACH_WITH_HASH (lflow, hmap_node, ovn_lflow_hash(&target),
+                             lflows) {
+        if (ovn_lflow_equal(lflow, &target)) {
+            return lflow;
+        }
+    }
+    return NULL;
+}
+
+static void
+ovn_lflow_destroy(struct hmap *lflows, struct ovn_lflow *lflow)
+{
+    if (lflow) {
+        hmap_remove(lflows, &lflow->hmap_node);
+        free(lflow->match);
+        free(lflow->actions);
+        free(lflow);
+    }
 }
 
 /* Appends port security constraints on L2 address field 'eth_addr_field'
@@ -241,394 +672,223 @@ lport_is_enabled(const struct nbrec_logical_port *lport)
     return !lport->enabled || *lport->enabled;
 }
 
-/* Updates the Logical_Flow table in the OVN_SB database, constructing its
- * contents based on the OVN_NB database. */
+/* Updates the Logical_Flow and Multicast_Group tables in the OVN_SB database,
+ * constructing their contents based on the OVN_NB database. */
 static void
-build_lflow(struct northd_context *ctx)
+build_lflows(struct northd_context *ctx, struct hmap *datapaths,
+             struct hmap *ports)
 {
-    struct lflow_ctx pc = {
-        .ovnsb_idl = ctx->ovnsb_idl,
-        .ovnsb_txn = ctx->ovnsb_txn,
-        .lflow_hmap = HMAP_INITIALIZER(&pc.lflow_hmap)
-    };
+    struct hmap lflows = HMAP_INITIALIZER(&lflows);
+    struct hmap mcgroups = HMAP_INITIALIZER(&mcgroups);
 
-    /* Add all the Logical_Flow entries currently in the southbound database to
-     * 'pc.lflow_hmap'.  We remove entries that we generate from the hmap,
-     * thus by the time we're done only entries that need to be removed
-     * remain. */
-    const struct sbrec_logical_flow *lflow;
-    SBREC_LOGICAL_FLOW_FOR_EACH (lflow, ctx->ovnsb_idl) {
-        struct lflow_hash_node *hash_node = xzalloc(sizeof *hash_node);
-        hash_node->lflow = lflow;
-        hmap_insert(&pc.lflow_hmap, &hash_node->node,
-                    lflow_hash_rec(lflow));
-    }
-
-    /* Table 0: Admission control framework. */
-    const struct nbrec_logical_switch *lswitch;
-    NBREC_LOGICAL_SWITCH_FOR_EACH (lswitch, ctx->ovnnb_idl) {
+    /* Ingress table 0: Admission control framework (priorities 0 and 100). */
+    struct ovn_datapath *od;
+    HMAP_FOR_EACH (od, key_node, datapaths) {
         /* Logical VLANs not supported. */
-        lflow_add(&pc, lswitch, 0, 100, "vlan.present", "drop;");
+        ovn_lflow_add(&lflows, od, P_IN, 0, 100, "vlan.present", "drop;");
 
         /* Broadcast/multicast source address is invalid. */
-        lflow_add(&pc, lswitch, 0, 100, "eth.src[40]", "drop;");
+        ovn_lflow_add(&lflows, od, P_IN, 0, 100, "eth.src[40]", "drop;");
 
         /* Port security flows have priority 50 (see below) and will continue
          * to the next table if packet source is acceptable. */
 
         /* Otherwise drop the packet. */
-        lflow_add(&pc, lswitch, 0, 0, "1", "drop;");
+        ovn_lflow_add(&lflows, od, P_IN, 0, 0, "1", "drop;");
     }
 
-    /* Table 0: Ingress port security. */
-    NBREC_LOGICAL_SWITCH_FOR_EACH (lswitch, ctx->ovnnb_idl) {
-        for (size_t i = 0; i < lswitch->n_ports; i++) {
-            const struct nbrec_logical_port *lport = lswitch->ports[i];
-            struct ds match = DS_EMPTY_INITIALIZER;
-            ds_put_cstr(&match, "inport == ");
-            json_string_escape(lport->name, &match);
-            build_port_security("eth.src",
-                                lport->port_security, lport->n_port_security,
-                                &match);
-            lflow_add(&pc, lswitch, 0, 50, ds_cstr(&match),
-                      lport_is_enabled(lport) ? "next;" : "drop;");
-            ds_destroy(&match);
-        }
+    /* Ingress table 0: Ingress port security (priority 50). */
+    struct ovn_port *op;
+    HMAP_FOR_EACH (op, key_node, ports) {
+        struct ds match = DS_EMPTY_INITIALIZER;
+        ds_put_cstr(&match, "inport == ");
+        json_string_escape(op->key, &match);
+        build_port_security("eth.src",
+                            op->nb->port_security, op->nb->n_port_security,
+                            &match);
+        ovn_lflow_add(&lflows, op->od, P_IN, 0, 50, ds_cstr(&match),
+                      lport_is_enabled(op->nb) ? "next;" : "drop;");
+        ds_destroy(&match);
     }
 
-    /* Table 1: Destination lookup:
-     *
-     *   - Broadcast and multicast handling (priority 100).
-     *   - Unicast handling (priority 50).
-     *   - Unknown unicast address handling (priority 0).
-     *   */
-    NBREC_LOGICAL_SWITCH_FOR_EACH (lswitch, ctx->ovnnb_idl) {
-        struct ds bcast;        /* Actions for broadcast on 'lswitch'. */
-        struct ds unknown;      /* Actions for unknown MACs on 'lswitch'. */
-
-        ds_init(&bcast);
-        ds_init(&unknown);
-        for (size_t i = 0; i < lswitch->n_ports; i++) {
-            const struct nbrec_logical_port *lport = lswitch->ports[i];
-
-            ds_put_cstr(&bcast, "outport = ");
-            json_string_escape(lport->name, &bcast);
-            ds_put_cstr(&bcast, "; next; ");
-
-            for (size_t j = 0; j < lport->n_macs; j++) {
-                const char *s = lport->macs[j];
-                uint8_t mac[ETH_ADDR_LEN];
-
-                if (eth_addr_from_string(s, mac)) {
-                    struct ds match, unicast;
-
-                    ds_init(&match);
-                    ds_put_format(&match, "eth.dst == %s", s);
-
-                    ds_init(&unicast);
-                    ds_put_cstr(&unicast, "outport = ");
-                    json_string_escape(lport->name, &unicast);
-                    ds_put_cstr(&unicast, "; next;");
-                    lflow_add(&pc, lswitch, 1, 50,
-                             ds_cstr(&match), ds_cstr(&unicast));
-                    ds_destroy(&unicast);
-                    ds_destroy(&match);
-                } else if (!strcmp(s, "unknown")) {
-                    ds_put_cstr(&unknown, "outport = ");
-                    json_string_escape(lport->name, &unknown);
-                    ds_put_cstr(&unknown, "; next; ");
-                } else {
-                    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-
-                    VLOG_INFO_RL(&rl, "%s: invalid syntax '%s' in macs column",
-                                 lport->name, s);
-                }
-            }
-        }
-
-        ds_chomp(&bcast, ' ');
-        lflow_add(&pc, lswitch, 1, 100, "eth.dst[40]", ds_cstr(&bcast));
-        ds_destroy(&bcast);
-
-        if (unknown.length) {
-            ds_chomp(&unknown, ' ');
-            lflow_add(&pc, lswitch, 1, 0, "1", ds_cstr(&unknown));
+    /* Ingress table 1: Destination lookup, broadcast and multicast handling
+     * (priority 100). */
+    HMAP_FOR_EACH (op, key_node, ports) {
+        if (lport_is_enabled(op->nb)) {
+            ovn_multicast_add(&mcgroups, &mc_flood, op);
         }
-        ds_destroy(&unknown);
+    }
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        ovn_lflow_add(&lflows, od, P_IN, 1, 100, "eth.dst[40]",
+                      "outport = \""MC_FLOOD"\"; output;");
     }
 
-    /* Table 2: ACLs. */
-    NBREC_LOGICAL_SWITCH_FOR_EACH (lswitch, ctx->ovnnb_idl) {
-        for (size_t i = 0; i < lswitch->n_acls; i++) {
-            const struct nbrec_acl *acl = lswitch->acls[i];
+    /* Ingress table 1: Destination lookup, unicast handling (priority 50), */
+    HMAP_FOR_EACH (op, key_node, ports) {
+        for (size_t i = 0; i < op->nb->n_macs; i++) {
+            uint8_t mac[ETH_ADDR_LEN];
+
+            if (eth_addr_from_string(op->nb->macs[i], mac)) {
+                struct ds match, actions;
+
+                ds_init(&match);
+                ds_put_format(&match, "eth.dst == %s", op->nb->macs[i]);
+
+                ds_init(&actions);
+                ds_put_cstr(&actions, "outport = ");
+                json_string_escape(op->nb->name, &actions);
+                ds_put_cstr(&actions, "; output;");
+                ovn_lflow_add(&lflows, op->od, P_IN, 1, 50,
+                              ds_cstr(&match), ds_cstr(&actions));
+                ds_destroy(&actions);
+                ds_destroy(&match);
+            } else if (!strcmp(op->nb->macs[i], "unknown")) {
+                ovn_multicast_add(&mcgroups, &mc_unknown, op);
+                op->od->has_unknown = true;
+            } else {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
 
-            NBREC_ACL_FOR_EACH (acl, ctx->ovnnb_idl) {
-                lflow_add(&pc, lswitch, 2, acl->priority, acl->match,
-                          (!strcmp(acl->action, "allow") ||
-                           !strcmp(acl->action, "allow-related")
-                           ? "next;" : "drop;"));
+                VLOG_INFO_RL(&rl, "%s: invalid syntax '%s' in macs column",
+                             op->nb->name, op->nb->macs[i]);
             }
         }
-
-        lflow_add(&pc, lswitch, 2, 0, "1", "next;");
     }
 
-    /* Table 3: Egress port security. */
-    NBREC_LOGICAL_SWITCH_FOR_EACH (lswitch, ctx->ovnnb_idl) {
-        lflow_add(&pc, lswitch, 3, 100, "eth.dst[40]", "output;");
-
-        for (size_t i = 0; i < lswitch->n_ports; i++) {
-            const struct nbrec_logical_port *lport = lswitch->ports[i];
-            struct ds match;
-
-            ds_init(&match);
-            ds_put_cstr(&match, "outport == ");
-            json_string_escape(lport->name, &match);
-            build_port_security("eth.dst",
-                                lport->port_security, lport->n_port_security,
-                                &match);
-
-            lflow_add(&pc, lswitch, 3, 50, ds_cstr(&match),
-                      lport_is_enabled(lport) ? "output;" : "drop;");
-
-            ds_destroy(&match);
+    /* Ingress table 1: Destination lookup for unknown MACs (priority 0). */
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        if (od->has_unknown) {
+            ovn_lflow_add(&lflows, od, P_IN, 1, 0, "1",
+                          "outport = \""MC_UNKNOWN"\"; output;");
         }
     }
 
-    /* Delete any existing Logical_Flow rows that were not re-generated.  */
-    struct lflow_hash_node *hash_node, *next_hash_node;
-    HMAP_FOR_EACH_SAFE (hash_node, next_hash_node, node, &pc.lflow_hmap) {
-        hmap_remove(&pc.lflow_hmap, &hash_node->node);
-        sbrec_logical_flow_delete(hash_node->lflow);
-        free(hash_node);
+    /* Egress table 0: ACLs (any priority). */
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        for (size_t i = 0; i < od->nb->n_acls; i++) {
+            const struct nbrec_acl *acl = od->nb->acls[i];
+            const char *action;
+
+            action = (!strcmp(acl->action, "allow") ||
+                      !strcmp(acl->action, "allow-related"))
+                ? "next;" : "drop;";
+            ovn_lflow_add(&lflows, od, P_OUT, 0, acl->priority, acl->match,
+                          action);
+        }
     }
-    hmap_destroy(&pc.lflow_hmap);
-}
-\f
-/*
- * Take two string columns and return true if:
- *  - neither are set
- *  - both are set and the strings are equal
- */
-static bool
-strings_equal(const char *s1, const char *s2)
-{
-    if (!!s1 != !!s2) {
-        /* One is set and the other is not. */
-        return false;
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        ovn_lflow_add(&lflows, od, P_OUT, 0, 0, "1", "next;");
     }
 
-    if (s1) {
-        /* Both are set. */
-        return strcmp(s1, s2) ? false : true;
+    /* Egress table 1: Egress port security multicast/broadcast (priority
+     * 100). */
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        ovn_lflow_add(&lflows, od, P_OUT, 1, 100, "eth.dst[40]", "output;");
     }
 
-    /* Both are NULL. */
-    return true;
-}
+    /* Egress table 1: Egress port security (priority 50). */
+    HMAP_FOR_EACH (op, key_node, ports) {
+        struct ds match;
 
-/*
- * Take two int64_t columns and return true if either:
- *  - neither are set
- *  - both are set and the first integers in each are equal
- */
-static bool
-int64_first_equal(int64_t *i1, size_t n_i1, int64_t *i2, size_t n_i2)
-{
-    if (n_i1 != n_i2) {
-        return false;
-    }
-
-    return i1 ? (i1[0] == i2[0]) : true;
-}
+        ds_init(&match);
+        ds_put_cstr(&match, "outport == ");
+        json_string_escape(op->key, &match);
+        build_port_security("eth.dst",
+                            op->nb->port_security, op->nb->n_port_security,
+                            &match);
 
-struct port_binding_hash_node {
-    struct hmap_node lp_node; /* In 'lp_map', by binding->logical_port. */
-    struct hmap_node tk_node; /* In 'tk_map', by binding->tunnel_key. */
-    const struct sbrec_port_binding *binding;
-};
+        ovn_lflow_add(&lflows, op->od, P_OUT, 1, 50, ds_cstr(&match),
+                      lport_is_enabled(op->nb) ? "output;" : "drop;");
 
-static bool
-tunnel_key_in_use(const struct hmap *tk_hmap, uint16_t tunnel_key)
-{
-    const struct port_binding_hash_node *hash_node;
-
-    HMAP_FOR_EACH_IN_BUCKET (hash_node, tk_node, hash_int(tunnel_key, 0),
-                             tk_hmap) {
-        if (hash_node->binding->tunnel_key == tunnel_key) {
-            return true;
-        }
+        ds_destroy(&match);
     }
-    return false;
-}
-
-/* Chooses and returns a positive tunnel key that is not already in use in
- * 'tk_hmap'.  Returns 0 if all tunnel keys are in use. */
-static uint16_t
-choose_tunnel_key(const struct hmap *tk_hmap)
-{
-    static uint16_t prev;
 
-    for (uint16_t key = prev + 1; key != prev; key++) {
-        if (!tunnel_key_in_use(tk_hmap, key)) {
-            prev = key;
-            return key;
+    /* Push changes to the Logical_Flow table to database. */
+    const struct sbrec_logical_flow *sbflow, *next_sbflow;
+    SBREC_LOGICAL_FLOW_FOR_EACH_SAFE (sbflow, next_sbflow, ctx->ovnsb_idl) {
+        struct ovn_datapath *od
+            = ovn_datapath_from_sbrec(datapaths, sbflow->logical_datapath);
+        if (!od) {
+            sbrec_logical_flow_delete(sbflow);
+            continue;
         }
-    }
-
-    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-    VLOG_WARN_RL(&rl, "all tunnel keys exhausted");
-    return 0;
-}
 
-/*
- * When a change has occurred in the OVN_Northbound database, we go through and
- * make sure that the contents of the Port_Binding table in the OVN_Southbound
- * database are up to date with the logical ports defined in the
- * OVN_Northbound database.
- */
-static void
-set_port_bindings(struct northd_context *ctx)
-{
-    const struct sbrec_port_binding *binding;
-
-    /*
-     * We will need to look up a port binding for every logical port.  We don't
-     * want to have to do an O(n) search for every binding, so start out by
-     * hashing them on the logical port.
-     *
-     * As we go through every logical port, we will update the binding if it
-     * exists or create one otherwise.  When the update is done, we'll remove
-     * it from the hashmap.  At the end, any bindings left in the hashmap are
-     * for logical ports that have been deleted.
-     *
-     * We index the logical_port column because that's the shared key between
-     * the OVN_NB and OVN_SB databases.  We index the tunnel_key column to
-     * allow us to choose a unique tunnel key for any Port_Binding rows we have
-     * to add.
-     */
-    struct hmap lp_hmap = HMAP_INITIALIZER(&lp_hmap);
-    struct hmap tk_hmap = HMAP_INITIALIZER(&tk_hmap);
-
-    SBREC_PORT_BINDING_FOR_EACH(binding, ctx->ovnsb_idl) {
-        struct port_binding_hash_node *hash_node = xzalloc(sizeof *hash_node);
-        hash_node->binding = binding;
-        hmap_insert(&lp_hmap, &hash_node->lp_node,
-                    hash_string(binding->logical_port, 0));
-        hmap_insert(&tk_hmap, &hash_node->tk_node,
-                    hash_int(binding->tunnel_key, 0));
-    }
-
-    const struct nbrec_logical_switch *lswitch;
-    NBREC_LOGICAL_SWITCH_FOR_EACH (lswitch, ctx->ovnnb_idl) {
-        const struct uuid *logical_datapath = &lswitch->header_.uuid;
-
-        for (size_t i = 0; i < lswitch->n_ports; i++) {
-            const struct nbrec_logical_port *lport = lswitch->ports[i];
-            struct port_binding_hash_node *hash_node;
-            binding = NULL;
-            HMAP_FOR_EACH_WITH_HASH(hash_node, lp_node,
-                                    hash_string(lport->name, 0), &lp_hmap) {
-                if (!strcmp(lport->name, hash_node->binding->logical_port)) {
-                    binding = hash_node->binding;
-                    break;
-                }
-            }
-
-            if (binding) {
-                /* We found an existing binding for this logical port.  Update
-                 * its contents. */
-
-                hmap_remove(&lp_hmap, &hash_node->lp_node);
-
-                if (!macs_equal(binding->mac, binding->n_mac,
-                                lport->macs, lport->n_macs)) {
-                    sbrec_port_binding_set_mac(binding,
-                                               (const char **) lport->macs,
-                                               lport->n_macs);
-                }
-                if (!strings_equal(binding->parent_port, lport->parent_name)) {
-                    sbrec_port_binding_set_parent_port(binding,
-                                                       lport->parent_name);
-                }
-                if (!int64_first_equal(binding->tag, binding->n_tag,
-                                       lport->tag, lport->n_tag)) {
-                    sbrec_port_binding_set_tag(binding,
-                                               lport->tag, lport->n_tag);
-                }
-                if (!uuid_equals(&binding->logical_datapath,
-                                 logical_datapath)) {
-                    sbrec_port_binding_set_logical_datapath(binding,
-                                                            *logical_datapath);
-                }
-                if (!strings_equal(binding->type, lport->type)) {
-                    sbrec_port_binding_set_type(binding, lport->type);
-                }
-                if (!smap_equal(&binding->options, &lport->options)) {
-                    sbrec_port_binding_set_options(binding, &lport->options);
-                }
-            } else {
-                /* There is no binding for this logical port, so create one. */
-
-                uint16_t tunnel_key = choose_tunnel_key(&tk_hmap);
-                if (!tunnel_key) {
-                    continue;
-                }
-
-                binding = sbrec_port_binding_insert(ctx->ovnsb_txn);
-                sbrec_port_binding_set_logical_port(binding, lport->name);
-                sbrec_port_binding_set_mac(binding,
-                                           (const char **) lport->macs,
-                                           lport->n_macs);
-                if (lport->parent_name && lport->n_tag > 0) {
-                    sbrec_port_binding_set_parent_port(binding,
-                                                       lport->parent_name);
-                    sbrec_port_binding_set_tag(binding,
-                                               lport->tag, lport->n_tag);
-                }
-
-                sbrec_port_binding_set_tunnel_key(binding, tunnel_key);
-                sbrec_port_binding_set_logical_datapath(binding,
-                                                        *logical_datapath);
-
-                sbrec_port_binding_set_type(binding, lport->type);
-                sbrec_port_binding_set_options(binding, &lport->options);
-
-                /* Add the tunnel key to the tk_hmap so that we don't try to
-                 * use it for another port.  (We don't want it in the lp_hmap
-                 * because that would just get the Binding record deleted
-                 * later.) */
-                struct port_binding_hash_node *hash_node
-                    = xzalloc(sizeof *hash_node);
-                hash_node->binding = binding;
-                hmap_insert(&tk_hmap, &hash_node->tk_node,
-                            hash_int(binding->tunnel_key, 0));
-            }
+        struct ovn_lflow *lflow = ovn_lflow_find(
+            &lflows, od, (!strcmp(sbflow->pipeline, "ingress") ? P_IN : P_OUT),
+            sbflow->table_id, sbflow->priority,
+            sbflow->match, sbflow->actions);
+        if (lflow) {
+            ovn_lflow_destroy(&lflows, lflow);
+        } else {
+            sbrec_logical_flow_delete(sbflow);
         }
     }
-
-    struct port_binding_hash_node *hash_node;
-    HMAP_FOR_EACH (hash_node, lp_node, &lp_hmap) {
-        hmap_remove(&lp_hmap, &hash_node->lp_node);
-        sbrec_port_binding_delete(hash_node->binding);
+    struct ovn_lflow *lflow, *next_lflow;
+    HMAP_FOR_EACH_SAFE (lflow, next_lflow, hmap_node, &lflows) {
+        sbflow = sbrec_logical_flow_insert(ctx->ovnsb_txn);
+        sbrec_logical_flow_set_logical_datapath(sbflow, lflow->od->sb);
+        sbrec_logical_flow_set_pipeline(
+            sbflow, lflow->pipeline == P_IN ? "ingress" : "egress");
+        sbrec_logical_flow_set_table_id(sbflow, lflow->table_id);
+        sbrec_logical_flow_set_priority(sbflow, lflow->priority);
+        sbrec_logical_flow_set_match(sbflow, lflow->match);
+        sbrec_logical_flow_set_actions(sbflow, lflow->actions);
+        ovn_lflow_destroy(&lflows, lflow);
     }
-    hmap_destroy(&lp_hmap);
+    hmap_destroy(&lflows);
+
+    /* Push changes to the Multicast_Group table to database. */
+    const struct sbrec_multicast_group *sbmc, *next_sbmc;
+    SBREC_MULTICAST_GROUP_FOR_EACH_SAFE (sbmc, next_sbmc, ctx->ovnsb_idl) {
+        struct ovn_datapath *od = ovn_datapath_from_sbrec(datapaths,
+                                                          sbmc->datapath);
+        if (!od) {
+            sbrec_multicast_group_delete(sbmc);
+            continue;
+        }
 
-    struct port_binding_hash_node *hash_node_next;
-    HMAP_FOR_EACH_SAFE (hash_node, hash_node_next, tk_node, &tk_hmap) {
-        hmap_remove(&tk_hmap, &hash_node->tk_node);
-        free(hash_node);
+        struct multicast_group group = { .name = sbmc->name,
+                                         .key = sbmc->tunnel_key };
+        struct ovn_multicast *mc = ovn_multicast_find(&mcgroups, od, &group);
+        if (mc) {
+            ovn_multicast_update_sbrec(mc, sbmc);
+            ovn_multicast_destroy(&mcgroups, mc);
+        } else {
+            sbrec_multicast_group_delete(sbmc);
+        }
+    }
+    struct ovn_multicast *mc, *next_mc;
+    HMAP_FOR_EACH_SAFE (mc, next_mc, hmap_node, &mcgroups) {
+        sbmc = sbrec_multicast_group_insert(ctx->ovnsb_txn);
+        sbrec_multicast_group_set_datapath(sbmc, mc->datapath->sb);
+        sbrec_multicast_group_set_name(sbmc, mc->group->name);
+        sbrec_multicast_group_set_tunnel_key(sbmc, mc->group->key);
+        ovn_multicast_update_sbrec(mc, sbmc);
+        ovn_multicast_destroy(&mcgroups, mc);
     }
-    hmap_destroy(&tk_hmap);
+    hmap_destroy(&mcgroups);
 }
-
+\f
 static void
 ovnnb_db_changed(struct northd_context *ctx)
 {
     VLOG_DBG("ovn-nb db contents have changed.");
 
-    set_port_bindings(ctx);
-    build_lflow(ctx);
+    struct hmap datapaths, ports;
+    build_datapaths(ctx, &datapaths);
+    build_ports(ctx, &datapaths, &ports);
+    build_lflows(ctx, &datapaths, &ports);
+
+    struct ovn_datapath *dp, *next_dp;
+    HMAP_FOR_EACH_SAFE (dp, next_dp, key_node, &datapaths) {
+        ovn_datapath_destroy(&datapaths, dp);
+    }
+    hmap_destroy(&datapaths);
+
+    struct ovn_port *port, *next_port;
+    HMAP_FOR_EACH_SAFE (port, next_port, key_node, &ports) {
+        ovn_port_destroy(&ports, port);
+    }
+    hmap_destroy(&ports);
 }
 
 /*
@@ -640,48 +900,48 @@ static void
 ovnsb_db_changed(struct northd_context *ctx)
 {
     struct hmap lports_hmap;
-    const struct sbrec_port_binding *binding;
-    const struct nbrec_logical_port *lport;
+    const struct sbrec_port_binding *sb;
+    const struct nbrec_logical_port *nb;
 
     struct lport_hash_node {
         struct hmap_node node;
-        const struct nbrec_logical_port *lport;
+        const struct nbrec_logical_port *nb;
     } *hash_node, *hash_node_next;
 
     VLOG_DBG("Recalculating port up states for ovn-nb db.");
 
     hmap_init(&lports_hmap);
 
-    NBREC_LOGICAL_PORT_FOR_EACH(lport, ctx->ovnnb_idl) {
+    NBREC_LOGICAL_PORT_FOR_EACH(nb, ctx->ovnnb_idl) {
         hash_node = xzalloc(sizeof *hash_node);
-        hash_node->lport = lport;
-        hmap_insert(&lports_hmap, &hash_node->node,
-                hash_string(lport->name, 0));
+        hash_node->nb = nb;
+        hmap_insert(&lports_hmap, &hash_node->node, hash_string(nb->name, 0));
     }
 
-    SBREC_PORT_BINDING_FOR_EACH(binding, ctx->ovnsb_idl) {
-        lport = NULL;
+    SBREC_PORT_BINDING_FOR_EACH(sb, ctx->ovnsb_idl) {
+        nb = NULL;
         HMAP_FOR_EACH_WITH_HASH(hash_node, node,
-                hash_string(binding->logical_port, 0), &lports_hmap) {
-            if (!strcmp(binding->logical_port, hash_node->lport->name)) {
-                lport = hash_node->lport;
+                                hash_string(sb->logical_port, 0),
+                                &lports_hmap) {
+            if (!strcmp(sb->logical_port, hash_node->nb->name)) {
+                nb = hash_node->nb;
                 break;
             }
         }
 
-        if (!lport) {
+        if (!nb) {
             /* The logical port doesn't exist for this port binding.  This can
              * happen under normal circumstances when ovn-northd hasn't gotten
              * around to pruning the Port_Binding yet. */
             continue;
         }
 
-        if (binding->chassis && (!lport->up || !*lport->up)) {
+        if (sb->chassis && (!nb->up || !*nb->up)) {
             bool up = true;
-            nbrec_logical_port_set_up(lport, &up, 1);
-        } else if (!binding->chassis && (!lport->up || *lport->up)) {
+            nbrec_logical_port_set_up(nb, &up, 1);
+        } else if (!sb->chassis && (!nb->up || *nb->up)) {
             bool up = false;
-            nbrec_logical_port_set_up(lport, &up, 1);
+            nbrec_logical_port_set_up(nb, &up, 1);
         }
     }
 
@@ -771,6 +1031,14 @@ parse_options(int argc OVS_UNUSED, char *argv[] OVS_UNUSED)
     free(short_options);
 }
 
+static void
+add_column_noalert(struct ovsdb_idl *idl,
+                   const struct ovsdb_idl_column *column)
+{
+    ovsdb_idl_add_column(idl, column);
+    ovsdb_idl_omit_alert(idl, column);
+}
+
 int
 main(int argc, char *argv[])
 {
@@ -810,30 +1078,37 @@ main(int argc, char *argv[])
     ctx.ovnnb_idl = ovnnb_idl = ovsdb_idl_create(ovnnb_db,
             &nbrec_idl_class, true, true);
 
-    /* There is only a small subset of changes to the ovn-sb db that ovn-northd
-     * has to care about, so we'll enable monitoring those directly. */
     ctx.ovnsb_idl = ovnsb_idl = ovsdb_idl_create(ovnsb_db,
             &sbrec_idl_class, false, true);
+
+    ovsdb_idl_add_table(ovnsb_idl, &sbrec_table_logical_flow);
+    add_column_noalert(ovnsb_idl, &sbrec_logical_flow_col_logical_datapath);
+    add_column_noalert(ovnsb_idl, &sbrec_logical_flow_col_pipeline);
+    add_column_noalert(ovnsb_idl, &sbrec_logical_flow_col_table_id);
+    add_column_noalert(ovnsb_idl, &sbrec_logical_flow_col_priority);
+    add_column_noalert(ovnsb_idl, &sbrec_logical_flow_col_match);
+    add_column_noalert(ovnsb_idl, &sbrec_logical_flow_col_actions);
+
+    ovsdb_idl_add_table(ovnsb_idl, &sbrec_table_multicast_group);
+    add_column_noalert(ovnsb_idl, &sbrec_multicast_group_col_datapath);
+    add_column_noalert(ovnsb_idl, &sbrec_multicast_group_col_tunnel_key);
+    add_column_noalert(ovnsb_idl, &sbrec_multicast_group_col_name);
+    add_column_noalert(ovnsb_idl, &sbrec_multicast_group_col_ports);
+
+    ovsdb_idl_add_table(ovnsb_idl, &sbrec_table_datapath_binding);
+    add_column_noalert(ovnsb_idl, &sbrec_datapath_binding_col_tunnel_key);
+    add_column_noalert(ovnsb_idl, &sbrec_datapath_binding_col_external_ids);
+
     ovsdb_idl_add_table(ovnsb_idl, &sbrec_table_port_binding);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_port_binding_col_logical_port);
+    add_column_noalert(ovnsb_idl, &sbrec_port_binding_col_datapath);
+    add_column_noalert(ovnsb_idl, &sbrec_port_binding_col_logical_port);
+    add_column_noalert(ovnsb_idl, &sbrec_port_binding_col_tunnel_key);
+    add_column_noalert(ovnsb_idl, &sbrec_port_binding_col_parent_port);
+    add_column_noalert(ovnsb_idl, &sbrec_port_binding_col_tag);
+    add_column_noalert(ovnsb_idl, &sbrec_port_binding_col_type);
+    add_column_noalert(ovnsb_idl, &sbrec_port_binding_col_options);
+    add_column_noalert(ovnsb_idl, &sbrec_port_binding_col_mac);
     ovsdb_idl_add_column(ovnsb_idl, &sbrec_port_binding_col_chassis);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_port_binding_col_mac);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_port_binding_col_tag);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_port_binding_col_parent_port);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_port_binding_col_logical_datapath);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_port_binding_col_tunnel_key);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_port_binding_col_type);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_port_binding_col_options);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_logical_flow_col_logical_datapath);
-    ovsdb_idl_omit_alert(ovnsb_idl, &sbrec_logical_flow_col_logical_datapath);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_logical_flow_col_table_id);
-    ovsdb_idl_omit_alert(ovnsb_idl, &sbrec_logical_flow_col_table_id);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_logical_flow_col_priority);
-    ovsdb_idl_omit_alert(ovnsb_idl, &sbrec_logical_flow_col_priority);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_logical_flow_col_match);
-    ovsdb_idl_omit_alert(ovnsb_idl, &sbrec_logical_flow_col_match);
-    ovsdb_idl_add_column(ovnsb_idl, &sbrec_logical_flow_col_actions);
-    ovsdb_idl_omit_alert(ovnsb_idl, &sbrec_logical_flow_col_actions);
 
     /*
      * The loop here just runs the IDL in a loop waiting for the seqno to
index 6e976ab100ac6f589123417c85907774b67eacb5..1b537f9feca60905d05519a0770444b0574d64aa 100644 (file)
@@ -98,7 +98,7 @@
         OVN/CMS Plugin.  The database schema is meant to be ``impedance
         matched'' with the concepts used in a CMS, so that it directly supports
         notions of logical switches, routers, ACLs, and so on.  See
-        <code>ovs-nb</code>(5) for details.
+        <code>ovn-nb</code>(5) for details.
       </p>
 
       <p>
     </li>
 
     <li>
-      <code>ovn-northd</code> receives the OVN Northbound database update.
-      In turn, it makes the corresponding updates to the OVN Southbound
-      database, by adding rows to the OVN Southbound database
-      <code>Logical_Flow</code> table to reflect the new port, e.g. add a
-      flow to recognize that packets destined to the new port's MAC
-      address should be delivered to it, and update the flow that
-      delivers broadcast and multicast packets to include the new port.
-      It also creates a record in the <code>Binding</code> table and
-      populates all its columns except the column that identifies the
+      <code>ovn-northd</code> receives the OVN Northbound database update.  In
+      turn, it makes the corresponding updates to the OVN Southbound database,
+      by adding rows to the OVN Southbound database <code>Logical_Flow</code>
+      table to reflect the new port, e.g. add a flow to recognize that packets
+      destined to the new port's MAC address should be delivered to it, and
+      update the flow that delivers broadcast and multicast packets to include
+      the new port.  It also creates a record in the <code>Binding</code> table
+      and populates all its columns except the column that identifies the
       <code>chassis</code>.
     </li>
 
     <li>
       On every hypervisor, <code>ovn-controller</code> receives the
       <code>Logical_Flow</code> table updates that <code>ovn-northd</code> made
-      in the previous step.  As long as the VM that owns the VIF is powered off,
-      <code>ovn-controller</code> cannot do much; it cannot, for example,
+      in the previous step.  As long as the VM that owns the VIF is powered
+      off, <code>ovn-controller</code> cannot do much; it cannot, for example,
       arrange to send packets to or receive packets from the VIF, because the
       VIF does not actually exist anywhere.
     </li>
       <code>Binding</code> table.  This provides <code>ovn-controller</code>
       the physical location of the logical port, so each instance updates the
       OpenFlow tables of its switch (based on logical datapath flows in the OVN
-      DB <code>Logical_Flow</code> table) so that packets to and from the VIF can
-      be properly handled via tunnels.
+      DB <code>Logical_Flow</code> table) so that packets to and from the VIF
+      can be properly handled via tunnels.
     </li>
 
     <li>
 
     <li>
       <code>ovn-northd</code> receives the OVN Northbound update and in turn
-      updates the OVN Southbound database accordingly, by removing or
-      updating the rows from the OVN Southbound database
-      <code>Logical_Flow</code> table and <code>Binding</code> table that
-      were related to the now-destroyed VIF.
+      updates the OVN Southbound database accordingly, by removing or updating
+      the rows from the OVN Southbound database <code>Logical_Flow</code> table
+      and <code>Binding</code> table that were related to the now-destroyed
+      VIF.
     </li>
 
     <li>
       On every hypervisor, <code>ovn-controller</code> receives the
       <code>Logical_Flow</code> table updates that <code>ovn-northd</code> made
-      in the previous step.  <code>ovn-controller</code> updates OpenFlow tables
-      to reflect the update, although there may not be much to do, since the VIF
-      had already become unreachable when it was removed from the
+      in the previous step.  <code>ovn-controller</code> updates OpenFlow
+      tables to reflect the update, although there may not be much to do, since
+      the VIF had already become unreachable when it was removed from the
       <code>Binding</code> table in a previous step.
     </li>
   </ol>
     </li>
 
     <li>
-      <code>ovn-northd</code> receives the OVN Northbound database update.
-      In turn, it makes the corresponding updates to the OVN Southbound
-      database, by adding rows to the OVN Southbound database's
-      <code>Logical_Flow</code> table to reflect the new port and also by
-      creating a new row in the <code>Binding</code> table and
-      populating all its columns except the column that identifies the
-      <code>chassis</code>.
+      <code>ovn-northd</code> receives the OVN Northbound database update.  In
+      turn, it makes the corresponding updates to the OVN Southbound database,
+      by adding rows to the OVN Southbound database's <code>Logical_Flow</code>
+      table to reflect the new port and also by creating a new row in the
+      <code>Binding</code> table and populating all its columns except the
+      column that identifies the <code>chassis</code>.
     </li>
 
     <li>
 
     <li>
       <code>ovn-northd</code> receives the OVN Northbound update and in turn
-      updates the OVN Southbound database accordingly, by removing or
-      updating the rows from the OVN Southbound database
-      <code>Logical_Flow</code> table that were related to the now-destroyed
-      CIF.  It also deletes the row in the <code>Binding</code> table
-      for that CIF.
+      updates the OVN Southbound database accordingly, by removing or updating
+      the rows from the OVN Southbound database <code>Logical_Flow</code> table
+      that were related to the now-destroyed CIF.  It also deletes the row in
+      the <code>Binding</code> table for that CIF.
     </li>
 
     <li>
     </li>
   </ol>
 
-  <h1>Design Decisions</h1>
+  <h2>Life Cycle of a Packet</h2>
 
-  <h2>Supported Tunnel Encapsulations</h2>
   <p>
-    For connecting hypervisors to each other, the only supported tunnel
-    encapsulations are Geneve and STT. Hypervisors may use VXLAN to
-    connect to gateways. We have limited support to these encapsulations
-    for the following reasons:
+    This section describes how a packet travels from one virtual machine or
+    container to another through OVN.  This description focuses on the physical
+    treatment of a packet; for a description of the logical life cycle of a
+    packet, please refer to the <code>Logical_Flow</code> table in
+    <code>ovn-sb</code>(5).
   </p>
 
-  <ul>
+  <p>
+    This section mentions several data and metadata fields, for clarity
+    summarized here:
+  </p>
+
+  <dl>
+    <dt>tunnel key</dt>
+    <dd>
+      When OVN encapsulates a packet in Geneve or another tunnel, it attaches
+      extra data to it to allow the receiving OVN instance to process it
+      correctly.  This takes different forms depending on the particular
+      encapsulation, but in each case we refer to it here as the ``tunnel
+      key.''  See <code>Tunnel Encapsulations</code>, below, for details.
+    </dd>
+
+    <dt>logical datapath field</dt>
+    <dd>
+      A field that denotes the logical datapath through which a packet is being
+      processed.  OVN uses the field that OpenFlow 1.1+ simply (and
+      confusingly) calls ``metadata'' to store the logical datapath.  (This
+      field is passed across tunnels as part of the tunnel key.)
+    </dd>
+
+    <dt>logical input port field</dt>
+    <dd>
+      A field that denotes the logical port from which the packet entered the
+      logical datapath.  OVN stores this in a Nicira extension register.  (This
+      field is passed across tunnels as part of the tunnel key.)
+    </dd>
+
+    <dt>logical output port field</dt>
+    <dd>
+      A field that denotes the logical port from which the packet will leave
+      the logical datapath.  This is initialized to 0 at the beginning of the
+      logical ingress pipeline.  OVN stores this in a Nicira extension
+      register.  (This field is passed across tunnels as part of the tunnel
+      key.)
+    </dd>
+
+    <dt>VLAN ID</dt>
+    <dd>
+      The VLAN ID is used as an interface between OVN and containers nested
+      inside a VM (see <code>Life Cycle of a container interface inside a
+      VM</code>, above, for more information).
+    </dd>
+  </dl>
+
+  <p>
+    Initially, a VM or container on the ingress hypervisor sends a packet on a
+    port attached to the OVN integration bridge.  Then:
+  </p>
+
+  <ol>
     <li>
       <p>
-        They support large amounts of metadata.  In addition to
-        specifying the logical switch, we will likely want to indicate
-        the logical source port and where we are in the logical
-        pipeline.  Geneve supports a 24-bit VNI field and TLV-based
-        extensions.  The header of STT includes a 64-bit context id.
+        OpenFlow table 0 performs physical-to-logical translation.  It matches
+        the packet's ingress port.  Its actions annotate the packet with
+        logical metadata, by setting the logical datapath field to identify the
+        logical datapath that the packet is traversing and the logical input
+        port field to identify the ingress port.  Then it resubmits to table 16
+        to enter the logical ingress pipeline.
+      </p>
+
+      <p>
+        Packets that originate from a container nested within a VM are treated
+        in a slightly different way.  The originating container can be
+        distinguished based on the VIF-specific VLAN ID, so the
+        physical-to-logical translation flows additionally match on VLAN ID and
+        the actions strip the VLAN header.  Following this step, OVN treats
+        packets from containers just like any other packets.
+      </p>
+
+      <p>
+        Table 0 also processes packets that arrive from other chassis.  It
+        distinguishes them from other packets by ingress port, which is a
+        tunnel.  As with packets just entering the OVN pipeline, the actions
+        annotate these packets with logical datapath and logical ingress port
+        metadata.  In addition, the actions set the logical output port field,
+        which is available because in OVN tunneling occurs after the logical
+        output port is known.  These three pieces of information are obtained
+        from the tunnel encapsulation metadata (see <code>Tunnel
+        Encapsulations</code> for encoding details).  Then the actions resubmit
+        to table 33 to enter the logical egress pipeline.
       </p>
     </li>
 
     <li>
       <p>
-        They use randomized UDP or TCP source ports that allows
-        efficient distribution among multiple paths in environments that
-        use ECMP in their underlay.
+        OpenFlow tables 16 through 31 execute the logical ingress pipeline from
+        the <code>Logical_Flow</code> table in the OVN Southbound database.
+        These tables are expressed entirely in terms of logical concepts like
+        logical ports and logical datapaths.  A big part of
+        <code>ovn-controller</code>'s job is to translate them into equivalent
+        OpenFlow (in particular it translates the table numbers:
+        <code>Logical_Flow</code> tables 0 through 15 become OpenFlow tables 16
+        through 31).  For a given packet, the logical ingress pipeline
+        eventually executes zero or more <code>output</code> actions:
       </p>
+
+      <ul>
+        <li>
+          If the pipeline executes no <code>output</code> actions at all, the
+          packet is effectively dropped.
+        </li>
+
+        <li>
+          Most commonly, the pipeline executes one <code>output</code> action,
+          which <code>ovn-controller</code> implements by resubmitting the
+          packet to table 32.
+        </li>
+
+        <li>
+          If the pipeline can execute more than one <code>output</code> action,
+          then each one is separately resubmitted to table 32.  This can be
+          used to send multiple copies of the packet to multiple ports.  (If
+          the packet was not modified between the <code>output</code> actions,
+          and some of the copies are destined to the same hypervisor, then
+          using a logical multicast output port would save bandwidth between
+          hypervisors.)
+        </li>
+      </ul>
     </li>
 
     <li>
       <p>
-        NICs are available that accelerate encapsulation and decapsulation.
+        OpenFlow tables 32 through 47 implement the <code>output</code> action
+        in the logical ingress pipeline.  Specifically, table 32 handles
+        packets to remote hypervisors, table 33 handles packets to the local
+        hypervisor, and table 34 discards packets whose logical ingress and
+        egress port are the same.
+      </p>
+
+      <p>
+        Each flow in table 32 matches on a logical output port for unicast or
+        multicast logical ports that include a logical port on a remote
+        hypervisor.  Each flow's actions implement sending a packet to the port
+        it matches.  For unicast logical output ports on remote hypervisors,
+        the actions set the tunnel key to the correct value, then send the
+        packet on the tunnel port to the correct hypervisor.  (When the remote
+        hypervisor receives the packet, table 0 there will recognize it as a
+        tunneled packet and pass it along to table 33.)  For multicast logical
+        output ports, the actions send one copy of the packet to each remote
+        hypervisor, in the same way as for unicast destinations.  If a
+        multicast group includes a logical port or ports on the local
+        hypervisor, then its actions also resubmit to table 33.  Table 32 also
+        includes a fallback flow that resubmits to table 33 if there is no
+        other match.
+      </p>
+
+      <p>
+        Flows in table 33 resemble those in table 32 but for logical ports that
+        reside locally rather than remotely.  For unicast logical output ports
+        on the local hypervisor, the actions just resubmit to table 34.  For
+        multicast output ports that include one or more logical ports on the
+        local hypervisor, for each such logical port <var>P</var>, the actions
+        change the logical output port to <var>P</var>, then resubmit to table
+        34.
+      </p>
+
+      <p>
+        Table 34 matches and drops packets for which the logical input and
+        output ports are the same.  It resubmits other packets to table 48.
       </p>
     </li>
+
+    <li>
+      <p>
+        OpenFlow tables 48 through 63 execute the logical egress pipeline from
+        the <code>Logical_Flow</code> table in the OVN Southbound database.
+        The egress pipeline can perform a final stage of validation before
+        packet delivery.  Eventually, it may execute an <code>output</code>
+        action, which <code>ovn-controller</code> implements by resubmitting to
+        table 64.  A packet for which the pipeline never executes
+        <code>output</code> is effectively dropped (although it may have been
+        transmitted through a tunnel across a physical network).
+      </p>
+
+      <p>
+        The egress pipeline cannot change the logical output port or cause
+        further tunneling.
+      </p>
+    </li>
+
+    <li>
+      <p>
+        OpenFlow table 64 performs logical-to-physical translation, the
+        opposite of table 0.  It matches the packet's logical egress port.  Its
+        actions output the packet to the port attached to the OVN integration
+        bridge that represents that logical port.  If the logical egress port
+        is a container nested with a VM, then before sending the packet the
+        actions push on a VLAN header with an appropriate VLAN ID.
+      </p>
+    </li>
+  </ol>
+
+  <h1>Design Decisions</h1>
+
+  <h2>Tunnel Encapsulations</h2>
+
+  <p>
+    OVN annotates logical network packets that it sends from one hypervisor to
+    another with the following three pieces of metadata, which are encoded in
+    an encapsulation-specific fashion:
+  </p>
+
+  <ul>
+    <li>
+      24-bit logical datapath identifier, from the <code>tunnel_key</code>
+      column in the OVN Southbound <code>Datapath_Binding</code> table.
+    </li>
+
+    <li>
+      15-bit logical ingress port identifier.  ID 0 is reserved for internal
+      use within OVN.  IDs 1 through 32767, inclusive, may be assigned to
+      logical ports (see the <code>tunnel_key</code> column in the OVN
+      Southbound <code>Port_Binding</code> table).
+    </li>
+
+    <li>
+      16-bit logical egress port identifier.  IDs 0 through 32767 have the same
+      meaning as for logical ingress ports.  IDs 32768 through 65535,
+      inclusive, may be assigned to logical multicast groups (see the
+      <code>tunnel_key</code> column in the OVN Southbound
+      <code>Multicast_Group</code> table).
+    </li>
   </ul>
 
   <p>
-    Due to its flexibility, the preferred encapsulation between
-    hypervisors is Geneve.  Some environments may want to use STT for
-    performance reasons until the NICs they use support hardware offload
-    of Geneve.
+    For hypervisor-to-hypervisor traffic, OVN supports only Geneve and STT
+    encapsulations, for the following reasons:
   </p>
 
+  <ul>
+    <li>
+      Only STT and Geneve support the large amounts of metadata (over 32 bits
+      per packet) that OVN uses (as described above).
+    </li>
+
+    <li>
+      STT and Geneve use randomized UDP or TCP source ports that allows
+      efficient distribution among multiple paths in environments that use ECMP
+      in their underlay.
+    </li>
+
+    <li>
+      NICs are available to offload STT and Geneve encapsulation and
+      decapsulation.
+    </li>
+  </ul>
+
+  <p>
+    Due to its flexibility, the preferred encapsulation between hypervisors is
+    Geneve.  For Geneve encapsulation, OVN transmits the logical datapath
+    identifier in the Geneve VNI.
+
+    <!-- Keep the following in sync with ovn/controller/physical.h. -->
+    OVN transmits the logical ingress and logical egress ports in a TLV with
+    class 0xffff, type 0, and a 32-bit value encoded as follows, from MSB to
+    LSB:
+  </p>
+
+  <diagram>
+    <header name="">
+      <bits name="rsv" above="1" below="0" width=".25"/>
+      <bits name="ingress port" above="15" width=".75"/>
+      <bits name="egress port" above="16" width=".75"/>
+    </header>
+  </diagram>
+
+  <p>
+    Environments whose NICs lack Geneve offload may prefer STT encapsulation
+    for performance reasons.  For STT encapsulation, OVN encodes all three
+    pieces of logical metadata in the STT 64-bit tunnel ID as follows, from MSB
+    to LSB:
+  </p>
+
+  <diagram>
+    <header name="">
+      <bits name="reserved" above="9" below="0" width=".5"/>
+      <bits name="ingress port" above="15" width=".75"/>
+      <bits name="egress port" above="16" width=".75"/>
+      <bits name="datapath" above="24" width="1.25"/>
+    </header>
+  </diagram>
+
   <p>
-    For connecting to gateways, the only supported tunnel encapsulations
-    are VXLAN, Geneve, and STT.  While support for Geneve is becoming
-    available for TOR (top-of-rack) switches, VXLAN is far more common.
-    Currently, gateways have a feature set that matches the capabilities
-    as defined by the VTEP schema, so fewer bits of metadata are
-    necessary.  In the future, gateways that do not support
-    encapsulations with large amounts of metadata may continue to have a
-    reduced feature set.
+    For connecting to gateways, in addition to Geneve and STT, OVN supports
+    VXLAN, because only VXLAN support is common on top-of-rack (ToR) switches.
+    Currently, gateways have a feature set that matches the capabilities as
+    defined by the VTEP schema, so fewer bits of metadata are necessary.  In
+    the future, gateways that do not support encapsulations with large amounts
+    of metadata may continue to have a reduced feature set.
   </p>
 </manpage>
index 7b9fbb2a63ec64b703567cf9fccff360a6bcad2e..ade81640365891c6062ab094f8a929c652479991 100644 (file)
     </column>
 
     <column name="match">
-      The packets that the ACL should match, in the same expression
-      language used for the <ref column="match" table="Logical_Flow"
+      The packets that the ACL should match, in the same expression language
+      used for the <ref column="match" table="Logical_Flow"
       db="OVN_Southbound"/> column in the OVN Southbound database's <ref
       table="Logical_Flow" db="OVN_Southbound"/> table.  Match
-      <code>inport</code> and <code>outport</code> against names of
-      logical ports within <ref column="lswitch"/> to implement ingress
-      and egress ACLs, respectively.  In logical switches connected to
-      logical routers, the special port name <code>ROUTER</code> refers
-      to the logical router port.
+      <code>inport</code> and <code>outport</code> against names of logical
+      ports within <ref column="lswitch"/> to implement ingress and egress
+      ACLs, respectively.  In logical switches connected to logical routers,
+      the special port name <code>ROUTER</code> refers to the logical router
+      port.
     </column>
 
     <column name="action">
index e5c1812e70fd7006c5dccc216b9d92b81045190a..40a29e9c5b39e466e9fd27e99be31799c3d4991a 100644 (file)
                                               "max": "unlimited"}}}},
         "Logical_Flow": {
             "columns": {
-                "logical_datapath": {"type": "uuid"},
+                "logical_datapath": {"type": {"key": {"type": "uuid",
+                                                      "refTable": "Datapath_Binding"}}},
+                "pipeline": {"type": {"key": {"type": "string",
+                                      "enum": ["set", ["ingress",
+                                                       "egress"]]}}},
                 "table_id": {"type": {"key": {"type": "integer",
                                               "minInteger": 0,
-                                              "maxInteger": 31}}},
+                                              "maxInteger": 15}}},
                 "priority": {"type": {"key": {"type": "integer",
                                               "minInteger": 0,
                                               "maxInteger": 65535}}},
                 "match": {"type": "string"},
                 "actions": {"type": "string"}},
             "isRoot": true},
+        "Multicast_Group": {
+            "columns": {
+                "datapath": {"type": {"key": {"type": "uuid",
+                                              "refTable": "Datapath_Binding"}}},
+                "name": {"type": "string"},
+                "tunnel_key": {
+                    "type": {"key": {"type": "integer",
+                                     "minInteger": 32768,
+                                     "maxInteger": 65535}}},
+                "ports": {"type": {"key": {"type": "uuid",
+                                           "refTable": "Port_Binding",
+                                           "refType": "weak"},
+                                   "min": 1, "max": "unlimited"}}},
+            "indexes": [["datapath", "tunnel_key"],
+                        ["datapath", "name"]],
+            "isRoot": true},
+        "Datapath_Binding": {
+            "columns": {
+                "tunnel_key": {
+                     "type": {"key": {"type": "integer",
+                                      "minInteger": 1,
+                                      "maxInteger": 16777215}}},
+                "external_ids": {
+                    "type": {"key": "string", "value": "string",
+                             "min": 0, "max": "unlimited"}}},
+            "indexes": [["tunnel_key"]],
+            "isRoot": true},
         "Port_Binding": {
             "columns": {
-                "logical_datapath": {"type": "uuid"},
                 "logical_port": {"type": "string"},
                 "type": {"type": "string"},
                 "options": {
                               "value": "string",
                               "min": 0,
                               "max": "unlimited"}},
+                "datapath": {"type": {"key": {"type": "uuid",
+                                              "refTable": "Datapath_Binding"}}},
                 "tunnel_key": {
                      "type": {"key": {"type": "integer",
                                       "minInteger": 1,
-                                      "maxInteger": 65535}}},
+                                      "maxInteger": 32767}}},
                 "parent_port": {"type": {"key": "string", "min": 0, "max": 1}},
                 "tag": {
                      "type": {"key": {"type": "integer",
                 "mac": {"type": {"key": "string",
                                  "min": 0,
                                  "max": "unlimited"}}},
-            "indexes": [["logical_port"], ["tunnel_key"]],
+            "indexes": [["datapath", "tunnel_key"], ["logical_port"]],
             "isRoot": true}},
     "version": "1.0.0"}
index 4860ec72dedb94dffa8fc3874d8b393963f96e34..baced61fb3a9607c32764377b2a2bc89b14f397c 100644 (file)
   </p>
 
   <p>
-    The <ref table="Logical_Flow"/> table is currently the only LN table.
+    <ref table="Logical_Flow"/> and <ref table="Multicast_Group"/> contain LN
+    data.
   </p>
 
   <h3>Bindings data</h3>
 
   <p>
-    The Binding tables contain the current placement of logical components
-    (such as VMs and VIFs) onto chassis and the bindings between logical ports
-    and MACs.
+    Bindings data link logical and physical components.  They show the current
+    placement of logical components (such as VMs and VIFs) onto chassis, and
+    map logical entities to the values that represent them in tunnel
+    encapsulations.
   </p>
 
   <p>
   </p>
 
   <p>
-    The <ref table="Port_Binding"/> table is currently the only binding data.
+    The <ref table="Port_Binding"/> and <ref table="Datapath_Binding"/> tables
+    contain binding data.
   </p>
 
+  <h2>Common Columns</h2>
+
+  <p>
+    Some tables contain a special column named <code>external_ids</code>.  This
+    column has the same form and purpose each place that it appears, so we
+    describe it here to save space later.
+  </p>
+
+  <dl>
+    <dt><code>external_ids</code>: map of string-string pairs</dt>
+    <dd>
+      Key-value pairs for use by the software that manages the OVN Southbound
+      database rather than by <code>ovn-controller</code>.  In particular,
+      <code>ovn-northd</code> can use key-value pairs in this column to relate
+      entities in the southbound database to higher-level entities (such as
+      entities in the OVN Northbound database).  Individual key-value pairs in
+      this column may be documented in some cases to aid in understanding and
+      troubleshooting, but the reader should not mistake such documentation as
+      comprehensive.
+    </dd>
+  </dl>
+
   <table name="Chassis" title="Physical Network Hypervisor and Gateway Information">
     <p>
       Each row in this table represents a hypervisor or gateway (a chassis) in
     </column>
   </table>
 
-  <table name="Logical_Flow" title="Logical Network Flow">
+  <table name="Logical_Flow" title="Logical Network Flows">
     <p>
       Each row in this table represents one logical flow.  The cloud management
       system, via its OVN integration, populates this table with logical flows
       The default action when no flow matches is to drop packets.
     </p>
 
+    <p><em>Logical Life Cycle of a Packet</em></p>
+
+    <p>
+      This following description focuses on the life cycle of a packet through
+      a logical datapath, ignoring physical details of the implementation.
+      Please refer to <em>Life Cycle of a Packet</em> in
+      <code>ovn-architecture</code>(7) for the physical information.
+    </p>
+
+    <p>
+      The description here is written as if OVN itself executes these steps,
+      but in fact OVN (that is, <code>ovn-controller</code>) programs Open
+      vSwitch, via OpenFlow and OVSDB, to execute them on its behalf.
+    </p>
+
+    <p>
+      At a high level, OVN passes each packet through the logical datapath's
+      logical ingress pipeline, which may output the packet to one or more
+      logical port or logical multicast groups.  For each such logical output
+      port, OVN passes the packet through the datapath's logical egress
+      pipeline, which may either drop the packet or deliver it to the
+      destination.  Between the two pipelines, outputs to logical multicast
+      groups are expanded into logical ports, so that the egress pipeline only
+      processes a single logical output port at a time.  Between the two
+      pipelines is also where, when necessary, OVN encapsulates a packet in a
+      tunnel (or tunnels) to transmit to remote hypervisors.
+    </p>
+
+    <p>
+      In more detail, to start, OVN searches the <ref table="Logical_Flow"/>
+      table for a row with correct <ref column="logical_datapath"/>, a <ref
+      column="pipeline"/> of <code>ingress</code>, a <ref column="table_id"/>
+      of 0, and a <ref column="match"/> that is true for the packet.  If none
+      is found, OVN drops the packet.  If OVN finds more than one, it chooses
+      the match with the highest <ref column="priority"/>.  Then OVN executes
+      each of the actions specified in the row's <ref table="actions"/> column,
+      in the order specified.  Some actions, such as those to modify packet
+      headers, require no further details.  The <code>next</code> and
+      <code>output</code> actions are special.
+    </p>
+
+    <p>
+      The <code>next</code> action causes the above process to be repeated
+      recursively, except that OVN searches for <ref column="table_id"/> of 1
+      instead of 0.  Similarly, any <code>next</code> action in a row found in
+      that table would cause a further search for a <ref column="table_id"/> of
+      2, and so on.  When recursive processing completes, flow control returns
+      to the action following <code>next</code>.
+    </p>
+
+    <p>
+      The <code>output</code> action also introduces recursion.  Its effect
+      depends on the current value of the <code>outport</code> field.  Suppose
+      <code>outport</code> designates a logical port.  First, OVN compares
+      <code>inport</code> to <code>outport</code>; if they are equal, it treats
+      the <code>output</code> as a no-op.  In the common case, where they are
+      different, the packet enters the egress pipeline.  This transition to the
+      egress pipeline discards register data, e.g. <code>reg0</code>
+      ... <code>reg5</code>, to achieve uniform behavior regardless of whether
+      the egress pipeline is on a different hypervisor (because registers
+      aren't preserve across tunnel encapsulation).
+    </p>
+
+    <p>
+      To execute the egress pipeline, OVN again searches the <ref
+      table="Logical_Flow"/> table for a row with correct <ref
+      column="logical_datapath"/>, a <ref column="table_id"/> of 0, a <ref
+      column="match"/> that is true for the packet, but now looking for a <ref
+      column="pipeline"/> of <code>egress</code>.  If no matching row is found,
+      the output becomes a no-op.  Otherwise, OVN executes the actions for the
+      matching flow (which is chosen from multiple, if necessary, as already
+      described).
+    </p>
+
+    <p>
+      In the <code>egress</code> pipeline, the <code>next</code> action acts as
+      already described, except that it, of course, searches for
+      <code>egress</code> flows.  The <code>output</code> action, however, now
+      directly outputs the packet to the output port (which is now fixed,
+      because <code>outport</code> is read-only within the egress pipeline).
+    </p>
+
+    <p>
+      The description earlier assumed that <code>outport</code> referred to a
+      logical port.  If it instead designates a logical multicast group, then
+      the description above still applies, with the addition of fan-out from
+      the logical multicast group to each logical port in the group.  For each
+      member of the group, OVN executes the logical pipeline as described, with
+      the logical output port replaced by the group member.
+    </p>
+
     <column name="logical_datapath">
-      The logical datapath to which the logical flow belongs.  A logical
-      datapath implements a logical pipeline among the ports in the <ref
-      table="Port_Binding"/> table associated with it.  (No table represents a
-      logical datapath.)  In practice, the pipeline in a given logical datapath
-      implements either a logical switch or a logical router, and
-      <code>ovn-northd</code> reuses the UUIDs for those logical entities from
-      the <code>OVN_Northbound</code> for logical datapaths.
+      The logical datapath to which the logical flow belongs.
+    </column>
+
+    <column name="pipeline">
+      <p>
+        The primary flows used for deciding on a packet's destination are the
+        <code>ingress</code> flows.  The <code>egress</code> flows implement
+        ACLs.  See <em>Logical Life Cycle of a Packet</em>, above, for details.
+      </p>
     </column>
 
     <column name="table_id">
 
       <p>
         String constants have the same syntax as quoted strings in JSON (thus,
-        they are Unicode strings).  String constants are used for naming
-        logical ports.  Thus, the useful values are <ref
-        column="logical_port"/> names from the <ref column="Port_Binding"/> and
-        <ref column="Gateway"/> tables in a logical flow's <ref
-        column="logical_datapath"/>.
+        they are Unicode strings).
       </p>
 
       <p>
 
       <p><em>Symbols</em></p>
 
+      <p>
+        Most of the symbols below have integer type.  Only <code>inport</code>
+        and <code>outport</code> have string type.  <code>inport</code> names a
+        logical port.  Thus, its value is a <ref column="logical_port"/> name
+        from the <ref table="Port_Binding"/> or <ref table="Gateway"/> tables.
+        <code>outport</code> may name a logical port, as <code>inport</code>,
+        or a logical multicast group defined in the <ref
+        table="Multicast_Group"/> table.  For both symbols, only names within
+        the flow's logical datapath may be used.
+      </p>
+
       <ul>
-        <li>
-          <code>metadata</code> <code>reg0</code> ... <code>reg7</code>
-          <code>xreg0</code> ... <code>xreg3</code>
-        </li>
-        <li><code>inport</code> <code>outport</code> <code>queue</code></li>
+        <li><code>reg0</code>...<code>reg5</code></li>
+        <li><code>inport</code> <code>outport</code></li>
         <li><code>eth.src</code> <code>eth.dst</code> <code>eth.type</code></li>
         <li><code>vlan.tci</code> <code>vlan.vid</code> <code>vlan.pcp</code> <code>vlan.present</code></li>
         <li><code>ip.proto</code> <code>ip.dscp</code> <code>ip.ecn</code> <code>ip.ttl</code> <code>ip.frag</code></li>
       </p>
 
       <p>
-        The following actions will be initially supported:
+       The following actions are defined:
       </p>
 
       <dl>
         <dt><code>output;</code></dt>
         <dd>
-          Outputs the packet to the logical port current designated by
-          <code>outport</code>.  Output to the ingress port is implicitly
-          dropped, that is, <code>output</code> becomes a no-op if
-          <code>outport</code> == <code>inport</code>.
-        </dd>
+          <p>
+           In the ingress pipeline, this action executes the
+           <code>egress</code> pipeline as a subroutine.  If
+           <code>outport</code> names a logical port, the egress pipeline
+           executes once; if it is a multicast group, the egress pipeline runs
+           once for each logical port in the group.
+          </p>
+
+          <p>
+            In the egress pipeline, this action performs the actual
+            output to the <code>outport</code> logical port.  (In the egress
+            pipeline, <code>outport</code> never names a multicast group.)
+          </p>
+
+          <p>
+            Output to the input port is implicitly dropped, that is,
+            <code>output</code> becomes a no-op if <code>outport</code> ==
+            <code>inport</code>.
+          </p>
+       </dd>
 
         <dt><code>next;</code></dt>
         <dd>
 
         <dt><code><var>field</var> = <var>constant</var>;</code></dt>
         <dd>
-          Sets data or metadata field <var>field</var> to constant value
-          <var>constant</var>, e.g. <code>outport = "vif0";</code> to set the
-          logical output port.  Assigning to a field with prerequisites
-          implicitly adds those prerequisites to <ref column="match"/>; thus,
-          for example, a flow that sets <code>tcp.dst</code> applies only to
-          TCP flows, regardless of whether its <ref column="match"/> mentions
-          any TCP field.  To set only a subset of bits in a field,
-          <var>field</var> may be a subfield or <var>constant</var> may be
-          masked, e.g. <code>vlan.pcp[2] = 1;</code> and <code>vlan.pcp =
-          4/4;</code> both set the most sigificant bit of the VLAN PCP.  Not
-          all fields are modifiable (e.g. <code>eth.type</code> and
-          <code>ip.proto</code> are read-only), and not all modifiable fields
-          may be partially modified (e.g. <code>ip.ttl</code> must assigned as
-          a whole).
-        </dd>
+          <p>
+           Sets data or metadata field <var>field</var> to constant value
+           <var>constant</var>, e.g. <code>outport = "vif0";</code> to set the
+           logical output port.  To set only a subset of bits in a field,
+           specify a subfield for <var>field</var> or a masked
+           <var>constant</var>, e.g. one may use <code>vlan.pcp[2] = 1;</code>
+           or <code>vlan.pcp = 4/4;</code> to set the most sigificant bit of
+           the VLAN PCP.
+          </p>
+
+          <p>
+            Assigning to a field with prerequisites implicitly adds those
+            prerequisites to <ref column="match"/>; thus, for example, a flow
+            that sets <code>tcp.dst</code> applies only to TCP flows,
+            regardless of whether its <ref column="match"/> mentions any TCP
+            field.
+          </p>
+
+          <p>
+            Not all fields are modifiable (e.g. <code>eth.type</code> and
+            <code>ip.proto</code> are read-only), and not all modifiable fields
+            may be partially modified (e.g. <code>ip.ttl</code> must assigned
+            as a whole).  The <code>outport</code> field is modifiable in the
+            <code>ingress</code> pipeline but not in the <code>egress</code>
+            pipeline.
+          </p>
+       </dd>
       </dl>
 
       <p>
     </column>
   </table>
 
+  <table name="Multicast_Group" title="Logical Port Multicast Groups">
+    <p>
+      The rows in this table define multicast groups of logical ports.
+      Multicast groups allow a single packet transmitted over a tunnel to a
+      hypervisor to be delivered to multiple VMs on that hypervisor, which
+      uses bandwidth more efficiently.
+    </p>
+
+    <p>
+      Each row in this table defines a logical multicast group numbered <ref
+      column="tunnel_key"/> within <ref column="datapath"/>, whose logical
+      ports are listed in the <ref column="ports"/> column.
+    </p>
+
+    <column name="datapath">
+      The logical datapath in which the multicast group resides.
+    </column>
+
+    <column name="tunnel_key">
+      The value used to designate this logical egress port in tunnel
+      encapsulations.  An index forces the key to be unique within the <ref
+      column="datapath"/>.  The unusual range ensures that multicast group IDs
+      do not overlap with logical port IDs.
+    </column>
+
+    <column name="name">
+      <p>
+        The logical multicast group's name.  An index forces the name to be
+        unique within the <ref column="datapath"/>.  Logical flows in the
+        ingress pipeline may output to the group just as for individual logical
+        ports, by assigning the group's name to <code>outport</code> and
+        executing an <code>output</code> action.
+      </p>
+
+      <p>
+        Multicast group names and logical port names share a single namespace
+        and thus should not overlap (but the database schema cannot enforce
+        this).  To try to avoid conflicts, <code>ovn-northd</code> uses names
+        that begin with <code>_MC_</code>.
+      </p>
+    </column>
+
+    <column name="ports">
+      The logical ports included in the multicast group.  All of these ports
+      must be in the <ref column="datapath"/> logical datapath (but the
+      database schema cannot enforce this).
+    </column>
+  </table>
+
+  <table name="Datapath_Binding" title="Physical-Logical Datapath Bindings">
+    <p>
+      Each row in this table identifies physical bindings of a logical
+      datapath.  A logical datapath implements a logical pipeline among the
+      ports in the <ref table="Port_Binding"/> table associated with it.  In
+      practice, the pipeline in a given logical datapath implements either a
+      logical switch or a logical router.
+    </p>
+
+    <column name="tunnel_key">
+      The tunnel key value to which the logical datapath is bound.
+      The <code>Tunnel Encapsulation</code> section in
+      <code>ovn-architecture</code>(7) describes how tunnel keys are
+      constructed for each supported encapsulation.
+    </column>
+
+    <column name="external_ids" key="logical-switch" type='{"type": "uuid"}'>
+      Each row in <ref table="Datapath_Binding"/> is associated with some
+      logical datapath.  <code>ovn-northd</code> uses this key to store the
+      UUID of the logical datapath <ref table="Logical_Switch"
+      db="OVN_Northbound"/> row in the <ref db="OVN_Northbound"/> database.
+    </column>
+
+    <group title="Common Columns">
+      The overall purpose of these columns is described under <code>Common
+      Columns</code> at the beginning of this document.
+
+      <column name="external_ids"/>
+    </group>
+  </table>
+
   <table name="Port_Binding" title="Physical-Logical Port Bindings">
     <p>
       Each row in this table identifies the physical location of a logical
     </p>
 
     <p>
-      When a chassis shuts down gracefully, it should cleanup the
+      When a chassis shuts down gracefully, it should clean up the
       <code>chassis</code> column that it previously had populated.
       (This is not critical because resources hosted on the chassis are equally
       unreachable regardless of whether their rows are present.)  To handle the
       <code>chassis</code> column with new information.
     </p>
 
-    <column name="logical_datapath">
-      The logical datapath to which the logical port belongs.  A logical
-      datapath implements a logical pipeline via logical flows in the <ref
-      table="Logical_Flow"/> table.  (No table represents a logical datapath.)
+    <column name="datapath">
+      The logical datapath to which the logical port belongs.
     </column>
 
     <column name="logical_port">
 
     <column name="tunnel_key">
       <p>
-        A number that represents the logical port in the key (e.g. VXLAN VNI or
-        STT key) field carried within tunnel protocol packets.  (This avoids
-        wasting space for a whole UUID in tunneled packets.  It also allows OVN
-        to support encapsulations that cannot fit an entire UUID in their
-        tunnel keys.)
+        A number that represents the logical port in the key (e.g. STT key or
+        Geneve TLV) field carried within tunnel protocol packets.
       </p>
 
       <p>
-        Tunnel ID 0 is reserved for internal use within OVN.
+        The tunnel ID must be unique within the scope of a logical datapath.
       </p>
     </column>