diff mbox series

[ovs-dev,ovn,v8,1/2] Support logical switches with multiple localnet ports

Message ID 20200519155816.24508-2-ihrachys@redhat.com
State New
Headers show
Series Support logical switches with multiple localnet ports | expand

Commit Message

Ihar Hrachyshka May 19, 2020, 3:58 p.m. UTC
Assuming only a single localnet port is actually plugged mapped on
each chassis, this allows to maintain disjoint networks plugged to the
same switch.  This is useful to simplify resource management for
OpenStack "routed provider networks" feature [1] where a single
"network" (which traditionally maps to logical switches in OVN) is
comprised of multiple L2 segments and assumes external L3 routing
implemented between the segments.

[1]: https://docs.openstack.org/ocata/networking-guide/config-routed-networks.html

Signed-off-by: Ihar Hrachyshka <ihrachys@redhat.com>
Acked-by: Dumitru Ceara <dceara@redhat.com>
Acked-by: Numan Siddique <numans@ovn.org>
---
 controller/binding.c   |  13 ++
 controller/patch.c     |  14 +-
 northd/ovn-northd.c    |  62 +++--
 ovn-architecture.7.xml |  50 ++--
 ovn-nb.xml             |  23 +-
 ovn-sb.xml             |  21 +-
 tests/ovn.at           | 504 +++++++++++++++++++++++++++++++++++++++++
 7 files changed, 634 insertions(+), 53 deletions(-)
diff mbox series

Patch

diff --git a/controller/binding.c b/controller/binding.c
index a5525a310..79dc046d9 100644
--- a/controller/binding.c
+++ b/controller/binding.c
@@ -694,12 +694,25 @@  add_localnet_egress_interface_mappings(
     }
 }
 
