diff mbox

[ovs-dev,v5] ovn: Add software l2 gateway.

Message ID 1466710445-15518-1-git-send-email-russell@ovn.org
State Changes Requested
Headers show

Commit Message

Russell Bryant June 23, 2016, 7:34 p.m. UTC
This patch implements one approach to using ovn-controller to implement
a software l2 gateway between logical and physical networks.

A new logical port type called "l2gateway" is introduced here.  It is very
close to how localnet ports work, with the following exception:

- A localnet port makes OVN use the physical network as the
  transport between hypervisors instead of tunnels. An l2gateway port still
  uses tunnels between all hypervisors, and packets only go to/from the
  specified physical network as needed via the chassis the l2gateway port
  is bound to.

- An l2gateway port also gets bound to a chassis while a localnet port does
  not.  This binding is not done by ovn-controller.  It is left as an
  administrative function.  In the case of OpenStack, the Neutron plugin
  will do this.

Signed-off-by: Russell Bryant <russell@ovn.org>
Acked-by: Ryan Moats <rmoats@us.ibm.com>
Acked-by: Ben Pfaff <blp@ovn.org>
---
 ovn/controller/binding.c            |   9 ++
 ovn/controller/ovn-controller.8.xml |  31 ++++++-
 ovn/controller/patch.c              |  34 +++++---
 ovn/controller/physical.c           |  22 +++--
 ovn/ovn-nb.xml                      |  21 ++++-
 ovn/ovn-sb.xml                      |  78 +++++++++++++++--
 tests/ovn.at                        | 164 ++++++++++++++++++++++++++++++++++++
 7 files changed, 332 insertions(+), 27 deletions(-)


v3->v4:
 - dusted off, rebased, resolved conflicts
 - Improved documentation in ovn-sb.xml to clarify the different meanings
   of the "chassis" column of "Port_Bindings" based on the different
   port types, as suggested by Ben.
 - Change port type from "gateway" to "l2gateway" to help avoid confusion
   with the l3 gateway work.
 - added ACKs from Ryan and Ben, but re-submitted so Justin can review, per
   his request.

v4->v5:
 - rebased, fixed conflicts and ovn-nbctl command names

Comments

Justin Pettit June 25, 2016, 1:29 a.m. UTC | #1
> On Jun 23, 2016, at 12:34 PM, Russell Bryant <russell@ovn.org> wrote:
> 
> This patch implements one approach to using ovn-controller to implement
> a software l2 gateway between logical and physical networks.
> 
> A new logical port type called "l2gateway" is introduced here.  It is very
> close to how localnet ports work, with the following exception:
> 
> - A localnet port makes OVN use the physical network as the
>  transport between hypervisors instead of tunnels. An l2gateway port still
>  uses tunnels between all hypervisors, and packets only go to/from the
>  specified physical network as needed via the chassis the l2gateway port
>  is bound to.
> 
> - An l2gateway port also gets bound to a chassis while a localnet port does
>  not.  This binding is not done by ovn-controller.  It is left as an
>  administrative function.  In the case of OpenStack, the Neutron plugin
>  will do this.

Do you think it's worth mentioning this in the documentation?