+static bool
+is_network_plugged(const struct sbrec_port_binding *binding_rec,
+                   struct shash *bridge_mappings)
+{
+    const char *network = smap_get(&binding_rec->options, "network_name");
+    return network ? !!shash_find_data(bridge_mappings, network) : false;
+}
+
 static void
 consider_localnet_port(const struct sbrec_port_binding *binding_rec,
                        struct shash *bridge_mappings,
                        struct sset *egress_ifaces,
                        struct hmap *local_datapaths)
 {
+    /* Ignore localnet ports for unplugged networks. */
+    if (!is_network_plugged(binding_rec, bridge_mappings)) {
+        return;
+    }
+
     add_localnet_egress_interface_mappings(binding_rec,
             bridge_mappings, egress_ifaces);
 
diff --git a/controller/patch.c b/controller/patch.c
index 349faae17..7ad30d9cc 100644
--- a/controller/patch.c
+++ b/controller/patch.c
@@ -198,8 +198,10 @@  add_bridge_mappings(struct ovsdb_idl_txn *ovs_idl_txn,
             continue;
         }
 
+        bool is_localnet = false;
         const char *patch_port_id;
         if (!strcmp(binding->type, "localnet")) {
+            is_localnet = true;
             patch_port_id = "ovn-localnet-port";
         } else if (!strcmp(binding->type, "l2gateway")) {
             if (!binding->chassis
@@ -224,9 +226,15 @@  add_bridge_mappings(struct ovsdb_idl_txn *ovs_idl_txn,
         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 %s port '%s' "
-                    "with network name '%s'",
-                    binding->type, binding->logical_port, network);
+            if (!is_localnet) {
+                VLOG_ERR_RL(&rl, "bridge not found for %s port '%s' "
+                        "with network name '%s'",
+                        binding->type, binding->logical_port, network);
+            } else {
+                VLOG_INFO_RL(&rl, "bridge not found for localnet port '%s' "
+                        "with network name '%s'; skipping",
+                        binding->logical_port, network);
+            }
             continue;
         }
 
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index 87625c395..6ccd84e49 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -543,7 +543,9 @@  struct ovn_datapath {
     /* The "derived" OVN port representing the instance of l3dgw_port on
      * the "redirect-chassis". */
     struct ovn_port *l3redirect_port;
-    struct ovn_port *localnet_port;
+
+    struct ovn_port **localnet_ports;
+    size_t n_localnet_ports;
 
     struct ovs_list lr_list; /* In list of logical router datapaths. */
     /* The logical router group to which this datapath belongs.
@@ -611,6 +613,7 @@  ovn_datapath_destroy(struct hmap *datapaths, struct ovn_datapath *od)
         ovn_destroy_tnlids(&od->port_tnlids);
         bitmap_free(od->ipam_info.allocated_ipv4s);
         free(od->router_ports);
+        free(od->localnet_ports);
         ovn_ls_port_group_destroy(&od->nb_pgs);
         destroy_mcast_info_for_datapath(od);
 
@@ -2025,6 +2028,7 @@  join_logical_ports(struct northd_context *ctx,
     struct ovn_datapath *od;
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (od->nbs) {
+            size_t n_allocated_localnet_ports = 0;
             for (size_t i = 0; i < od->nbs->n_ports; i++) {
                 const struct nbrec_logical_switch_port *nbsp
                     = od->nbs->ports[i];
@@ -2059,7 +2063,12 @@  join_logical_ports(struct northd_context *ctx,
                 }
 
                 if (!strcmp(nbsp->type, "localnet")) {
-                   od->localnet_port = op;
+                   if (od->n_localnet_ports >= n_allocated_localnet_ports) {
+                       od->localnet_ports = x2nrealloc(
+                           od->localnet_ports, &n_allocated_localnet_ports,
+                           sizeof *od->localnet_ports);
+                   }
+                   od->localnet_ports[od->n_localnet_ports++] = op;
                 }
 
                 op->lsp_addrs
@@ -3016,7 +3025,7 @@  ovn_port_update_sbrec(struct northd_context *ctx,
                               "reside-on-redirect-chassis", false) ||
                 op->peer == op->peer->od->l3dgw_port)) {
                 add_router_port_garp = true;
-            } else if (chassis && op->od->localnet_port) {
+            } else if (chassis && op->od->n_localnet_ports) {
                 add_router_port_garp = true;
             }
 
@@ -4734,8 +4743,8 @@  build_pre_acls(struct ovn_datapath *od, struct hmap *lflows)
         for (size_t i = 0; i < od->n_router_ports; i++) {
             build_pre_acl_flows(od, od->router_ports[i], lflows);
         }
-        if (od->localnet_port) {
-            build_pre_acl_flows(od, od->localnet_port, lflows);
+        for (size_t i = 0; i < od->n_localnet_ports; i++) {
+            build_pre_acl_flows(od, od->localnet_ports[i], lflows);
         }
 
         /* Ingress and Egress Pre-ACL Table (Priority 110).
@@ -6001,9 +6010,9 @@  build_lswitch_rport_arp_req_flow_for_ip(struct sset *ips,
     /* Send a the packet only to the router pipeline and skip flooding it
      * in the broadcast domain (except for the localnet port).
      */
-    if (od->localnet_port) {
+    for (size_t i = 0; i < od->n_localnet_ports; i++) {
         ds_put_format(&actions, "clone { outport = %s; output; }; ",
-                      od->localnet_port->json_key);
+                      od->localnet_ports[i]->json_key);
     }
     ds_put_format(&actions, "outport = %s; output;", patch_op->json_key);
     ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_L2_LKUP, priority,
@@ -6585,25 +6594,31 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         }
 
         bool is_external = lsp_is_external(op->nbsp);
-        if (is_external && (!op->od->localnet_port ||
+        if (is_external && (!op->od->n_localnet_ports ||
                             !op->nbsp->ha_chassis_group)) {
-            /* If it's an external port and there is no localnet port
+            /* If it's an external port and there are no localnet ports
              * and if it doesn't belong to an HA chassis group ignore it. */
             continue;
         }
 
         for (size_t i = 0; i < op->n_lsp_addrs; i++) {
-            const char *json_key;
             if (is_external) {
-                json_key = op->od->localnet_port->json_key;
+                for (size_t j = 0; j < op->od->n_localnet_ports; j++) {
+                    build_dhcpv4_options_flows(
+                        op, &op->lsp_addrs[i],
+                        op->od->localnet_ports[j]->json_key, is_external,
+                        lflows);
+                    build_dhcpv6_options_flows(
+                        op, &op->lsp_addrs[i],
+                        op->od->localnet_ports[j]->json_key, is_external,
+                        lflows);
+                }
             } else {
-                json_key = op->json_key;
+                build_dhcpv4_options_flows(op, &op->lsp_addrs[i], op->json_key,
+                                           is_external, lflows);
+                build_dhcpv6_options_flows(op, &op->lsp_addrs[i], op->json_key,
+                                           is_external, lflows);
             }
-            build_dhcpv4_options_flows(op, &op->lsp_addrs[i], json_key,
-                                       is_external, lflows);
-
-            build_dhcpv6_options_flows(op, &op->lsp_addrs[i], json_key,
-                                       is_external, lflows);
         }
     }
 
@@ -6659,8 +6674,7 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
     }
 
     HMAP_FOR_EACH (op, key_node, ports) {
-        if (!op->nbsp || !lsp_is_external(op->nbsp) ||
-            !op->od->localnet_port) {
+        if (!op->nbsp || !lsp_is_external(op->nbsp)) {
            continue;
         }
 
@@ -6668,8 +6682,10 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
          * external ports  on chassis not binding those ports.
          * This makes the router pipeline to be run only on the chassis
          * binding the external ports. */
-        build_drop_arp_nd_flows_for_unbound_router_ports(
-            op, op->od->localnet_port, lflows);
+        for (size_t i = 0; i < op->od->n_localnet_ports; i++) {
+            build_drop_arp_nd_flows_for_unbound_router_ports(
+                op, op->od->localnet_ports[i], lflows);
+        }
     }
 
     char *svc_check_match = xasprintf("eth.dst == %s", svc_monitor_mac);
@@ -6887,7 +6903,7 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                               ETH_ADDR_ARGS(mac));
                 if (op->peer->od->l3dgw_port
                     && op->peer->od->l3redirect_port
-                    && op->od->localnet_port) {
+                    && op->od->n_localnet_ports) {
                     bool add_chassis_resident_check = false;
                     if (op->peer == op->peer->od->l3dgw_port) {
                         /* The peer of this port represents a distributed
@@ -8193,7 +8209,7 @@  build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
                           op->lrp_networks.ipv4_addrs[i].addr_s);
 
             if (op->od->l3dgw_port && op->od->l3redirect_port && op->peer
-                && op->peer->od->localnet_port) {
+                && op->peer->od->n_localnet_ports) {
                 bool add_chassis_resident_check = false;
                 if (op == op->od->l3dgw_port) {
                     /* Traffic with eth.src = l3dgw_port->lrp_networks.ea_s
diff --git a/ovn-architecture.7.xml b/ovn-architecture.7.xml
index 5c125043e..246cebc19 100644
--- a/ovn-architecture.7.xml
+++ b/ovn-architecture.7.xml
@@ -441,9 +441,8 @@ 
 
   <p>
     A <code>localnet</code> logical switch port bridges a logical switch to a
-    physical VLAN.  Any given logical switch should have no more than one
-    <code>localnet</code> port.  Such a logical switch is used in two
-    scenarios:
+    physical VLAN.  A logical switch may have one or more <code>localnet</code>
+    ports.  Such a logical switch is used in two scenarios:
   </p>
 
   <ul>
@@ -463,6 +462,31 @@ 
     </li>
   </ul>
 
+  <p>
+    When a logical switch contains multiple <code>localnet</code> ports, the
+    following is assumed.
+  </p>
+
+  <ul>
+    <li>
+      Each chassis has a bridge mapping for one of the <code>localnet</code>
+      physical networks only.
+    </li>
+
+    <li>
+      To facilitate interconnectivity between VIF ports of the switch that are
+      located on different chassis with different physical network
+      connectivity, the fabric implements L3 routing between these adjacent
+      physical network segments.
+    </li>
+  </ul>
+
+  <p>
+    Note: nothing said above implies that a chassis cannot be plugged to
+    multiple physical networks as long as they belong to different
+    switches.
+  </p>
+
   <p>
     A <code>localport</code> logical switch port is a special kind of VIF
     logical switch port.  These ports are present in every chassis, not bound
@@ -1951,13 +1975,13 @@ 
   <ol>
     <li>
       The packet first enters the ingress pipeline, and then egress pipeline of
-      the source localnet logical switch datapath and is sent out via the
+      the source localnet logical switch datapath and is sent out via a
       localnet port of the source localnet logical switch (instead of sending
       it to router pipeline).
     </li>
 
     <li>
-      The gateway chassis receives the packet via the localnet port of the
+      The gateway chassis receives the packet via a localnet port of the
       source localnet logical switch and sends it to the integration bridge.
       The packet then enters the ingress pipeline, and then egress pipeline of
       the source localnet logical switch datapath and enters the ingress
@@ -1972,11 +1996,11 @@ 
       From the router datapath, packet enters the ingress pipeline and then
       egress pipeline of the destination localnet logical switch datapath.
       It then goes out of the integration bridge to the provider bridge (
-      belonging to the destination logical switch) via the localnet port.
+      belonging to the destination logical switch) via a localnet port.
     </li>
 
     <li>
-      The destination chassis receives the packet via the localnet port and
+      The destination chassis receives the packet via a localnet port and
       sends it to the integration bridge. The packet enters the
       ingress pipeline and then egress pipeline of the destination localnet
       logical switch and finally delivered to the destination VM port.
@@ -1991,13 +2015,13 @@ 
   <ol>
     <li>
       The packet first enters the ingress pipeline, and then egress pipeline of
-      the source localnet logical switch datapath and is sent out via the
+      the source localnet logical switch datapath and is sent out via a
       localnet port of the source localnet logical switch (instead of sending
       it to router pipeline).
     </li>
 
     <li>
-      The gateway chassis receives the packet via the localnet port of the
+      The gateway chassis receives the packet via a localnet port of the
       source localnet logical switch and sends it to the integration bridge.
       The packet then enters the ingress pipeline, and then egress pipeline of
       the source localnet logical switch datapath and enters the ingress
@@ -2013,7 +2037,7 @@ 
       egress pipeline of the localnet logical switch datapath which provides
       external connectivity. It then goes out of the integration bridge to the
       provider bridge (belonging to the logical switch which provides external
-      connectivity) via the localnet port.
+      connectivity) via a localnet port.
     </li>
   </ol>
 
@@ -2023,7 +2047,7 @@ 
 
   <ol>
     <li>
-      The gateway chassis receives the packet from the localnet port of
+      The gateway chassis receives the packet from a localnet port of
       the logical switch which provides external connectivity. The packet then
       enters the ingress pipeline and then egress pipeline of the localnet
       logical switch (which provides external connectivity). The packet then
@@ -2034,12 +2058,12 @@ 
       The ingress pipeline of the logical router datapath applies the unNATting
       rules. The packet then enters the ingress pipeline and then egress
       pipeline of the source localnet logical switch. Since the source VM
-      doesn't reside in the gateway chassis, the packet is sent out via the
+      doesn't reside in the gateway chassis, the packet is sent out via a
       localnet port of the source logical switch.
     </li>
 
     <li>
-      The source chassis receives the packet via the localnet port and
+      The source chassis receives the packet via a localnet port and
       sends it to the integration bridge. The packet enters the
       ingress pipeline and then egress pipeline of the source localnet
       logical switch and finally gets delivered to the source VM port.
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 95ee4c9e6..acf56486b 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -244,14 +244,14 @@ 
     <p>
       There are two kinds of logical switches, that is, ones that fully
       virtualize the network (overlay logical switches) and ones that provide
-      simple connectivity to a physical network (bridged logical switches).
+      simple connectivity to physical networks (bridged logical switches).
       They work in the same way when providing connectivity between logical
-      ports on same chasis, but differently when connecting remote logical
+      ports on same chassis, but differently when connecting remote logical
       ports.  Overlay logical switches connect remote logical ports by tunnels,
       while bridged logical switches provide connectivity to remote ports by
-      bridging the packets to directly connected physical L2 segment with the
+      bridging the packets to directly connected physical L2 segments with the
       help of <code>localnet</code> ports.  Each bridged logical switch has
-      one and only one <code>localnet</code> port, which has only one special
+      one or more <code>localnet</code> ports, which have only one special
       address <code>unknown</code>.
     </p>
 
@@ -527,10 +527,15 @@ 
 
           <dt><code>localnet</code></dt>
           <dd>
-            A connection to a locally accessible network from each
-            <code>ovn-controller</code> instance.  A logical switch can only
-            have a single <code>localnet</code> port attached.  This is used
-            to model direct connectivity to an existing network.
+            A connection to a locally accessible network from
+            <code>ovn-controller</code> instances that have a corresponding
+            bridge mapping.  A logical switch can have multiple
+            <code>localnet</code> ports attached.  This type is used to model
+            direct connectivity to existing networks.  In this case, each
+            chassis should have a mapping for one of the physical networks
+            only.  Note: nothing said above implies that a chassis cannot be
+            plugged to multiple physical networks as long as they belong to
+            different switches.
           </dd>
 
           <dt><code>localport</code></dt>
@@ -721,7 +726,7 @@ 
           Required.  The name of the network to which the <code>localnet</code>
           port is connected.  Each hypervisor, via <code>ovn-controller</code>,
           uses its local configuration to determine exactly how to connect to
-          this locally accessible network.
+          this locally accessible network, if at all.
         </column>
       </group>
 
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 3aa7cd4da..1fa769f3d 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -2626,10 +2626,15 @@  tcp.flags = RST;
 
           <dt><code>localnet</code></dt>
           <dd>
-            A connection to a locally accessible network from each
-            <code>ovn-controller</code> instance.  A logical switch can only
-            have a single <code>localnet</code> port attached.  This is used
-            to model direct connectivity to an existing network.
+            A connection to a locally accessible network from
+            <code>ovn-controller</code> instances that have a corresponding
+            bridge mapping.  A logical switch can have multiple
+            <code>localnet</code> ports attached.  This type is used to model
+            direct connectivity to existing networks.  In this case, each
+            chassis should have a mapping for one of the physical networks
+            only.  Note: nothing said above implies that a chassis cannot be
+            plugged to multiple physical networks as long as they belong to
+            different switches.
           </dd>
 
           <dt><code>localport</code></dt>
@@ -2777,7 +2782,13 @@  tcp.flags = RST;
           switch must have a bridge mapping configured to reach that
           <code>localnet</code>.  Traffic that arrives on a
           <code>localnet</code> port is never forwarded over a tunnel to
-          another chassis.
+          another chassis.  If there are multiple <code>localnet</code>
+          ports in a logical switch, each chassis should only have a single
+          bridge mapping for one of the physical networks.  Note: In case of
+          multiple <code>localnet</code> ports, to provide interconnectivity
+          between all VIFs located on different chassis with different fabric
+          connectivity, the fabric should implement some form of routing
+          between the segments.
         </p>
       </column>
 
diff --git a/tests/ovn.at b/tests/ovn.at
index f39fda2e4..4370b3728 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -48,6 +48,10 @@  m4_define([OVN_CHECK_PACKETS_REMOVE_BROADCAST],
   [ovn_check_packets_remove_broadcast__ "$1" "$2"
    AT_CHECK([sort $rcv_text], [0], [expout])])
 
+m4_define([OVN_CHECK_PACKETS_CONTAIN],
+  [ovn_check_packets__ "$1" "$2"
+   AT_CHECK([sort $rcv_text | comm --nocheck-order -2 -3 expout -], [0], [])])
+
 AT_BANNER([OVN components])
 
 AT_SETUP([ovn -- lexer])
@@ -2488,6 +2492,506 @@  OVN_CLEANUP([hv1],[hv2])
 
 AT_CLEANUP
 
+AT_SETUP([ovn -- 2 HVs, 2 LS, routing works for multiple colacated segments attached to different switches])
+ovn_start
+
+for tag in `seq 10 30`; do
+    net_add n-$tag
+done
+
+for i in 1 2; do
+    sim_add hv-$i
+    as hv-$i
+    ovs-vsctl add-br br-phys11
+    ovs-vsctl add-br br-phys21
+    ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys-11:br-phys11,phys-21:br-phys21
+    ovn_attach n-11 br-phys11 192.168.0.${i}1
+    ovn_attach n-21 br-phys21 192.168.0.${i}2
+done
+
+for i in 1 2; do
+    lsname=ls-${i}0
+    ovn-nbctl ls-add $lsname
+    for tag in `seq ${i}1 ${i}9`; do
+        ln_port_name=ln-$tag
+        ovn-nbctl lsp-add $lsname $ln_port_name "" $tag
+        ovn-nbctl lsp-set-addresses $ln_port_name unknown
+        ovn-nbctl lsp-set-type $ln_port_name localnet
+        ovn-nbctl lsp-set-options $ln_port_name network_name=phys-$tag
+    done
+done
+
+for hv in 1 2; do
+    as hv-$hv
+    for ls in 1 2; do
+        lsp_name=lp-$hv-$ls
+        ovs-vsctl add-port br-int vif-$hv-$ls -- \
+            set Interface vif-$hv-$ls external-ids:iface-id=$lsp_name \
+                                  options:tx_pcap=hv-$hv/vif-$hv-$ls-tx.pcap \
+                                  options:rxq_pcap=hv-$hv/vif-$hv-$ls-rx.pcap \
+                                  ofport-request=$hv$ls
+
+        ovn-nbctl lsp-add ls-${ls}0 $lsp_name
+        ovn-nbctl lsp-set-addresses $lsp_name f0:00:00:00:00:${hv}${ls}
+        ovn-nbctl lsp-set-port-security $lsp_name f0:00:00:00:00:${hv}${ls}
+
+        OVS_WAIT_UNTIL([test x`ovn-nbctl lsp-get-up $lsp_name` = xup])
+    done
+done
+
+
+ovn-nbctl --wait=sb sync
+ovn-nbctl show
+ovn-sbctl dump-flows
+
+echo "------ OVN dump ------"
+ovn-nbctl show
+ovn-sbctl show
+
+for i in 1 2; do
+    hv=hv-$i
+    echo "------ $hv dump ------"
+    as $hv ovs-vsctl show
+    as $hv ovs-ofctl -O OpenFlow13 dump-flows br-int
+done
+
+# vif ports
+for i in 1-1 1-2 2-1 2-2; do
+    : > vif-$i.expected
+done
+
+# localnet ports
+for hv in 1 2; do
+    : > out-$hv.expected
+done
+
+test_packet() {
+    local hv=$1 inport=$2 outport=$3 dst=$4 src=$5 eth=$6 lout=$7
+
+    : > expout
+    if test $lout = unknown; then
+        # Expect the packet cloned to all localnet ports
+        for tag in `seq ${hv}1 ${hv}9`; do
+            echo "output(\"ln-$tag\");" >> expout
+        done
+    else
+        echo "output(\"$lout\");" >> expout
+    fi
+
+    # First try tracing the packet.
+    uflow="inport==\"lp-$inport\" && eth.dst==$dst && eth.src==$src && eth.type==0x$eth"
+    AT_CAPTURE_FILE([trace])
+    AT_CHECK([ovn-trace --all ls-${hv}0 "$uflow" | tee trace | sed '1,/Minimal trace/d'], [0], [expout])
+
+    # Then actually send a packet, for an end-to-end test.
+    local packet=$(echo $dst$src | sed 's/://g')${eth}
+    as hv-$hv ovs-appctl netdev-dummy/receive vif-$inport $packet
+
+    if test $lout != unknown; then
+        # Expect the packet received by the peer VIF port
+        echo $packet >> vif-$outport.expected
+    fi
+
+    # regardless, the packet is sent through the bridge
+    local packet=$(echo $dst$src | sed 's/://g')810000$(printf "%.2x\n" ${hv}1)${eth}
+    echo $packet >> out-$hv.expected
+}
+
+test_packet 1 1-1 2-1 f0:00:00:00:00:21 f0:00:00:00:00:11 1001 lp-2-1
+test_packet 2 2-2 1-2 f0:00:00:00:00:12 f0:00:00:00:00:22 1001 lp-1-2
+
+# unknown mac goes through localnet port
+test_packet 1 1-1 2-1 f0:00:00:00:00:e0 f0:00:00:00:00:11 1001 unknown
+test_packet 2 2-2 1-2 f0:00:00:00:00:e0 f0:00:00:00:00:22 1001 unknown
+
+# Now check the packets actually received against the ones expected.
+for hv in 1 2; do
+    for ls in 1 2; do
+        port=$hv-$ls
+        # check that packets targeted to actual vifs arrived on the other end
+        OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv-$hv/vif-$port-tx.pcap], [vif-$port.expected])
+    done
+    # check that all packets, whether to known or unknown mac addresses, were sent to fabric
+    OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv-$hv/br-phys${hv}1_n-${hv}1-tx.pcap], [out-$hv.expected])
+done
+
+OVN_CLEANUP([hv-1],[hv-2])
+
+AT_CLEANUP
+
+AT_SETUP([ovn -- 2 HVs, 2 LS, broadcast traffic with multiple localnet ports per switch])
+ovn_start
+
+for tag in `seq 10 30`; do
+    net_add n-$tag
+done
+
+for i in 1 2; do
+    sim_add hv-$i
+    as hv-$i
+    ovs-vsctl add-br br-phys11
+    ovs-vsctl add-br br-phys21
+    ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys-11:br-phys11,phys-21:br-phys21
+    ovn_attach n-11 br-phys11 192.168.0.${i}1
+    ovn_attach n-21 br-phys21 192.168.0.${i}2
+done
+
+for i in 1 2; do
+    lsname=ls-${i}0
+    ovn-nbctl ls-add $lsname
+    for tag in `seq ${i}1 ${i}9`; do
+        ln_port_name=ln-$tag
+        ovn-nbctl lsp-add $lsname $ln_port_name "" $tag
+        ovn-nbctl lsp-set-addresses $ln_port_name unknown
+        ovn-nbctl lsp-set-type $ln_port_name localnet
+        ovn-nbctl lsp-set-options $ln_port_name network_name=phys-$tag
+    done
+done
+
+for hv in 1 2; do
+    as hv-$hv
+    for ls in 1 2; do
+        for peer in 8 9; do
+            lsp_name=lp-$hv-$ls-$peer
+            ovs-vsctl add-port br-int vif-$hv-$ls-$peer -- \
+                set Interface vif-$hv-$ls-$peer external-ids:iface-id=$lsp_name \
+                                      options:tx_pcap=hv-$hv/vif-$hv-$ls-$peer-tx.pcap \
+                                      options:rxq_pcap=hv-$hv/vif-$hv-$ls-$peer-rx.pcap \
+                                      ofport-request=$hv$ls$peer
+
+            ovn-nbctl lsp-add ls-${ls}0 $lsp_name
+            ovn-nbctl lsp-set-addresses $lsp_name f0:00:00:00:0${peer}:${hv}${ls}
+            ovn-nbctl lsp-set-port-security $lsp_name f0:00:00:00:0${peer}:${hv}${ls}
+
+            OVS_WAIT_UNTIL([test x`ovn-nbctl lsp-get-up $lsp_name` = xup])
+
+            : > vif-$hv-$ls-$peer.expected
+        done
+    done
+done
+
+
+ovn-nbctl --wait=sb sync
+ovn-nbctl show
+ovn-sbctl dump-flows
+
+echo "------ OVN dump ------"
+ovn-nbctl show
+ovn-sbctl show
+
+for i in 1 2; do
+    hv=hv-$i
+    echo "------ $hv dump ------"
+    as $hv ovs-vsctl show
+    as $hv ovs-ofctl -O OpenFlow13 dump-flows br-int
+done
+
+# localnet ports
+for hv in 1 2; do
+    : > out-$hv.expected
+done
+
+test_packet() {
+    local hv=$1 inport=$2 dst=$3 src=$4 eth=$5
+    shift; shift; shift; shift; shift
+
+    : > expout
+    for lout in "$@"; do
+        if test $lout = unknown; then
+            # Expect the packet cloned to all localnet ports
+            for tag in `seq ${hv}1 ${hv}9`; do
+                echo "output(\"ln-$tag\");" >> expout
+            done
+        else
+            echo "output(\"$lout\");" >> expout
+        fi
+    done
+
+    # First try tracing the packet.
+    uflow="inport==\"lp-$inport\" && eth.dst==$dst && eth.src==$src && eth.type==0x$eth"
+    AT_CAPTURE_FILE([trace])
+    AT_CHECK([ovn-trace --all ls-${hv}0 "$uflow" | tee trace | sed '1,/Minimal trace/d' | sort], [0], [expout])
+
+    # Then actually send a packet, for an end-to-end test.
+    local packet=$(echo $dst$src | sed 's/://g')${eth}
+    as hv-$hv ovs-appctl netdev-dummy/receive vif-$inport $packet
+
+    for lout in "$@"; do
+        if test $lout != unknown; then
+            # Expect the packet received by the peer VIF port
+            echo $packet >> vif-${lout#lp-}.expected
+        fi
+    done
+
+    # regardless, the packet is sent through the bridge
+    local packet=$(echo $dst$src | sed 's/://g')810000$(printf "%.2x\n" ${hv}1)${eth}
+    echo $packet >> out-$hv.expected
+}
+
+test_packet 1 1-1-8 f0:00:00:00:08:21 f0:00:00:00:08:11 1001 lp-2-1-8
+test_packet 2 2-2-8 f0:00:00:00:08:12 f0:00:00:00:08:22 1001 lp-1-2-8
+
+# unknown mac goes through localnet port
+test_packet 1 1-1-8 f0:00:00:00:08:e0 f0:00:00:00:08:11 1001 unknown
+test_packet 2 2-2-8 f0:00:00:00:08:e0 f0:00:00:00:08:22 1001 unknown
+
+# broadcast traffic goes to all peers, foreign and local
+test_packet 1 1-1-8 ff:ff:ff:ff:ff:ff f0:00:00:00:08:11 1001 $(for n in `seq 11 19`; do echo ln-$n; done) lp-1-1-9 lp-2-1-8 lp-2-1-9
+
+# Now check the packets actually received against the ones expected.
+for hv in 1 2; do
+    for ls in 1 2; do
+        for peer in 8 9; do
+            port=$hv-$ls-$peer
+            # check that packets targeted to actual vifs arrived on the other end
+            OVN_CHECK_PACKETS_CONTAIN([hv-$hv/vif-$port-tx.pcap], [vif-$port.expected])
+        done
+    done
+    # check that all packets, whether to known or unknown mac addresses, were sent to fabric
+    OVN_CHECK_PACKETS_CONTAIN([hv-$hv/br-phys${hv}1_n-${hv}1-tx.pcap], [out-$hv.expected])
+done
+
+OVN_CLEANUP([hv-1],[hv-2])
+
+AT_CLEANUP
+
+AT_SETUP([ovn -- 2 HVs, 2 LS, switching between multiple localnet ports with same tags])
+ovn_start
+
+# In this test case we create two switches with multiple localnet ports. Only a
+# single localnet of the same tag is connected to fabric for each switch. Two
+# hypervisors have VIFs that belong to these switches. The test validates that
+# routing between these switches and hypervisors still works regardless of the
+# number of (unplugged) localnet ports.
+
+# two switches, each connected to lots of networks
+for i in 1 2; do
+    ovn-nbctl ls-add ls-$i
+    for tag in `seq 10 20`; do
+        ln_port_name=ln-$i-$tag
+        ovn-nbctl lsp-add ls-$i $ln_port_name "" $tag
+        ovn-nbctl lsp-set-addresses $ln_port_name unknown
+        ovn-nbctl lsp-set-type $ln_port_name localnet
+        ovn-nbctl lsp-set-options $ln_port_name network_name=phys-$tag
+    done
+done
+
+# multiple networks
+for tag in `seq 10 20`; do
+    net_add n-$tag
+done
+
+# two hypervisors, each connected to the same network
+for i in 1 2; do
+    sim_add hv-$i
+    as hv-$i
+    ovs-vsctl add-br br-phys
+    ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys-20:br-phys
+    ovn_attach n-10 br-phys 192.168.0.$i
+done
+
+# two vif ports, one per switch
+for i in 1 2; do
+    as hv-$i
+    ovs-vsctl add-port br-int vif-$i -- \
+        set Interface vif-$i external-ids:iface-id=lp-$i \
+                              options:tx_pcap=hv-$i/vif-$i-tx.pcap \
+                              options:rxq_pcap=hv-$i/vif-$i-rx.pcap \
+                              ofport-request=$i
+
+    lsp_name=lp-$i
+    ovn-nbctl lsp-add ls-$i $lsp_name
+    ovn-nbctl lsp-set-addresses $lsp_name f0:00:00:00:00:0$i
+    ovn-nbctl lsp-set-port-security $lsp_name f0:00:00:00:00:0$i
+
+    OVS_WAIT_UNTIL([test x`ovn-nbctl lsp-get-up $lsp_name` = xup])
+done
+
+ovn-nbctl --wait=sb sync
+ovn-nbctl show
+ovn-sbctl dump-flows
+
+# vif ports
+for i in 1 2; do
+    : > vif-$i.expected
+done
+
+# localnet ports
+for i in 1 2; do
+    for tag in `seq 10 20`; do
+        : > $i-$tag.expected
+    done
+done
+
+test_packet() {
+    local inport=$1 outport=$2 dst=$3 src=$4 eth=$5 eout=$6 lout=$7
+
+    # Expect the packet cloned to all localnet ports
+    : > expout
+    for tag in `seq 10 20`; do
+        echo "output(\"ln-$inport-$tag\");" >> expout
+    done
+
+    # First try tracing the packet.
+    uflow="inport==\"lp-$inport\" && eth.dst==$dst && eth.src==$src && eth.type==0x$eth"
+    AT_CAPTURE_FILE([trace])
+    AT_CHECK([ovn-trace --all ls-$inport "$uflow" | tee trace | sed '1,/Minimal trace/d'], [0], [expout])
+
+    # Then actually send a packet, for an end-to-end test.
+    local packet=$(echo $dst$src | sed 's/://g')${eth}
+    as hv-$1 ovs-appctl netdev-dummy/receive vif-$inport $packet
+
+    # Expect the packet received by the peer VIF port
+    echo $packet >> vif-$outport.expected
+
+    # Expect the packet to transfer through the common fabric network
+    local packet=$(echo $dst$src | sed 's/://g')810000$(printf "%.2x" 20)${eth}
+    echo $packet >> $1-10.expected
+}
+
+test_packet 1 2 f0:00:00:00:00:02 f0:00:00:00:00:01 1001 ln-1-10 ln-1-10
+test_packet 1 2 f0:00:00:00:00:02 f0:00:00:00:00:01 1002 ln-1-10 ln-1-10
+
+test_packet 2 1 f0:00:00:00:00:01 f0:00:00:00:00:02 1003 ln-2-10 ln-2-10
+test_packet 2 1 f0:00:00:00:00:01 f0:00:00:00:00:02 1004 ln-2-10 ln-2-10
+
+# Dump a bunch of info helpful for debugging if there's a failure.
+
+echo "------ OVN dump ------"
+ovn-nbctl show
+ovn-sbctl show
+
+for i in 1 2; do
+    hv=hv-$i
+    echo "------ $hv dump ------"
+    as $hv ovs-vsctl show
+    as $hv ovs-ofctl -O OpenFlow13 dump-flows br-int
+done
+
+# Now check the packets actually received against the ones expected.
+for i in 1 2; do
+    OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv-$i/vif-$i-tx.pcap], [vif-$i.expected])
+    OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv-$i/br-phys_n-10-tx.pcap], [$i-10.expected])
+done
+
+OVN_CLEANUP([hv-1],[hv-2])
+
+AT_CLEANUP
+
+AT_SETUP([ovn -- 2 HVs, 1 LS, no switching between multiple localnet ports with different tags])
+ovn_start
+
+# In this test case we create a single switch connected to two physical
+# networks via two localnet ports. Then we create two hypervisors, with 2
+# ports on each. The test validates no interconnectivity between VIF ports
+# located on chassis plugged to different physical networks.
+
+# create the single switch with two locanet ports
+ovn-nbctl ls-add ls1
+for tag in 10 20; do
+    ln_port_name=ln-$tag
+    ovn-nbctl lsp-add ls1 $ln_port_name "" $tag
+    ovn-nbctl lsp-set-addresses $ln_port_name unknown
+    ovn-nbctl lsp-set-type $ln_port_name localnet
+    ovn-nbctl lsp-set-options $ln_port_name network_name=phys-$tag
+done
+
+# create fabric networks
+for tag in 10 20; do
+    net_add n-$tag
+done
+
+# create four chassis, each connected to one network, each with a single VIF port
+for tag in 10 20; do
+    for i in 1 2; do
+        sim_add hv-$tag-$i
+        as hv-$tag-$i
+        ovs-vsctl add-br br-phys
+        ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys-$tag:br-phys
+        ovn_attach n-$tag br-phys 192.168.$i.$tag
+
+        ovs-vsctl add-port br-int vif-$tag-$i -- \
+            set Interface vif-$tag-$i external-ids:iface-id=lp-$tag-$i \
+                                  options:tx_pcap=hv-$tag-$i/vif-$tag-$i-tx.pcap \
+                                  options:rxq_pcap=hv-$tag-$i/vif-$tag-$i-rx.pcap \
+                                  ofport-request=$tag$i
+
+        lsp_name=lp-$tag-$i
+        ovn-nbctl lsp-add ls1 $lsp_name
+        ovn-nbctl lsp-set-addresses $lsp_name f0:00:00:00:0$i:$tag
+        ovn-nbctl lsp-set-port-security $lsp_name f0:00:00:00:0$i:$tag
+
+        OVS_WAIT_UNTIL([test x`ovn-nbctl lsp-get-up $lsp_name` = xup])
+    done
+done
+ovn-nbctl --wait=sb sync
+ovn-sbctl dump-flows
+
+for tag in 10 20; do
+    for i in 1 2; do
+        : > $tag-$i.expected
+    done
+done
+
+vif_to_hv() {
+    echo hv-$1
+}
+
+test_packet() {
+    local inport=$1 dst=$2 src=$3 eth=$4 eout=$5 lout=$6
+
+    # First try tracing the packet.
+    uflow="inport==\"lp-$inport\" && eth.dst==$dst && eth.src==$src && eth.type==0x$eth"
+    echo "output(\"$lout\");" > expout
+    AT_CAPTURE_FILE([trace])
+    AT_CHECK([ovn-trace --all ls1 "$uflow" | tee trace | sed '1,/Minimal trace/d'], [0], [expout])
+
+    # Then actually send a packet, for an end-to-end test.
+    local packet=$(echo $dst$src | sed 's/://g')${eth}
+    hv=`vif_to_hv $inport`
+    vif=vif-$inport
+    as $hv ovs-appctl netdev-dummy/receive $vif $packet
+    if test $eth = 1002 -o $eth = 2002; then
+        echo $packet >> ${eout#lp-}.expected
+    fi
+}
+
+# different fabric networks -> should fail
+test_packet 10-1 f0:00:00:00:01:20 f0:00:00:00:01:10 1001 lp-20-1 lp-20-1
+test_packet 20-1 f0:00:00:00:01:10 f0:00:00:00:01:20 2001 lp-10-1 lp-10-1
+
+# same fabric networks -> should pass
+test_packet 10-1 f0:00:00:00:02:10 f0:00:00:00:01:10 1002 lp-10-2 lp-10-2
+test_packet 20-1 f0:00:00:00:02:20 f0:00:00:00:01:20 2002 lp-20-2 lp-20-2
+test_packet 10-2 f0:00:00:00:01:10 f0:00:00:00:02:10 1002 lp-10-1 lp-10-1
+test_packet 20-2 f0:00:00:00:01:20 f0:00:00:00:02:20 2002 lp-20-1 lp-20-1
+
+# Dump a bunch of info helpful for debugging if there's a failure.
+echo "------ OVN dump ------"
+ovn-nbctl show
+ovn-sbctl show
+
+for tag in 10 20; do
+    for i in 1 2; do
+        hv=hv-$tag-$i
+        echo "------ $hv dump ------"
+        as $hv ovs-vsctl show
+        as $hv ovs-ofctl -O OpenFlow13 dump-flows br-int
+    done
+done
+
+# Now check the packets actually received against the ones expected.
+for tag in 10 20; do
+    for i in 1 2; do
+        echo "hv = $tag-$i"
+        OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv-$tag-$i/vif-$tag-$i-tx.pcap], [$tag-$i.expected])
+    done
+done
+
+OVN_CLEANUP([hv-10-1],[hv-10-2],[hv-20-1],[hv-20-2])
+
+AT_CLEANUP
+
 AT_SETUP([ovn -- vtep: 3 HVs, 1 VIFs/HV, 1 GW, 1 LS])
 AT_KEYWORDS([vtep])
 ovn_start