> diff --git a/ovn/controller/binding.c b/ovn/controller/binding.c
> index a07c327..dcfcd23 100644
> --- a/ovn/controller/binding.c
> +++ b/ovn/controller/binding.c
> @@ -154,6 +154,15 @@ binding_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
>                 }
>                 sbrec_port_binding_set_chassis(binding_rec, chassis_rec);
>             }
> +        } else if (!strcmp(binding_rec->type, "l2gateway")
> +                   && binding_rec->chassis == chassis_rec) {
> +            /* A locally bound gateway port.
> +             *
> +             * ovn-controller does not bind gateway ports itself.
> +             * Choosing a chassis for a gateway port is left
> +             * up to an entity external to OVN. */

I'd specify "L2 gateway in the comments above, since right below there's the L3 gateway.

> +            sset_add(all_lports, binding_rec->logical_port);
> +            add_local_datapath(local_datapaths, binding_rec);
>         } else if (chassis_rec && binding_rec->chassis == chassis_rec
>                    && strcmp(binding_rec->type, "gateway")) {
>             if (ctx->ovnsb_idl_txn) {

...

> +      <group title="Options for l2gateway ports">
> +        <p>
> +          These options apply when <ref column="type"/> is
> +          <code>l2gateway</code>.
> +        </p>
> +
> +        <column name="options" key="network_name">
> +          Required.  The name of the network to which the <code>l2gateway</code>
> +          port is connected.  The gateway, via <code>ovn-controller</code>,
> +          uses its local configuration to determine exactly how to connect to
> +          this network.
> +        </column>
> +      </group>

It looks like L3 gateways are bound by setting the "chassis" option in logical routers, but L2 gateways are bound by setting "ovn-bridge-mappings" in the Southbound database.  It's a bit unfortunate that we're using pretty different methods for similar actions.  Do you think there's a way that we can make them more similar?

I know it's a hot-button issue, but I wonder if it's worth dusting off the logical-physical separation conversation again.

> diff --git a/ovn/controller/ovn-controller.8.xml b/ovn/controller/ovn-controller.8.xml
> index 1ee3a6e..228a8cd 100644
> --- a/ovn/controller/ovn-controller.8.xml
> +++ b/ovn/controller/ovn-controller.8.xml
> ...
> @@ -199,6 +199,29 @@
>       </dd>
> 
>       <dt>
> +        <code>external-ids:ovn-gateway-port</code> in the <code>Port</code>
> +        table
> +      </dt>
> +      <dd>
> +        <p>
> +          The presence of this key identifies a patch port as one created by
> +          <code>ovn-controller</code> to connect the integration bridge and
> +          another bridge to implement a <code>gateway</code> logical port.
> +          Its value is the name of the logical port with <code>type</code>
> +          set to <code>gateway</code> that the port implements. See
> +          <code>external_ids:ovn-bridge-mappings</code>, above, for more
> +          information.
> +        </p>
> +
> +        <p>
> +          Each <code>gateway</code> logical port is implemented as a pair
> +          of patch ports, one in the integration bridge, one in a different
> +          bridge, with the same <code>external-ids:ovn-gateway-port</code>
> +          value.
> +        </p>

Should these "gateway" references be renamed "l2gateway"?

> diff --git a/ovn/controller/patch.c b/ovn/controller/patch.c
> index 652466b..edf3baf 100644
> --- a/ovn/controller/patch.c
> +++ b/ovn/controller/patch.c
> ...
> @@ -195,31 +197,40 @@ add_bridge_mappings(struct controller_ctx *ctx,
>                 continue;
>             }
>             ld->localnet_port = binding;
> +            patch_port_id = "ovn-localnet-port";
> +        } else if (!strcmp(binding->type, "l2gateway")) {
> +            if (!binding->chassis || strcmp(chassis_id, binding->chassis->name)) {
> +                /* This gateway port is not bound to this chassis, so we should
> +                 * not create any patch ports for it. */
> +                continue;
> +            }

The usual comments about referring to it as "L2 gateway".  I'll be pointing out a few more instances of this, but you might do a general pass to clarify spots that just refer to "gateway".

> +            patch_port_id = "ovn-gateway-port";

I think it might be clearer to call this "ovs-l2gateway-port".

> @@ -327,8 +338,9 @@ patch_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
>     struct shash existing_ports = SHASH_INITIALIZER(&existing_ports);
>     const struct ovsrec_port *port;
>     OVSREC_PORT_FOR_EACH (port, ctx->ovs_idl) {
> -        if (smap_get(&port->external_ids, "ovn-localnet-port") ||
> -            smap_get(&port->external_ids, "ovn-logical-patch-port")) {
> +        if (smap_get(&port->external_ids, "ovn-localnet-port")
> +            || smap_get(&port->external_ids, "ovn-gateway-port")
> +            || smap_get(&port->external_ids, "ovn-logical-patch-port")) {

"ovn-l2gateway-port"?

> diff --git a/ovn/controller/physical.c b/ovn/controller/physical.c
> index 85528e0..d83048e 100644
> --- a/ovn/controller/physical.c
> +++ b/ovn/controller/physical.c
> @@ -169,6 +169,8 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
> 
>         const char *localnet = smap_get(&port_rec->external_ids,
>                                         "ovn-localnet-port");
> +        const char *gateway = smap_get(&port_rec->external_ids,
> +                                        "ovn-gateway-port");

Ditto.  Also may want to rename the variable.

> diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
> index e9353f3..0387ed1 100644
> --- a/ovn/ovn-sb.xml
> +++ b/ovn/ovn-sb.xml
> @@ -1295,10 +1295,39 @@ tcp.flags = RST;
>       </column>
> 
>       <column name="chassis">
> -        The physical location of the logical port.  To successfully identify a
> -        chassis, this column must be a <ref table="Chassis"/> record.  This is
> -        populated by
> -        <code>ovn-controller</code>/<code>ovn-controller-vtep</code>.
> +        The meaning of this column depends on the value of the <ref column="type"/>
> +        column.  This is the meaning for each <ref column="type"/>
> +
> +        <dl>
> +          <dt>(empty string)</dt>
> +          <dd>
> +            The physical location of the logical port.  To successfully identify a
> +            chassis, this column must be a <ref table="Chassis"/> record.  This is
> +            populated by <code>ovn-controller</code>.
> +          </dd>
> +
> +          <dt>vtep</dt>
> +          <dd>
> +            The physical location of the hardware_vtep gateway.  To successfully
> +            identify a chassis, this column must be a <ref table="Chassis"/> record.
> +            This is populated by <code>ovn-controller-vtep</code>.
> +          </dd>
> +
> +          <dt>localnet</dt>
> +          <dd>
> +            Always empty.  A localnet port is realized on every chassis that has
> +            connectivity to the corresponding physical network.
> +          </dd>
> +
> +          <dt>l2gateway</dt>
> +          <dd>
> +            The physical location of this L2 gateway.  To successfully identify a
> +            chassis, this column must be a <ref table="Chassis"/> record.
> +            This is populated by an entity external to OVN, either manually or by
> +            a CMS.
> +          </dd>

It looks like we now have a "gateway" port type.  First, we should probably rename that. (l3gateway?)  Second, you might want to add that to your list.

> @@ -1362,6 +1391,14 @@ tcp.flags = RST;
>             to model direct connectivity to an existing network.
>           </dd>
> 
> +          <dt><code>l2gateway</code></dt>
> +          <dd>
> +            A connection to a physical network.  The chassis this
> +            <ref table="Port_Binding"/> is bound to will serve as
> +            an L2 gateway to the network named by
> +            <ref column="options" table="Port_Binding"/>:<code>network_name</code>.
> +          </dd>

As I mentioned earlier, there's now a "gateway" type.  In addition to possibly change the name, I think it would be good to clearly distinguish between the two.

> +
>           <dt><code>vtep</code></dt>
>           <dd>
>             A port to a logical switch on a VTEP gateway chassis.  In order to
> @@ -1444,6 +1481,36 @@ tcp.flags = RST;
>       </column>
>     </group>
> 
> +    <group title="Gateway Options">

I think this is defining a second group of "Gateway Options".  I assume you'll want to prepend "L3" on the existing one and "L2" to this one.

> +      <p>
> +        These options apply to logical ports with <ref column="type"/> of
> +        <code>l2gateway</code>.
> +      </p>
> +
> +      <column name="options" key="network_name">
> +        Required.  <code>ovn-controller</code> uses the configuration entry
> +        <code>ovn-bridge-mappings</code> to determine how to connect to this
> +        network.  <code>ovn-bridge-mappings</code> is a list of network names
> +        mapped to a local OVS bridge that provides access to that network.  An
> +        example of configuring <code>ovn-bridge-mappings</code> would be:
> +
> +        <pre>$ ovs-vsctl set open . external-ids:ovn-bridge-mappings=physnet1:br-eth0,physnet2:br-eth1</pre>

Does it work in pratice to set more than one physical network?

> @@ -1501,7 +1568,8 @@ tcp.flags = RST;
> 
>         <p>
>           This column is used for a different purpose when <ref column="type"/>
> -          is <code>localnet</code> (see <code>Localnet Options</code>, above).
> +          is <code>localnet</code> (see <code>Localnet Options</code>, above)
> +          or <code>l2gateway</code> (see <code>Gateway Options</code>, above).

You'll want to update this to "L2 Gateway Options", I assume.

Thanks for implementing this.  Sorry for taking so long to review it; I realize that most of my feedback is related to L3 gateway going in before this work that you did months and months ago.  Still, I think it would be good to try to be very clear about L2 vs L3 vs vtep gateways.

As for interfacing with L2 vs L3 gateways, I'm fine if you want to check this in using the bridge mappings method.  However, I do think it would be good to start a conversation on whether we can come up with a consistent way to handle logical and physical configuration.

Acked-by: Justin Pettit <jpettit@ovn.org>

--Justin
diff mbox

Patch

diff --git a/ovn/controller/binding.c b/ovn/controller/binding.c
index a07c327..dcfcd23 100644
--- a/ovn/controller/binding.c
+++ b/ovn/controller/binding.c
@@ -154,6 +154,15 @@  binding_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
                 }
                 sbrec_port_binding_set_chassis(binding_rec, chassis_rec);
             }
+        } else if (!strcmp(binding_rec->type, "l2gateway")
+                   && binding_rec->chassis == chassis_rec) {
+            /* A locally bound gateway port.
+             *
+             * ovn-controller does not bind gateway ports itself.
+             * Choosing a chassis for a gateway port is left
+             * up to an entity external to OVN. */
+            sset_add(all_lports, binding_rec->logical_port);
+            add_local_datapath(local_datapaths, binding_rec);
         } else if (chassis_rec && binding_rec->chassis == chassis_rec
                    && strcmp(binding_rec->type, "gateway")) {
             if (ctx->ovnsb_idl_txn) {
diff --git a/ovn/controller/ovn-controller.8.xml b/ovn/controller/ovn-controller.8.xml
index 1ee3a6e..228a8cd 100644
--- a/ovn/controller/ovn-controller.8.xml
+++ b/ovn/controller/ovn-controller.8.xml
@@ -184,10 +184,10 @@ 
           The presence of this key identifies a patch port as one created by
           <code>ovn-controller</code> to connect the integration bridge and
           another bridge to implement a <code>localnet</code> logical port.
-          Its value is the name of the logical port with type=localnet that
-          the port implements.
-          See <code>external_ids:ovn-bridge-mappings</code>, above,
-          for more information.
+          Its value is the name of the logical port with <code>type</code>
+          set to <code>localnet</code> that the port implements. See
+          <code>external_ids:ovn-bridge-mappings</code>, above, for more
+          information.
         </p>
 
         <p>
@@ -199,6 +199,29 @@ 
       </dd>
 
       <dt>
+        <code>external-ids:ovn-gateway-port</code> in the <code>Port</code>
+        table
+      </dt>
+      <dd>
+        <p>
+          The presence of this key identifies a patch port as one created by
+          <code>ovn-controller</code> to connect the integration bridge and
+          another bridge to implement a <code>gateway</code> logical port.
+          Its value is the name of the logical port with <code>type</code>
+          set to <code>gateway</code> that the port implements. See
+          <code>external_ids:ovn-bridge-mappings</code>, above, for more
+          information.
+        </p>
+
+        <p>
+          Each <code>gateway</code> logical port is implemented as a pair
+          of patch ports, one in the integration bridge, one in a different
+          bridge, with the same <code>external-ids:ovn-gateway-port</code>
+          value.
+        </p>
+      </dd>
+
+      <dt>
         <code>external-ids:ovn-logical-patch-port</code> in the
         <code>Port</code> table
       </dt>
diff --git a/ovn/controller/patch.c b/ovn/controller/patch.c
index 652466b..edf3baf 100644
--- a/ovn/controller/patch.c
+++ b/ovn/controller/patch.c
@@ -134,7 +134,8 @@  static void
 add_bridge_mappings(struct controller_ctx *ctx,
                     const struct ovsrec_bridge *br_int,
                     struct shash *existing_ports,
-                    struct hmap *local_datapaths)
+                    struct hmap *local_datapaths,
+                    const char *chassis_id)
 {
     /* Get ovn-bridge-mappings. */
     const char *mappings_cfg = "";
@@ -175,6 +176,7 @@  add_bridge_mappings(struct controller_ctx *ctx,
 
     const struct sbrec_port_binding *binding;
     SBREC_PORT_BINDING_FOR_EACH (binding, ctx->ovnsb_idl) {
+        const char *patch_port_id;
         if (!strcmp(binding->type, "localnet")) {
             struct local_datapath *ld
                 = get_local_datapath(local_datapaths,
@@ -195,31 +197,40 @@  add_bridge_mappings(struct controller_ctx *ctx,
                 continue;
             }
             ld->localnet_port = binding;
+            patch_port_id = "ovn-localnet-port";
+        } else if (!strcmp(binding->type, "l2gateway")) {
+            if (!binding->chassis || strcmp(chassis_id, binding->chassis->name)) {
+                /* This gateway port is not bound to this chassis, so we should
+                 * not create any patch ports for it. */
+                continue;
+            }
+            patch_port_id = "ovn-gateway-port";
         } else {
-            /* Not a binding for a localnet port. */
+            /* not a localnet or gateway port. */
             continue;
         }
 
         const char *network = smap_get(&binding->options, "network_name");
         if (!network) {
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-            VLOG_ERR_RL(&rl, "localnet port '%s' has no network name.",
-                         binding->logical_port);
+            VLOG_ERR_RL(&rl, "%s port '%s' has no network name.",
+                         binding->type, binding->logical_port);
             continue;
         }
         struct ovsrec_bridge *br_ln = shash_find_data(&bridge_mappings, network);
         if (!br_ln) {
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-            VLOG_ERR_RL(&rl, "bridge not found for localnet port '%s' "
-                    "with network name '%s'", binding->logical_port, network);
+            VLOG_ERR_RL(&rl, "bridge not found for %s port '%s' "
+                    "with network name '%s'",
+                    binding->type, binding->logical_port, network);
             continue;
         }
 
         char *name1 = patch_port_name(br_int->name, binding->logical_port);
         char *name2 = patch_port_name(binding->logical_port, br_int->name);
-        create_patch_port(ctx, "ovn-localnet-port", binding->logical_port,
+        create_patch_port(ctx, patch_port_id, binding->logical_port,
                           br_int, name1, br_ln, name2, existing_ports);
-        create_patch_port(ctx, "ovn-localnet-port", binding->logical_port,
+        create_patch_port(ctx, patch_port_id, binding->logical_port,
                           br_ln, name2, br_int, name1, existing_ports);
         free(name1);
         free(name2);
@@ -327,8 +338,9 @@  patch_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
     struct shash existing_ports = SHASH_INITIALIZER(&existing_ports);
     const struct ovsrec_port *port;
     OVSREC_PORT_FOR_EACH (port, ctx->ovs_idl) {
-        if (smap_get(&port->external_ids, "ovn-localnet-port") ||
-            smap_get(&port->external_ids, "ovn-logical-patch-port")) {
+        if (smap_get(&port->external_ids, "ovn-localnet-port")
+            || smap_get(&port->external_ids, "ovn-gateway-port")
+            || smap_get(&port->external_ids, "ovn-logical-patch-port")) {
             shash_add(&existing_ports, port->name, port);
         }
     }
@@ -336,7 +348,7 @@  patch_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int,
     /* Create in the database any patch ports that should exist.  Remove from
      * 'existing_ports' any patch ports that do exist in the database and
      * should be there. */
-    add_bridge_mappings(ctx, br_int, &existing_ports, local_datapaths);
+    add_bridge_mappings(ctx, br_int, &existing_ports, local_datapaths, chassis_id);
     add_logical_patch_ports(ctx, br_int, chassis_id, &existing_ports,
                             patched_datapaths);
 
diff --git a/ovn/controller/physical.c b/ovn/controller/physical.c
index 85528e0..d83048e 100644
--- a/ovn/controller/physical.c
+++ b/ovn/controller/physical.c
@@ -169,6 +169,8 @@  physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
 
         const char *localnet = smap_get(&port_rec->external_ids,
                                         "ovn-localnet-port");
+        const char *gateway = smap_get(&port_rec->external_ids,
+                                        "ovn-gateway-port");
         const char *logpatch = smap_get(&port_rec->external_ids,
                                         "ovn-logical-patch-port");
 
@@ -191,6 +193,10 @@  physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
                 /* localnet patch ports can be handled just like VIFs. */
                 simap_put(&localvif_to_ofport, localnet, ofport);
                 break;
+            } else if (is_patch && gateway) {
+                /* gateway patch ports can be handled just like VIFs. */
+                simap_put(&localvif_to_ofport, gateway, ofport);
+                break;
             } else if (is_patch && logpatch) {
                 /* Logical patch ports can be handled just like VIFs. */
                 simap_put(&localvif_to_ofport, logpatch, ofport);
@@ -269,13 +275,13 @@  physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
          *       OpenFlow port for the VIF.  'tun' will be NULL.
          *
          *       The same logic handles logical patch ports, as well as
-         *       localnet patch ports.
+         *       localnet and gateway patch ports.
          *
          *       For a container nested inside a VM and accessible via a VLAN,
          *       'tag' is the VLAN ID; otherwise 'tag' is 0.
          *
-         *       For a localnet patch port, if a VLAN ID was configured, 'tag'
-         *       is set to that VLAN ID; otherwise 'tag' is 0.
+         *       For a localnet or gateway patch port, if a VLAN ID was
+         *       configured, 'tag' is set to that VLAN ID; otherwise 'tag' is 0.
          *
          *     - If the port is on a remote chassis, the OpenFlow port for a
          *       tunnel to the VIF's remote chassis.  'tun' identifies that
@@ -297,7 +303,9 @@  physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
         } else {
             ofport = u16_to_ofp(simap_get(&localvif_to_ofport,
                                           binding->logical_port));
-            if (!strcmp(binding->type, "localnet") && ofport && binding->tag) {
+            if ((!strcmp(binding->type, "localnet")
+                 || !strcmp(binding->type, "l2gateway"))
+                && ofport && binding->tag) {
                 tag = *binding->tag;
             }
         }
@@ -355,7 +363,8 @@  physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
             /* Match a VLAN tag and strip it, including stripping priority tags
              * (e.g. VLAN ID 0).  In the latter case we'll add a second flow
              * for frames that lack any 802.1Q header later. */
-            if (tag || !strcmp(binding->type, "localnet")) {
+            if (tag || !strcmp(binding->type, "localnet")
+                || !strcmp(binding->type, "l2gateway")) {
                 match_set_dl_vlan(&match, htons(tag));
                 ofpact_put_STRIP_VLAN(&ofpacts);
             }
@@ -392,7 +401,8 @@  physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve,
             ofctrl_add_flow(flow_table, OFTABLE_PHY_TO_LOG,
                             tag ? 150 : 100, &match, &ofpacts);
 
-            if (!tag && !strcmp(binding->type, "localnet")) {
+            if (!tag && (!strcmp(binding->type, "localnet")
+                         || !strcmp(binding->type, "l2gateway"))) {
                 /* Add a second flow for frames that lack any 802.1Q
                  * header.  For these, drop the OFPACT_STRIP_VLAN
                  * action. */
diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml
index 6355c44..17881f5 100644
--- a/ovn/ovn-nb.xml
+++ b/ovn/ovn-nb.xml
@@ -134,6 +134,11 @@ 
             to model direct connectivity to an existing network.
           </dd>
 
+          <dt><code>l2gateway</code></dt>
+          <dd>
+            A connection to a physical network.
+          </dd>
+
           <dt><code>vtep</code></dt>
           <dd>
             A port to a logical switch on a VTEP gateway.
@@ -175,6 +180,20 @@ 
         </column>
       </group>
 
+      <group title="Options for l2gateway ports">
+        <p>
+          These options apply when <ref column="type"/> is
+          <code>l2gateway</code>.
+        </p>
+
+        <column name="options" key="network_name">
+          Required.  The name of the network to which the <code>l2gateway</code>
+          port is connected.  The gateway, via <code>ovn-controller</code>,
+          uses its local configuration to determine exactly how to connect to
+          this network.
+        </column>
+      </group>
+
       <group title="Options for vtep ports">
         <p>
           These options apply when <ref column="type"/> is <code>vtep</code>.
@@ -658,7 +677,7 @@ 
         </p>
       </column>
     </group>
-    
+
     <group title="Common Columns">
       <column name="external_ids">
         See <em>External IDs</em> at the beginning of this document.
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index e9353f3..0387ed1 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1295,10 +1295,39 @@  tcp.flags = RST;
       </column>
 
       <column name="chassis">
-        The physical location of the logical port.  To successfully identify a
-        chassis, this column must be a <ref table="Chassis"/> record.  This is
-        populated by
-        <code>ovn-controller</code>/<code>ovn-controller-vtep</code>.
+        The meaning of this column depends on the value of the <ref column="type"/>
+        column.  This is the meaning for each <ref column="type"/>
+
+        <dl>
+          <dt>(empty string)</dt>
+          <dd>
+            The physical location of the logical port.  To successfully identify a
+            chassis, this column must be a <ref table="Chassis"/> record.  This is
+            populated by <code>ovn-controller</code>.
+          </dd>
+
+          <dt>vtep</dt>
+          <dd>
+            The physical location of the hardware_vtep gateway.  To successfully
+            identify a chassis, this column must be a <ref table="Chassis"/> record.
+            This is populated by <code>ovn-controller-vtep</code>.
+          </dd>
+
+          <dt>localnet</dt>
+          <dd>
+            Always empty.  A localnet port is realized on every chassis that has
+            connectivity to the corresponding physical network.
+          </dd>
+
+          <dt>l2gateway</dt>
+          <dd>
+            The physical location of this L2 gateway.  To successfully identify a
+            chassis, this column must be a <ref table="Chassis"/> record.
+            This is populated by an entity external to OVN, either manually or by
+            a CMS.
+          </dd>
+        </dl>
+
       </column>
 
       <column name="tunnel_key">
@@ -1362,6 +1391,14 @@  tcp.flags = RST;
             to model direct connectivity to an existing network.
           </dd>
 
+          <dt><code>l2gateway</code></dt>
+          <dd>
+            A connection to a physical network.  The chassis this
+            <ref table="Port_Binding"/> is bound to will serve as
+            an L2 gateway to the network named by
+            <ref column="options" table="Port_Binding"/>:<code>network_name</code>.
+          </dd>
+
           <dt><code>vtep</code></dt>
           <dd>
             A port to a logical switch on a VTEP gateway chassis.  In order to
@@ -1444,6 +1481,36 @@  tcp.flags = RST;
       </column>
     </group>
 
+    <group title="Gateway Options">
+      <p>
+        These options apply to logical ports with <ref column="type"/> of
+        <code>l2gateway</code>.
+      </p>
+
+      <column name="options" key="network_name">
+        Required.  <code>ovn-controller</code> uses the configuration entry
+        <code>ovn-bridge-mappings</code> to determine how to connect to this
+        network.  <code>ovn-bridge-mappings</code> is a list of network names
+        mapped to a local OVS bridge that provides access to that network.  An
+        example of configuring <code>ovn-bridge-mappings</code> would be:
+
+        <pre>$ ovs-vsctl set open . external-ids:ovn-bridge-mappings=physnet1:br-eth0,physnet2:br-eth1</pre>
+
+        <p>
+          When a logical switch has a <code>l2gateway</code> port attached,
+          the chassis that the <code>l2gateway</code> port is bound to
+          must have a bridge mapping configured to reach the network
+          identified by <code>network_name</code>.
+        </p>
+      </column>
+
+      <column name="tag">
+        If set, indicates that the gateway is connected to a specific
+        VLAN on the physical network. The VLAN ID is used to match
+        incoming traffic and is also added to outgoing traffic.
+      </column>
+    </group>
+
     <group title="VTEP Options">
       <p>
         These options apply to logical ports with <ref column="type"/> of
@@ -1501,7 +1568,8 @@  tcp.flags = RST;
 
         <p>
           This column is used for a different purpose when <ref column="type"/>
-          is <code>localnet</code> (see <code>Localnet Options</code>, above).
+          is <code>localnet</code> (see <code>Localnet Options</code>, above)
+          or <code>l2gateway</code> (see <code>Gateway Options</code>, above).
         </p>
       </column>
     </group>
diff --git a/tests/ovn.at b/tests/ovn.at
index a52def4..6d0bd47 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1250,6 +1250,170 @@  for sim in hv1 hv2 hv3 vtep main; do
 done
 AT_CLEANUP
 
+# Similar test to "hardware GW"
+AT_SETUP([ovn -- 3 HVs, 1 VIFs/HV, 1 software GW, 1 LS])
+AT_SKIP_IF([test $HAVE_PYTHON = no])
+ovn_start
+
+# Configure the Northbound database
+ovn-nbctl ls-add lsw0
+
+ovn-nbctl lsp-add lsw0 lp1
+ovn-nbctl lsp-set-addresses lp1 f0:00:00:00:00:01
+
+ovn-nbctl lsp-add lsw0 lp2
+ovn-nbctl lsp-set-addresses lp2 f0:00:00:00:00:02
+
+ovn-nbctl lsp-add lsw0 lp-gw
+ovn-nbctl lsp-set-type lp-gw l2gateway
+ovn-nbctl lsp-set-options lp-gw network_name=physnet1
+ovn-nbctl lsp-set-addresses lp-gw unknown
+
+net_add n1               # Network to connect hv1, hv2, and gw
+net_add n2               # Network to connect gw and hv3
+
+# Create hypervisor hv1 connected to n1
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovs-vsctl add-port br-int vif1 -- set Interface vif1 external-ids:iface-id=lp1 options:tx_pcap=hv1/vif1-tx.pcap options:rxq_pcap=hv1/vif1-rx.pcap ofport-request=1
+
+# Create hypervisor hv2 connected to n1
+sim_add hv2
+as hv2
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+ovs-vsctl add-port br-int vif2 -- set Interface vif2 external-ids:iface-id=lp2 options:tx_pcap=hv2/vif2-tx.pcap options:rxq_pcap=hv2/vif2-rx.pcap ofport-request=1
+
+# Create hypervisor hv_gw connected to n1 and n2
+# connect br-phys bridge to n1; connect hv-gw bridge to n2
+sim_add hv_gw
+as hv_gw
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.3
+ovs-vsctl add-br br-phys2
+net_attach n2 br-phys2
+ovs-vsctl set open . external_ids:ovn-bridge-mappings="physnet1:br-phys2"
+
+# Bind our gateway port to the hv_gw chassis
+ovn-sbctl lport-bind lp-gw hv_gw
+
+# Add hv3 on the other side of the GW
+sim_add hv3
+as hv3
+ovs-vsctl add-br br-phys
+net_attach n2 br-phys
+ovs-vsctl add-port br-phys vif3 -- set Interface vif3 options:tx_pcap=hv3/vif3-tx.pcap options:rxq_pcap=hv3/vif3-rx.pcap ofport-request=1
+
+
+# Pre-populate the hypervisors' ARP tables so that we don't lose any
+# packets for ARP resolution (native tunneling doesn't queue packets
+# for ARP resolution).
+ovn_populate_arp
+
+# Allow some time for ovn-northd and ovn-controller to catch up.
+# XXX This should be more systematic.
+sleep 1
+
+# test_packet INPORT DST SRC ETHTYPE OUTPORT...
+#
+# This shell function causes a packet to be received on INPORT.  The packet's
+# content has Ethernet destination DST and source SRC (each exactly 12 hex
+# digits) and Ethernet type ETHTYPE (4 hex digits).  The OUTPORTs (zero or
+# more) list the VIFs on which the packet should be received.  INPORT and the
+# OUTPORTs are specified as lport numbers, e.g. 1 for vif1.
+trim_zeros() {
+    sed 's/\(00\)\{1,\}$//'
+}
+for i in 1 2 3; do
+    : > $i.expected
+done
+test_packet() {
+    local inport=$1 packet=$2$3$4; shift; shift; shift; shift
+    #hv=hv`echo $inport | sed 's/^\(.\).*/\1/'`
+    hv=hv$inport
+    vif=vif$inport
+    as $hv ovs-appctl netdev-dummy/receive $vif $packet
+    for outport; do
+        echo $packet | trim_zeros >> $outport.expected
+    done
+}
+
+# Send packets between all pairs of source and destination ports:
+#
+# 1. Unicast packets are delivered to exactly one lport (except that packets
+#    destined to their input ports are dropped).
+#
+# 2. Broadcast and multicast are delivered to all lports except the input port.
+#
+# 3. The lswitch delivers packets with an unknown destination to lports with
+#    "unknown" among their MAC addresses (and port security disabled).
+for s in 1 2 3 ; do
+    bcast=
+    unknown=
+    for d in 1 2 3 ; do
+        if test $d != $s; then unicast=$d; else unicast=; fi
+        test_packet $s f0000000000$d f0000000000$s 00$s$d $unicast       #1
+
+        # The vtep (vif3) is the only one configured for "unknown"
+        if test $d != $s && test $d = 3; then
+            unknown="$unknown $d"
+        fi
+        bcast="$bcast $unicast"
+    done
+
+    test_packet $s ffffffffffff f0000000000$s 0${s}ff $bcast             #2
+    test_packet $s 010000000000 f0000000000$s 0${s}ff $bcast             #3
+    test_packet $s f0000000ffff f0000000000$s 0${s}66 $unknown           #4
+done
+
+# Allow some time for packet forwarding.
+# XXX This can be improved.
+sleep 3
+
+echo "------ ovn-nbctl show ------"
+ovn-nbctl show
+echo "------ ovn-sbctl show ------"
+ovn-sbctl show
+
+echo "------ hv1 ------"
+as hv1 ovs-vsctl show
+echo "------ hv1 br-int ------"
+as hv1 ovs-ofctl -O OpenFlow13 dump-flows br-int
+echo "------ hv1 br-phys ------"
+as hv1 ovs-ofctl -O OpenFlow13 dump-flows br-phys
+
+echo "------ hv2 ------"
+as hv2 ovs-vsctl show
+echo "------ hv2 br-int ------"
+as hv2 ovs-ofctl -O OpenFlow13 dump-flows br-int
+echo "------ hv2 br-phys ------"
+as hv2 ovs-ofctl -O OpenFlow13 dump-flows br-phys
+
+echo "------ hv_gw ------"
+as hv_gw ovs-vsctl show
+echo "------ hv_gw br-phys ------"
+as hv_gw ovs-ofctl -O OpenFlow13 dump-flows br-phys
+echo "------ hv_gw br-phys2 ------"
+as hv_gw ovs-ofctl -O OpenFlow13 dump-flows br-phys2
+
+echo "------ hv3 ------"
+as hv3 ovs-vsctl show
+echo "------ hv3 br-phys ------"
+as hv3 ovs-ofctl -O OpenFlow13 dump-flows br-phys
+
+# Now check the packets actually received against the ones expected.
+for i in 1 2 3; do
+    file=hv$i/vif$i-tx.pcap
+    echo $file
+    $PYTHON "$top_srcdir/utilities/ovs-pcap.in" $file | trim_zeros > $i.packets
+    sort $i.expected > expout
+    AT_CHECK([sort $i.packets], [0], [expout])
+    echo
+done
+AT_CLEANUP
+
 # 3 hypervisors, 3 logical switches with 3 logical ports each, 1 logical router
 AT_SETUP([ovn -- 3 HVs, 3 LS, 3 lports/LS, 1 LR])
 AT_SKIP_IF([test $HAVE_PYTHON = no])