diff mbox series

[ovs-dev,RFC,ovn] Support logical switches with multiple localnet ports

Message ID 20200415014412.449850-1-ihrachys@redhat.com
State Superseded, archived
Headers show
Series [ovs-dev,RFC,ovn] Support logical switches with multiple localnet ports | expand

Commit Message

Ihar Hrachyshka April 15, 2020, 1:44 a.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.

TODO: consider E-W routing between localnet vlan tagged LSs
      (ovn-chassis-mac-mappings).

Note: the test requires [2] to actually validate packets.

[1]: https://docs.openstack.org/ocata/networking-guide/config-routed-networks.html
[2]: https://patchwork.ozlabs.org/project/openvswitch/list/?series=169291

Signed-off-by: Ihar Hrachyshka <ihrachys@redhat.com>
---
 controller/binding.c   |  16 ++
 controller/patch.c     |  24 ++-
 northd/ovn-northd.c    | 351 ++++++++++++++++++++++-------------------
 ovn-architecture.7.xml |  25 ++-
 ovn-nb.xml             |  21 +--
 ovn-sb.xml             |  21 ++-
 tests/ovn.at           | 112 +++++++++++++
 7 files changed, 373 insertions(+), 197 deletions(-)

Comments

Maciej Jozefczyk April 23, 2020, 11:53 a.m. UTC | #1
Hey,

I tested this patch with multi node devstack environment - more detailed
information: [1]

I verified that those things are working:
* DHCP and metadata for instances in all segments (VM1, VM2, VM3)
* traffic from VM1 to VM3 (same segment, different chassis)
* traffic from host1 to VM1 (from provider network segment-1 to VM using
OVN and vice versa)
* traffic from host2 to VM2 (from provider network segment-2 to VM using
OVN and vice versa)
* Security groups for traffic within OVN (VM1<>VM3) and outside
(VM1<>host1, VM2<>host2).
* ARP responder (tested between VM1 and VM3).

Things to check:
* possibility of binding localnet ports on bridge mappings change or
recomputation (only)


Tested-By: Maciej Jozefczyk <mjozefcz@redhat.com>

[1]
https://github.com/mjozefcz/vagrants/tree/master/ovn-routed-provider-networks-devstack


On Wed, Apr 15, 2020 at 3:45 AM Ihar Hrachyshka <ihrachys@redhat.com> wrote:

> 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.
>
> TODO: consider E-W routing between localnet vlan tagged LSs
>       (ovn-chassis-mac-mappings).
>
> Note: the test requires [2] to actually validate packets.
>
> [1]:
> https://docs.openstack.org/ocata/networking-guide/config-routed-networks.html
> [2]: https://patchwork.ozlabs.org/project/openvswitch/list/?series=169291
>
> Signed-off-by: Ihar Hrachyshka <ihrachys@redhat.com>
> ---
>  controller/binding.c   |  16 ++
>  controller/patch.c     |  24 ++-
>  northd/ovn-northd.c    | 351 ++++++++++++++++++++++-------------------
>  ovn-architecture.7.xml |  25 ++-
>  ovn-nb.xml             |  21 +--
>  ovn-sb.xml             |  21 ++-
>  tests/ovn.at           | 112 +++++++++++++
>  7 files changed, 373 insertions(+), 197 deletions(-)
>
> diff --git a/controller/binding.c b/controller/binding.c
> index 5ea12a8be..f4ae42806 100644
> --- a/controller/binding.c
> +++ b/controller/binding.c
> @@ -680,12 +680,28 @@ 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");
> +    if (!network) {
> +        return false;
> +    }
> +    return shash_find_data(bridge_mappings, network);
> +}
> +
>  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..52255cc3a 100644
> --- a/controller/patch.c
> +++ b/controller/patch.c
> @@ -198,9 +198,9 @@ add_bridge_mappings(struct ovsdb_idl_txn *ovs_idl_txn,
>              continue;
>          }
>
> -        const char *patch_port_id;
> +        bool is_localnet = false;
>          if (!strcmp(binding->type, "localnet")) {
> -            patch_port_id = "ovn-localnet-port";
> +            is_localnet = true;
>          } else if (!strcmp(binding->type, "l2gateway")) {
>              if (!binding->chassis
>                  || strcmp(chassis->name, binding->chassis->name)) {
> @@ -208,7 +208,6 @@ add_bridge_mappings(struct ovsdb_idl_txn *ovs_idl_txn,
>                   * so we should not create any patch ports for it. */
>                  continue;
>              }
> -            patch_port_id = "ovn-l2gateway-port";
>          } else {
>              /* not a localnet or L2 gateway port. */
>              continue;
> @@ -224,12 +223,25 @@ 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;
>          }
>
> +        const char *patch_port_id;
> +        if (is_localnet) {
> +            patch_port_id = "ovn-localnet-port";
> +        } else {
> +            patch_port_id = "ovn-l2gateway-port";
> +        }
> +
>          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(ovs_idl_txn, patch_port_id,
> binding->logical_port,
> diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
> index 076278197..91730f7f3 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);
>
> @@ -2053,7 +2056,11 @@ join_logical_ports(struct northd_context *ctx,
>                  }
>
>                  if (!strcmp(nbsp->type, "localnet")) {
> -                   od->localnet_port = op;
> +                   od->localnet_ports = xrealloc(
> +                       od->localnet_ports,
> +                       sizeof *od->localnet_ports * (od->n_localnet_ports
> + 1)
> +                   );
> +                   od->localnet_ports[od->n_localnet_ports++] = op;
>                  }
>
>                  op->lsp_addrs
> @@ -2974,7 +2981,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->localnet_ports) {
>                  add_router_port_garp = true;
>              }
>
> @@ -4662,23 +4669,25 @@ build_pre_acls(struct ovn_datapath *od, struct
> hmap *lflows)
>              ds_destroy(&match_in);
>              ds_destroy(&match_out);
>          }
> -        if (od->localnet_port) {
> -            struct ds match_in = DS_EMPTY_INITIALIZER;
> -            struct ds match_out = DS_EMPTY_INITIALIZER;
> +        if (od->localnet_ports) {
> +            for (size_t i = 0; i < od->n_localnet_ports; i++) {
> +                struct ds match_in = DS_EMPTY_INITIALIZER;
> +                struct ds match_out = DS_EMPTY_INITIALIZER;
>
> -            ds_put_format(&match_in, "ip && inport == %s",
> -                          od->localnet_port->json_key);
> -            ds_put_format(&match_out, "ip && outport == %s",
> -                          od->localnet_port->json_key);
> -            ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_PRE_ACL, 110,
> -                                    ds_cstr(&match_in), "next;",
> -                                    &od->localnet_port->nbsp->header_);
> -            ovn_lflow_add_with_hint(lflows, od, S_SWITCH_OUT_PRE_ACL, 110,
> -                                    ds_cstr(&match_out), "next;",
> -                                    &od->localnet_port->nbsp->header_);
> +                ds_put_format(&match_in, "ip && inport == %s",
> +                              od->localnet_ports[i]->json_key);
> +                ds_put_format(&match_out, "ip && outport == %s",
> +                              od->localnet_ports[i]->json_key);
> +                ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_PRE_ACL,
> 110,
> +                                        ds_cstr(&match_in), "next;",
> +
> &od->localnet_ports[i]->nbsp->header_);
> +                ovn_lflow_add_with_hint(lflows, od, S_SWITCH_OUT_PRE_ACL,
> 110,
> +                                        ds_cstr(&match_out), "next;",
> +
> &od->localnet_ports[i]->nbsp->header_);
>
> -            ds_destroy(&match_in);
> -            ds_destroy(&match_out);
> +                ds_destroy(&match_in);
> +                ds_destroy(&match_out);
> +            }
>          }
>
>          /* Ingress and Egress Pre-ACL Table (Priority 110).
> @@ -5919,9 +5928,11 @@ 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) {
> -        ds_put_format(&actions, "clone { outport = %s; output; }; ",
> -                      od->localnet_port->json_key);
> +    if (od->localnet_ports) {
> +        for (size_t i = 0; i < od->n_localnet_ports; i++) {
> +            ds_put_format(&actions, "clone { outport = %s; output; }; ",
> +                          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,
> @@ -6323,9 +6334,9 @@ 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->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;
>          }
> @@ -6339,84 +6350,93 @@ build_lswitch_flows(struct hmap *datapaths, struct
> hmap *ports,
>                  stage_hint = NULL;
>              }
>
> -            for (size_t j = 0; j < op->lsp_addrs[i].n_ipv4_addrs; j++) {
> -                struct ds options_action = DS_EMPTY_INITIALIZER;
> -                struct ds response_action = DS_EMPTY_INITIALIZER;
> -                struct ds ipv4_addr_match = DS_EMPTY_INITIALIZER;
> -                if (build_dhcpv4_action(
> -                        op, op->lsp_addrs[i].ipv4_addrs[j].addr,
> -                        &options_action, &response_action,
> &ipv4_addr_match)) {
> -                    ds_clear(&match);
> -                    ds_put_format(
> -                        &match, "inport == %s && eth.src == %s && "
> -                        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255
> && "
> -                        "udp.src == 68 && udp.dst == 67",
> -                        is_external ? op->od->localnet_port->json_key :
> -                            op->json_key,
> -                        op->lsp_addrs[i].ea_s);
> -
> -                    if (is_external) {
> -                        ds_put_format(&match, " &&
> is_chassis_resident(%s)",
> -                                      op->json_key);
> -                    }
> +            size_t jmax = is_external? op->od->n_localnet_ports: 1;
> +            for (size_t j = 0; j < jmax; j++) {
> +                for (size_t k = 0; k < op->lsp_addrs[i].n_ipv4_addrs;
> k++) {
> +                    struct ds options_action = DS_EMPTY_INITIALIZER;
> +                    struct ds response_action = DS_EMPTY_INITIALIZER;
> +                    struct ds ipv4_addr_match = DS_EMPTY_INITIALIZER;
> +                    if (build_dhcpv4_action(
> +                            op, op->lsp_addrs[i].ipv4_addrs[k].addr,
> +                            &options_action, &response_action,
> +                            &ipv4_addr_match)) {
> +                        ds_clear(&match);
> +                        ds_put_format(
> +                            &match,
> +                            "inport == %s && eth.src == %s && "
> +                            "ip4.src == 0.0.0.0 && "
> +                            "ip4.dst == 255.255.255.255 && "
> +                            "udp.src == 68 && udp.dst == 67",
> +                            is_external ?
> op->od->localnet_ports[j]->json_key :
> +                                op->json_key,
> +                            op->lsp_addrs[i].ea_s);
>
> -                    ovn_lflow_add_with_hint(lflows, op->od,
> -                                            S_SWITCH_IN_DHCP_OPTIONS, 100,
> -                                            ds_cstr(&match),
> -                                            ds_cstr(&options_action),
> -                                            stage_hint);
> -                    ds_clear(&match);
> -                    /* Allow ip4.src = OFFER_IP and
> -                     * ip4.dst = {SERVER_IP, 255.255.255.255} for the
> below
> -                     * cases
> -                     *  -  When the client wants to renew the IP by
> sending
> -                     *     the DHCPREQUEST to the server ip.
> -                     *  -  When the client wants to renew the IP by
> -                     *     broadcasting the DHCPREQUEST.
> -                     */
> -                    ds_put_format(
> -                        &match, "inport == %s && eth.src == %s && "
> -                        "%s && udp.src == 68 && udp.dst == 67",
> -                        is_external ? op->od->localnet_port->json_key :
> -                            op->json_key,
> -                        op->lsp_addrs[i].ea_s, ds_cstr(&ipv4_addr_match));
> +                        if (is_external) {
> +                            ds_put_format(
> +                                &match, " && is_chassis_resident(%s)",
> +                                op->json_key);
> +                        }
>
> -                    if (is_external) {
> -                        ds_put_format(&match, " &&
> is_chassis_resident(%s)",
> -                                      op->json_key);
> -                    }
> +                        ovn_lflow_add_with_hint(lflows, op->od,
> +                                                S_SWITCH_IN_DHCP_OPTIONS,
> 100,
> +                                                ds_cstr(&match),
> +                                                ds_cstr(&options_action),
> +                                                stage_hint);
> +                        ds_clear(&match);
> +                        /* Allow ip4.src = OFFER_IP and
> +                         * ip4.dst = {SERVER_IP, 255.255.255.255} for the
> below
> +                         * cases
> +                         *  -  When the client wants to renew the IP by
> sending
> +                         *     the DHCPREQUEST to the server ip.
> +                         *  -  When the client wants to renew the IP by
> +                         *     broadcasting the DHCPREQUEST.
> +                         */
> +                        ds_put_format(
> +                            &match, "inport == %s && eth.src == %s && "
> +                            "%s && udp.src == 68 && udp.dst == 67",
> +                            is_external ?
> op->od->localnet_ports[j]->json_key :
> +                                op->json_key,
> +                            op->lsp_addrs[i].ea_s,
> ds_cstr(&ipv4_addr_match));
> +
> +                        if (is_external) {
> +                            ds_put_format(
> +                                &match, " && is_chassis_resident(%s)",
> +                                op->json_key);
> +                        }
>
> -                    ovn_lflow_add_with_hint(lflows, op->od,
> -                                            S_SWITCH_IN_DHCP_OPTIONS, 100,
> -                                            ds_cstr(&match),
> -                                            ds_cstr(&options_action),
> -                                            stage_hint);
> -                    ds_clear(&match);
> +                        ovn_lflow_add_with_hint(lflows, op->od,
> +                                                S_SWITCH_IN_DHCP_OPTIONS,
> 100,
> +                                                ds_cstr(&match),
> +                                                ds_cstr(&options_action),
> +                                                stage_hint);
> +                        ds_clear(&match);
>
> -                    /* If REGBIT_DHCP_OPTS_RESULT is set, it means the
> -                     * put_dhcp_opts action  is successful. */
> -                    ds_put_format(
> -                        &match, "inport == %s && eth.src == %s && "
> -                        "ip4 && udp.src == 68 && udp.dst == 67"
> -                        " && "REGBIT_DHCP_OPTS_RESULT,
> -                        is_external ? op->od->localnet_port->json_key :
> -                            op->json_key,
> -                        op->lsp_addrs[i].ea_s);
> -
> -                    if (is_external) {
> -                        ds_put_format(&match, " &&
> is_chassis_resident(%s)",
> -                                      op->json_key);
> -                    }
> +                        /* If REGBIT_DHCP_OPTS_RESULT is set, it means the
> +                         * put_dhcp_opts action  is successful. */
> +                        ds_put_format(
> +                            &match, "inport == %s && eth.src == %s && "
> +                            "ip4 && udp.src == 68 && udp.dst == 67"
> +                            " && "REGBIT_DHCP_OPTS_RESULT,
> +                            is_external ?
> op->od->localnet_ports[j]->json_key :
> +                                op->json_key,
> +                            op->lsp_addrs[i].ea_s);
>
> -                    ovn_lflow_add_with_hint(lflows, op->od,
> -                                            S_SWITCH_IN_DHCP_RESPONSE,
> 100,
> -                                            ds_cstr(&match),
> -                                            ds_cstr(&response_action),
> -                                            stage_hint);
> -                    ds_destroy(&options_action);
> -                    ds_destroy(&response_action);
> -                    ds_destroy(&ipv4_addr_match);
> -                    break;
> +                        if (is_external) {
> +                            ds_put_format(
> +                                &match, " && is_chassis_resident(%s)",
> +                                op->json_key);
> +                        }
> +
> +                        ovn_lflow_add_with_hint(lflows, op->od,
> +
> S_SWITCH_IN_DHCP_RESPONSE, 100,
> +                                                ds_cstr(&match),
> +                                                ds_cstr(&response_action),
> +                                                stage_hint);
> +                        ds_destroy(&options_action);
> +                        ds_destroy(&response_action);
> +                        ds_destroy(&ipv4_addr_match);
> +                        break;
> +                    }
>                  }
>              }
>
> @@ -6426,43 +6446,46 @@ build_lswitch_flows(struct hmap *datapaths, struct
> hmap *ports,
>                  stage_hint = NULL;
>              }
>
> -            for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
> -                struct ds options_action = DS_EMPTY_INITIALIZER;
> -                struct ds response_action = DS_EMPTY_INITIALIZER;
> -                if (build_dhcpv6_action(
> -                        op, &op->lsp_addrs[i].ipv6_addrs[j].addr,
> -                        &options_action, &response_action)) {
> -                    ds_clear(&match);
> -                    ds_put_format(
> -                        &match, "inport == %s && eth.src == %s"
> -                        " && ip6.dst == ff02::1:2 && udp.src == 546 &&"
> -                        " udp.dst == 547",
> -                        is_external ? op->od->localnet_port->json_key :
> -                            op->json_key,
> -                        op->lsp_addrs[i].ea_s);
> -
> -                    if (is_external) {
> -                        ds_put_format(&match, " &&
> is_chassis_resident(%s)",
> -                                      op->json_key);
> -                    }
> +            for (size_t j = 0; j < jmax; j++) {
> +                for (size_t k = 0; k < op->lsp_addrs[i].n_ipv6_addrs;
> k++) {
> +                    struct ds options_action = DS_EMPTY_INITIALIZER;
> +                    struct ds response_action = DS_EMPTY_INITIALIZER;
> +                    if (build_dhcpv6_action(
> +                            op, &op->lsp_addrs[i].ipv6_addrs[k].addr,
> +                            &options_action, &response_action)) {
> +                        ds_clear(&match);
> +                        ds_put_format(
> +                            &match, "inport == %s && eth.src == %s"
> +                            " && ip6.dst == ff02::1:2 && udp.src == 546
> &&"
> +                            " udp.dst == 547",
> +                            is_external ?
> op->od->localnet_ports[j]->json_key :
> +                                op->json_key,
> +                            op->lsp_addrs[i].ea_s);
>
> -                    ovn_lflow_add_with_hint(lflows, op->od,
> -                                            S_SWITCH_IN_DHCP_OPTIONS, 100,
> -                                            ds_cstr(&match),
> -                                            ds_cstr(&options_action),
> -                                            stage_hint);
> +                        if (is_external) {
> +                            ds_put_format(
> +                                &match, " && is_chassis_resident(%s)",
> +                                op->json_key);
> +                        }
>
> -                    /* If REGBIT_DHCP_OPTS_RESULT is set to 1, it means
> the
> -                     * put_dhcpv6_opts action is successful */
> -                    ds_put_cstr(&match, " && "REGBIT_DHCP_OPTS_RESULT);
> -                    ovn_lflow_add_with_hint(lflows, op->od,
> -                                            S_SWITCH_IN_DHCP_RESPONSE,
> 100,
> -                                            ds_cstr(&match),
> -                                            ds_cstr(&response_action),
> -                                            stage_hint);
> -                    ds_destroy(&options_action);
> -                    ds_destroy(&response_action);
> -                    break;
> +                        ovn_lflow_add_with_hint(lflows, op->od,
> +                                                S_SWITCH_IN_DHCP_OPTIONS,
> 100,
> +                                                ds_cstr(&match),
> +                                                ds_cstr(&options_action),
> +                                                stage_hint);
> +
> +                        /* If REGBIT_DHCP_OPTS_RESULT is set to 1, it
> means the
> +                         * put_dhcpv6_opts action is successful */
> +                        ds_put_cstr(&match, " &&
> "REGBIT_DHCP_OPTS_RESULT);
> +                        ovn_lflow_add_with_hint(lflows, op->od,
> +
> S_SWITCH_IN_DHCP_RESPONSE, 100,
> +                                                ds_cstr(&match),
> +                                                ds_cstr(&response_action),
> +                                                stage_hint);
> +                        ds_destroy(&options_action);
> +                        ds_destroy(&response_action);
> +                        break;
> +                    }
>                  }
>              }
>          }
> @@ -6521,7 +6544,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) {
> +            !op->od->localnet_ports) {
>             continue;
>          }
>
> @@ -6536,36 +6559,42 @@ build_lswitch_flows(struct hmap *datapaths, struct
> hmap *ports,
>                  for (size_t k = 0; k < rp->n_lsp_addrs; k++) {
>                      for (size_t l = 0; l < rp->lsp_addrs[k].n_ipv4_addrs;
>                           l++) {
> -                        ds_clear(&match);
> -                        ds_put_format(
> -                            &match, "inport == %s && eth.src == %s"
> -                            " && !is_chassis_resident(%s)"
> -                            " && arp.tpa == %s && arp.op == 1",
> -                            op->od->localnet_port->json_key,
> -                            op->lsp_addrs[i].ea_s, op->json_key,
> -                            rp->lsp_addrs[k].ipv4_addrs[l].addr_s);
> -                        ovn_lflow_add_with_hint(lflows, op->od,
> -                                                S_SWITCH_IN_EXTERNAL_PORT,
> -                                                100, ds_cstr(&match),
> "drop;",
> -                                                &op->nbsp->header_);
> +                        for (size_t m = 0; m < op->od->n_localnet_ports;
> m++) {
> +                            ds_clear(&match);
> +                            ds_put_format(
> +                                &match, "inport == %s && eth.src == %s"
> +                                " && !is_chassis_resident(%s)"
> +                                " && arp.tpa == %s && arp.op == 1",
> +                                op->od->localnet_ports[m]->json_key,
> +                                op->lsp_addrs[i].ea_s, op->json_key,
> +                                rp->lsp_addrs[k].ipv4_addrs[l].addr_s);
> +                            ovn_lflow_add_with_hint(
> +                                lflows, op->od,
> +                                S_SWITCH_IN_EXTERNAL_PORT, 100,
> +                                ds_cstr(&match), "drop;",
> +                                &op->nbsp->header_);
> +                        }
>                      }
>                      for (size_t l = 0; l < rp->lsp_addrs[k].n_ipv6_addrs;
>                           l++) {
> -                        ds_clear(&match);
> -                        ds_put_format(
> -                            &match, "inport == %s && eth.src == %s"
> -                            " && !is_chassis_resident(%s)"
> -                            " && nd_ns && ip6.dst == {%s, %s} && "
> -                            "nd.target == %s",
> -                            op->od->localnet_port->json_key,
> -                            op->lsp_addrs[i].ea_s, op->json_key,
> -                            rp->lsp_addrs[k].ipv6_addrs[l].addr_s,
> -                            rp->lsp_addrs[k].ipv6_addrs[l].sn_addr_s,
> -                            rp->lsp_addrs[k].ipv6_addrs[l].addr_s);
> -                        ovn_lflow_add_with_hint(lflows, op->od,
> -
> S_SWITCH_IN_EXTERNAL_PORT, 100,
> -                                                ds_cstr(&match), "drop;",
> -                                                &op->nbsp->header_);
> +                        for (size_t m = 0; m < op->od->n_localnet_ports;
> m++) {
> +                            ds_clear(&match);
> +                            ds_put_format(
> +                                &match, "inport == %s && eth.src == %s"
> +                                " && !is_chassis_resident(%s)"
> +                                " && nd_ns && ip6.dst == {%s, %s} && "
> +                                "nd.target == %s",
> +                                op->od->localnet_ports[m]->json_key,
> +                                op->lsp_addrs[i].ea_s, op->json_key,
> +                                rp->lsp_addrs[k].ipv6_addrs[l].addr_s,
> +                                rp->lsp_addrs[k].ipv6_addrs[l].sn_addr_s,
> +                                rp->lsp_addrs[k].ipv6_addrs[l].addr_s);
> +                            ovn_lflow_add_with_hint(
> +                                lflows, op->od,
> +                                S_SWITCH_IN_EXTERNAL_PORT, 100,
> +                                ds_cstr(&match), "drop;",
> +                                &op->nbsp->header_);
> +                        }
>                      }
>                  }
>              }
> @@ -6787,7 +6816,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->localnet_ports) {
>                      bool add_chassis_resident_check = false;
>                      if (op->peer == op->peer->od->l3dgw_port) {
>                          /* The peer of this port represents a distributed
> @@ -8084,7 +8113,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->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 533ae716d..88edb6f32 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>
> @@ -1895,13 +1894,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
> @@ -1916,11 +1915,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.
> @@ -1935,13 +1934,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
> @@ -1957,7 +1956,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>
>
> @@ -1967,7 +1966,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
> @@ -1978,12 +1977,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 541ec20c1..6f84c427c 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,13 @@
>
>            <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 corresponding
> +            bridge mapping.  A logical switch can have multiple
> +            <code>localnet</code> ports attached, as long as each
> +            <code>ovn-controller</code> is plugged to a single local
> network
> +            only.  In this case, each hypervisor implements part of switch
> +            external network connectivity.
>            </dd>
>
>            <dt><code>localport</code></dt>
> @@ -721,7 +724,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 3ae9d4f92..2d2d08027 100644
> --- a/ovn-sb.xml
> +++ b/ovn-sb.xml
> @@ -2606,10 +2606,13 @@ 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 some or all
> +            <code>ovn-controller</code> instances.  This is used
> +            to model direct connectivity to existing networks.  A logical
> +            switch can have multiple <code>localnet</code> ports
> attached, as
> +            long as each <code>ovn-controller</code> is plugged to a
> single
> +            local network only.  In this case, each hypervisor implements
> part
> +            of switch external network connectivity.
>            </dd>
>
>            <dt><code>localport</code></dt>
> @@ -2754,10 +2757,12 @@ tcp.flags = RST;
>          <p>
>            When a logical switch has a <code>localnet</code> port attached,
>            every chassis that may have a local vif attached to that logical
> -          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.
> +          switch that needs this external connectivity must have a bridge
> +          mapping configured to reach that <code>localnet</code>.  If the
> +          mapping is missing, the vif won't be plugged to this network.
> It may
> +          still reach the other network if routing is implemented by
> fabric.
> +          Traffic that arrives on a <code>localnet</code> port is never
> +          forwarded over a tunnel to another chassis.
>          </p>
>        </column>
>
> diff --git a/tests/ovn.at b/tests/ovn.at
> index 013583826..42089763a 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -2438,6 +2438,118 @@ OVN_CLEANUP([hv1],[hv2])
>
>  AT_CLEANUP
>
> +AT_SETUP([ovn -- 2 HVs, multiple localnet ports])
> +ovn_start
> +
> +# In this test case we create a single switch connected to two physical
> +# networks via multiple localnet ports. Then we create two hypervisors,
> with 2
> +# ports on each. Each pair of adjecent ports belong to the same network
> segment
> +# and assume interconnectivity. There is no direct interconnectivity
> between
> +# ports located on chassis attached to different segments. (It is assumed
> that
> +# in real life external fabric L3 routing will deliver packets between the
> +# segments as needed.)
> +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
> +
> +for tag in 10 20; do
> +    net_add n-$tag
> +done
> +
> +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
> +}
> +
> +# 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
> +
> +# 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
> +
> +# 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
> --
> 2.25.2
>
>
> _______________________________________________
> dev mailing list
> dev@openvswitch.org
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
>
>
Dumitru Ceara April 23, 2020, 1:31 p.m. UTC | #2
On 4/15/20 3:44 AM, Ihar Hrachyshka wrote:
> 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.
> 
> TODO: consider E-W routing between localnet vlan tagged LSs
>       (ovn-chassis-mac-mappings).
> 
> Note: the test requires [2] to actually validate packets.
> 
> [1]: https://docs.openstack.org/ocata/networking-guide/config-routed-networks.html
> [2]: https://patchwork.ozlabs.org/project/openvswitch/list/?series=169291
> 
> Signed-off-by: Ihar Hrachyshka <ihrachys@redhat.com>

Hi Ihar,

I only reviewed the C files (so no doc or tests) and that part looks ok
to me. I do have a few (non-functionality related) comments inline.

Thanks,
Dumitru

> ---
>  controller/binding.c   |  16 ++
>  controller/patch.c     |  24 ++-
>  northd/ovn-northd.c    | 351 ++++++++++++++++++++++-------------------
>  ovn-architecture.7.xml |  25 ++-
>  ovn-nb.xml             |  21 +--
>  ovn-sb.xml             |  21 ++-
>  tests/ovn.at           | 112 +++++++++++++
>  7 files changed, 373 insertions(+), 197 deletions(-)
> 
> diff --git a/controller/binding.c b/controller/binding.c
> index 5ea12a8be..f4ae42806 100644
> --- a/controller/binding.c
> +++ b/controller/binding.c
> @@ -680,12 +680,28 @@ 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");
> +    if (!network) {
> +        return false;
> +    }
> +    return shash_find_data(bridge_mappings, network);
> +}
> +
>  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..52255cc3a 100644
> --- a/controller/patch.c
> +++ b/controller/patch.c
> @@ -198,9 +198,9 @@ add_bridge_mappings(struct ovsdb_idl_txn *ovs_idl_txn,
>              continue;
>          }
>  
> -        const char *patch_port_id;
> +        bool is_localnet = false;
>          if (!strcmp(binding->type, "localnet")) {
> -            patch_port_id = "ovn-localnet-port";
> +            is_localnet = true;
>          } else if (!strcmp(binding->type, "l2gateway")) {
>              if (!binding->chassis
>                  || strcmp(chassis->name, binding->chassis->name)) {
> @@ -208,7 +208,6 @@ add_bridge_mappings(struct ovsdb_idl_txn *ovs_idl_txn,
>                   * so we should not create any patch ports for it. */
>                  continue;
>              }
> -            patch_port_id = "ovn-l2gateway-port";
>          } else {
>              /* not a localnet or L2 gateway port. */
>              continue;
> @@ -224,12 +223,25 @@ 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;
>          }
>  
> +        const char *patch_port_id;
> +        if (is_localnet) {
> +            patch_port_id = "ovn-localnet-port";
> +        } else {
> +            patch_port_id = "ovn-l2gateway-port";
> +        }
> +
>          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(ovs_idl_txn, patch_port_id, binding->logical_port,
> diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
> index 076278197..91730f7f3 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);
>  
> @@ -2053,7 +2056,11 @@ join_logical_ports(struct northd_context *ctx,
>                  }
>  
>                  if (!strcmp(nbsp->type, "localnet")) {
> -                   od->localnet_port = op;
> +                   od->localnet_ports = xrealloc(
> +                       od->localnet_ports,
> +                       sizeof *od->localnet_ports * (od->n_localnet_ports + 1)
> +                   );
> +                   od->localnet_ports[od->n_localnet_ports++] = op;

How many localnet ports would we expect at most for a single logical
switch in a regular deployment? Should we use x2nrealloc() instead of
xrealloc()?

>                  }
>  
>                  op->lsp_addrs
> @@ -2974,7 +2981,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->localnet_ports) {

This works but would it be better to make it more explicit by using "if
(chassis && op->od->n_localnet_ports)" instead?

>                  add_router_port_garp = true;
>              }
>  
> @@ -4662,23 +4669,25 @@ build_pre_acls(struct ovn_datapath *od, struct hmap *lflows)
>              ds_destroy(&match_in);
>              ds_destroy(&match_out);
>          }
> -        if (od->localnet_port) {
> -            struct ds match_in = DS_EMPTY_INITIALIZER;
> -            struct ds match_out = DS_EMPTY_INITIALIZER;
> +        if (od->localnet_ports) {

We could skip this if. The condition in the "for" loop below is enough.

> +            for (size_t i = 0; i < od->n_localnet_ports; i++) {
> +                struct ds match_in = DS_EMPTY_INITIALIZER;
> +                struct ds match_out = DS_EMPTY_INITIALIZER;
>  
> -            ds_put_format(&match_in, "ip && inport == %s",
> -                          od->localnet_port->json_key);
> -            ds_put_format(&match_out, "ip && outport == %s",
> -                          od->localnet_port->json_key);
> -            ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_PRE_ACL, 110,
> -                                    ds_cstr(&match_in), "next;",
> -                                    &od->localnet_port->nbsp->header_);
> -            ovn_lflow_add_with_hint(lflows, od, S_SWITCH_OUT_PRE_ACL, 110,
> -                                    ds_cstr(&match_out), "next;",
> -                                    &od->localnet_port->nbsp->header_);
> +                ds_put_format(&match_in, "ip && inport == %s",
> +                              od->localnet_ports[i]->json_key);
> +                ds_put_format(&match_out, "ip && outport == %s",
> +                              od->localnet_ports[i]->json_key);
> +                ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_PRE_ACL, 110,
> +                                        ds_cstr(&match_in), "next;",
> +                                        &od->localnet_ports[i]->nbsp->header_);
> +                ovn_lflow_add_with_hint(lflows, od, S_SWITCH_OUT_PRE_ACL, 110,
> +                                        ds_cstr(&match_out), "next;",
> +                                        &od->localnet_ports[i]->nbsp->header_);
>  
> -            ds_destroy(&match_in);
> -            ds_destroy(&match_out);
> +                ds_destroy(&match_in);
> +                ds_destroy(&match_out);
> +            }
>          }
>  
>          /* Ingress and Egress Pre-ACL Table (Priority 110).
> @@ -5919,9 +5928,11 @@ 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) {
> -        ds_put_format(&actions, "clone { outport = %s; output; }; ",
> -                      od->localnet_port->json_key);
> +    if (od->localnet_ports) {

Same here.

> +        for (size_t i = 0; i < od->n_localnet_ports; i++) {
> +            ds_put_format(&actions, "clone { outport = %s; output; }; ",
> +                          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,
> @@ -6323,9 +6334,9 @@ 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->localnet_ports ||
>                              !op->nbsp->ha_chassis_group)) {

Would "!od->n_localnet_ports" make it more readable?

> -            /* 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;
>          }
> @@ -6339,84 +6350,93 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
>                  stage_hint = NULL;
>              }
>  
> -            for (size_t j = 0; j < op->lsp_addrs[i].n_ipv4_addrs; j++) {
> -                struct ds options_action = DS_EMPTY_INITIALIZER;
> -                struct ds response_action = DS_EMPTY_INITIALIZER;
> -                struct ds ipv4_addr_match = DS_EMPTY_INITIALIZER;
> -                if (build_dhcpv4_action(
> -                        op, op->lsp_addrs[i].ipv4_addrs[j].addr,
> -                        &options_action, &response_action, &ipv4_addr_match)) {
> -                    ds_clear(&match);
> -                    ds_put_format(
> -                        &match, "inport == %s && eth.src == %s && "
> -                        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
> -                        "udp.src == 68 && udp.dst == 67",
> -                        is_external ? op->od->localnet_port->json_key :
> -                            op->json_key,
> -                        op->lsp_addrs[i].ea_s);
> -
> -                    if (is_external) {
> -                        ds_put_format(&match, " && is_chassis_resident(%s)",
> -                                      op->json_key);
> -                    }
> +            size_t jmax = is_external? op->od->n_localnet_ports: 1;

Maybe we can find a better variable name instead of "jmax". Also,
wouldn't it be more readable to avoid so many nested for loops and add a
function to be called for each localnet/external port?

> +            for (size_t j = 0; j < jmax; j++) {
> +                for (size_t k = 0; k < op->lsp_addrs[i].n_ipv4_addrs; k++) {
> +                    struct ds options_action = DS_EMPTY_INITIALIZER;
> +                    struct ds response_action = DS_EMPTY_INITIALIZER;
> +                    struct ds ipv4_addr_match = DS_EMPTY_INITIALIZER;
> +                    if (build_dhcpv4_action(
> +                            op, op->lsp_addrs[i].ipv4_addrs[k].addr,
> +                            &options_action, &response_action,
> +                            &ipv4_addr_match)) {
> +                        ds_clear(&match);
> +                        ds_put_format(
> +                            &match,
> +                            "inport == %s && eth.src == %s && "
> +                            "ip4.src == 0.0.0.0 && "
> +                            "ip4.dst == 255.255.255.255 && "
> +                            "udp.src == 68 && udp.dst == 67",
> +                            is_external ? op->od->localnet_ports[j]->json_key :
> +                                op->json_key,
> +                            op->lsp_addrs[i].ea_s);
>  
> -                    ovn_lflow_add_with_hint(lflows, op->od,
> -                                            S_SWITCH_IN_DHCP_OPTIONS, 100,
> -                                            ds_cstr(&match),
> -                                            ds_cstr(&options_action),
> -                                            stage_hint);
> -                    ds_clear(&match);
> -                    /* Allow ip4.src = OFFER_IP and
> -                     * ip4.dst = {SERVER_IP, 255.255.255.255} for the below
> -                     * cases
> -                     *  -  When the client wants to renew the IP by sending
> -                     *     the DHCPREQUEST to the server ip.
> -                     *  -  When the client wants to renew the IP by
> -                     *     broadcasting the DHCPREQUEST.
> -                     */
> -                    ds_put_format(
> -                        &match, "inport == %s && eth.src == %s && "
> -                        "%s && udp.src == 68 && udp.dst == 67",
> -                        is_external ? op->od->localnet_port->json_key :
> -                            op->json_key,
> -                        op->lsp_addrs[i].ea_s, ds_cstr(&ipv4_addr_match));
> +                        if (is_external) {
> +                            ds_put_format(
> +                                &match, " && is_chassis_resident(%s)",
> +                                op->json_key);
> +                        }
>  
> -                    if (is_external) {
> -                        ds_put_format(&match, " && is_chassis_resident(%s)",
> -                                      op->json_key);
> -                    }
> +                        ovn_lflow_add_with_hint(lflows, op->od,
> +                                                S_SWITCH_IN_DHCP_OPTIONS, 100,
> +                                                ds_cstr(&match),
> +                                                ds_cstr(&options_action),
> +                                                stage_hint);
> +                        ds_clear(&match);
> +                        /* Allow ip4.src = OFFER_IP and
> +                         * ip4.dst = {SERVER_IP, 255.255.255.255} for the below
> +                         * cases
> +                         *  -  When the client wants to renew the IP by sending
> +                         *     the DHCPREQUEST to the server ip.
> +                         *  -  When the client wants to renew the IP by
> +                         *     broadcasting the DHCPREQUEST.
> +                         */
> +                        ds_put_format(
> +                            &match, "inport == %s && eth.src == %s && "
> +                            "%s && udp.src == 68 && udp.dst == 67",
> +                            is_external ? op->od->localnet_ports[j]->json_key :
> +                                op->json_key,
> +                            op->lsp_addrs[i].ea_s, ds_cstr(&ipv4_addr_match));
> +
> +                        if (is_external) {
> +                            ds_put_format(
> +                                &match, " && is_chassis_resident(%s)",
> +                                op->json_key);
> +                        }
>  
> -                    ovn_lflow_add_with_hint(lflows, op->od,
> -                                            S_SWITCH_IN_DHCP_OPTIONS, 100,
> -                                            ds_cstr(&match),
> -                                            ds_cstr(&options_action),
> -                                            stage_hint);
> -                    ds_clear(&match);
> +                        ovn_lflow_add_with_hint(lflows, op->od,
> +                                                S_SWITCH_IN_DHCP_OPTIONS, 100,
> +                                                ds_cstr(&match),
> +                                                ds_cstr(&options_action),
> +                                                stage_hint);
> +                        ds_clear(&match);
>  
> -                    /* If REGBIT_DHCP_OPTS_RESULT is set, it means the
> -                     * put_dhcp_opts action  is successful. */
> -                    ds_put_format(
> -                        &match, "inport == %s && eth.src == %s && "
> -                        "ip4 && udp.src == 68 && udp.dst == 67"
> -                        " && "REGBIT_DHCP_OPTS_RESULT,
> -                        is_external ? op->od->localnet_port->json_key :
> -                            op->json_key,
> -                        op->lsp_addrs[i].ea_s);
> -
> -                    if (is_external) {
> -                        ds_put_format(&match, " && is_chassis_resident(%s)",
> -                                      op->json_key);
> -                    }
> +                        /* If REGBIT_DHCP_OPTS_RESULT is set, it means the
> +                         * put_dhcp_opts action  is successful. */
> +                        ds_put_format(
> +                            &match, "inport == %s && eth.src == %s && "
> +                            "ip4 && udp.src == 68 && udp.dst == 67"
> +                            " && "REGBIT_DHCP_OPTS_RESULT,
> +                            is_external ? op->od->localnet_ports[j]->json_key :
> +                                op->json_key,
> +                            op->lsp_addrs[i].ea_s);
>  
> -                    ovn_lflow_add_with_hint(lflows, op->od,
> -                                            S_SWITCH_IN_DHCP_RESPONSE, 100,
> -                                            ds_cstr(&match),
> -                                            ds_cstr(&response_action),
> -                                            stage_hint);
> -                    ds_destroy(&options_action);
> -                    ds_destroy(&response_action);
> -                    ds_destroy(&ipv4_addr_match);
> -                    break;
> +                        if (is_external) {
> +                            ds_put_format(
> +                                &match, " && is_chassis_resident(%s)",
> +                                op->json_key);
> +                        }
> +
> +                        ovn_lflow_add_with_hint(lflows, op->od,
> +                                                S_SWITCH_IN_DHCP_RESPONSE, 100,
> +                                                ds_cstr(&match),
> +                                                ds_cstr(&response_action),
> +                                                stage_hint);
> +                        ds_destroy(&options_action);
> +                        ds_destroy(&response_action);
> +                        ds_destroy(&ipv4_addr_match);
> +                        break;
> +                    }
>                  }
>              }
>  
> @@ -6426,43 +6446,46 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
>                  stage_hint = NULL;
>              }
>  
> -            for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
> -                struct ds options_action = DS_EMPTY_INITIALIZER;
> -                struct ds response_action = DS_EMPTY_INITIALIZER;
> -                if (build_dhcpv6_action(
> -                        op, &op->lsp_addrs[i].ipv6_addrs[j].addr,
> -                        &options_action, &response_action)) {
> -                    ds_clear(&match);
> -                    ds_put_format(
> -                        &match, "inport == %s && eth.src == %s"
> -                        " && ip6.dst == ff02::1:2 && udp.src == 546 &&"
> -                        " udp.dst == 547",
> -                        is_external ? op->od->localnet_port->json_key :
> -                            op->json_key,
> -                        op->lsp_addrs[i].ea_s);
> -
> -                    if (is_external) {
> -                        ds_put_format(&match, " && is_chassis_resident(%s)",
> -                                      op->json_key);
> -                    }
> +            for (size_t j = 0; j < jmax; j++) {

Same here.

> +                for (size_t k = 0; k < op->lsp_addrs[i].n_ipv6_addrs; k++) {
> +                    struct ds options_action = DS_EMPTY_INITIALIZER;
> +                    struct ds response_action = DS_EMPTY_INITIALIZER;
> +                    if (build_dhcpv6_action(
> +                            op, &op->lsp_addrs[i].ipv6_addrs[k].addr,
> +                            &options_action, &response_action)) {
> +                        ds_clear(&match);
> +                        ds_put_format(
> +                            &match, "inport == %s && eth.src == %s"
> +                            " && ip6.dst == ff02::1:2 && udp.src == 546 &&"
> +                            " udp.dst == 547",
> +                            is_external ? op->od->localnet_ports[j]->json_key :
> +                                op->json_key,
> +                            op->lsp_addrs[i].ea_s);
>  
> -                    ovn_lflow_add_with_hint(lflows, op->od,
> -                                            S_SWITCH_IN_DHCP_OPTIONS, 100,
> -                                            ds_cstr(&match),
> -                                            ds_cstr(&options_action),
> -                                            stage_hint);
> +                        if (is_external) {
> +                            ds_put_format(
> +                                &match, " && is_chassis_resident(%s)",
> +                                op->json_key);
> +                        }
>  
> -                    /* If REGBIT_DHCP_OPTS_RESULT is set to 1, it means the
> -                     * put_dhcpv6_opts action is successful */
> -                    ds_put_cstr(&match, " && "REGBIT_DHCP_OPTS_RESULT);
> -                    ovn_lflow_add_with_hint(lflows, op->od,
> -                                            S_SWITCH_IN_DHCP_RESPONSE, 100,
> -                                            ds_cstr(&match),
> -                                            ds_cstr(&response_action),
> -                                            stage_hint);
> -                    ds_destroy(&options_action);
> -                    ds_destroy(&response_action);
> -                    break;
> +                        ovn_lflow_add_with_hint(lflows, op->od,
> +                                                S_SWITCH_IN_DHCP_OPTIONS, 100,
> +                                                ds_cstr(&match),
> +                                                ds_cstr(&options_action),
> +                                                stage_hint);
> +
> +                        /* If REGBIT_DHCP_OPTS_RESULT is set to 1, it means the
> +                         * put_dhcpv6_opts action is successful */
> +                        ds_put_cstr(&match, " && "REGBIT_DHCP_OPTS_RESULT);
> +                        ovn_lflow_add_with_hint(lflows, op->od,
> +                                                S_SWITCH_IN_DHCP_RESPONSE, 100,
> +                                                ds_cstr(&match),
> +                                                ds_cstr(&response_action),
> +                                                stage_hint);
> +                        ds_destroy(&options_action);
> +                        ds_destroy(&response_action);
> +                        break;
> +                    }
>                  }
>              }
>          }
> @@ -6521,7 +6544,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) {
> +            !op->od->localnet_ports) {
>             continue;
>          }
>  
> @@ -6536,36 +6559,42 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
>                  for (size_t k = 0; k < rp->n_lsp_addrs; k++) {
>                      for (size_t l = 0; l < rp->lsp_addrs[k].n_ipv4_addrs;
>                           l++) {
> -                        ds_clear(&match);
> -                        ds_put_format(
> -                            &match, "inport == %s && eth.src == %s"
> -                            " && !is_chassis_resident(%s)"
> -                            " && arp.tpa == %s && arp.op == 1",
> -                            op->od->localnet_port->json_key,
> -                            op->lsp_addrs[i].ea_s, op->json_key,
> -                            rp->lsp_addrs[k].ipv4_addrs[l].addr_s);
> -                        ovn_lflow_add_with_hint(lflows, op->od,
> -                                                S_SWITCH_IN_EXTERNAL_PORT,
> -                                                100, ds_cstr(&match), "drop;",
> -                                                &op->nbsp->header_);
> +                        for (size_t m = 0; m < op->od->n_localnet_ports; m++) {
> +                            ds_clear(&match);
> +                            ds_put_format(
> +                                &match, "inport == %s && eth.src == %s"
> +                                " && !is_chassis_resident(%s)"
> +                                " && arp.tpa == %s && arp.op == 1",
> +                                op->od->localnet_ports[m]->json_key,
> +                                op->lsp_addrs[i].ea_s, op->json_key,
> +                                rp->lsp_addrs[k].ipv4_addrs[l].addr_s);
> +                            ovn_lflow_add_with_hint(
> +                                lflows, op->od,
> +                                S_SWITCH_IN_EXTERNAL_PORT, 100,
> +                                ds_cstr(&match), "drop;",
> +                                &op->nbsp->header_);
> +                        }
>                      }
>                      for (size_t l = 0; l < rp->lsp_addrs[k].n_ipv6_addrs;
>                           l++) {
> -                        ds_clear(&match);
> -                        ds_put_format(
> -                            &match, "inport == %s && eth.src == %s"
> -                            " && !is_chassis_resident(%s)"
> -                            " && nd_ns && ip6.dst == {%s, %s} && "
> -                            "nd.target == %s",
> -                            op->od->localnet_port->json_key,
> -                            op->lsp_addrs[i].ea_s, op->json_key,
> -                            rp->lsp_addrs[k].ipv6_addrs[l].addr_s,
> -                            rp->lsp_addrs[k].ipv6_addrs[l].sn_addr_s,
> -                            rp->lsp_addrs[k].ipv6_addrs[l].addr_s);
> -                        ovn_lflow_add_with_hint(lflows, op->od,
> -                                                S_SWITCH_IN_EXTERNAL_PORT, 100,
> -                                                ds_cstr(&match), "drop;",
> -                                                &op->nbsp->header_);
> +                        for (size_t m = 0; m < op->od->n_localnet_ports; m++) {

I think that indices i, j, k, l, m make this quite hard to follow :)
Should we refactor this part a bit to make it more readable?

> +                            ds_clear(&match);
> +                            ds_put_format(
> +                                &match, "inport == %s && eth.src == %s"
> +                                " && !is_chassis_resident(%s)"
> +                                " && nd_ns && ip6.dst == {%s, %s} && "
> +                                "nd.target == %s",
> +                                op->od->localnet_ports[m]->json_key,
> +                                op->lsp_addrs[i].ea_s, op->json_key,
> +                                rp->lsp_addrs[k].ipv6_addrs[l].addr_s,
> +                                rp->lsp_addrs[k].ipv6_addrs[l].sn_addr_s,
> +                                rp->lsp_addrs[k].ipv6_addrs[l].addr_s);
> +                            ovn_lflow_add_with_hint(
> +                                lflows, op->od,
> +                                S_SWITCH_IN_EXTERNAL_PORT, 100,
> +                                ds_cstr(&match), "drop;",
> +                                &op->nbsp->header_);
> +                        }
>                      }
>                  }
>              }
> @@ -6787,7 +6816,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->localnet_ports) {
>                      bool add_chassis_resident_check = false;
>                      if (op->peer == op->peer->od->l3dgw_port) {
>                          /* The peer of this port represents a distributed
> @@ -8084,7 +8113,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->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 533ae716d..88edb6f32 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>
> @@ -1895,13 +1894,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
> @@ -1916,11 +1915,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.
> @@ -1935,13 +1934,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
> @@ -1957,7 +1956,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>
>  
> @@ -1967,7 +1966,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
> @@ -1978,12 +1977,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 541ec20c1..6f84c427c 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,13 @@
>  
>            <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 corresponding
> +            bridge mapping.  A logical switch can have multiple
> +            <code>localnet</code> ports attached, as long as each
> +            <code>ovn-controller</code> is plugged to a single local network
> +            only.  In this case, each hypervisor implements part of switch
> +            external network connectivity.
>            </dd>
>  
>            <dt><code>localport</code></dt>
> @@ -721,7 +724,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 3ae9d4f92..2d2d08027 100644
> --- a/ovn-sb.xml
> +++ b/ovn-sb.xml
> @@ -2606,10 +2606,13 @@ 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 some or all
> +            <code>ovn-controller</code> instances.  This is used
> +            to model direct connectivity to existing networks.  A logical
> +            switch can have multiple <code>localnet</code> ports attached, as
> +            long as each <code>ovn-controller</code> is plugged to a single
> +            local network only.  In this case, each hypervisor implements part
> +            of switch external network connectivity.
>            </dd>
>  
>            <dt><code>localport</code></dt>
> @@ -2754,10 +2757,12 @@ tcp.flags = RST;
>          <p>
>            When a logical switch has a <code>localnet</code> port attached,
>            every chassis that may have a local vif attached to that logical
> -          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.
> +          switch that needs this external connectivity must have a bridge
> +          mapping configured to reach that <code>localnet</code>.  If the
> +          mapping is missing, the vif won't be plugged to this network.  It may
> +          still reach the other network if routing is implemented by fabric.
> +          Traffic that arrives on a <code>localnet</code> port is never
> +          forwarded over a tunnel to another chassis.
>          </p>
>        </column>
>  
> diff --git a/tests/ovn.at b/tests/ovn.at
> index 013583826..42089763a 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -2438,6 +2438,118 @@ OVN_CLEANUP([hv1],[hv2])
>  
>  AT_CLEANUP
>  
> +AT_SETUP([ovn -- 2 HVs, multiple localnet ports])
> +ovn_start
> +
> +# In this test case we create a single switch connected to two physical
> +# networks via multiple localnet ports. Then we create two hypervisors, with 2
> +# ports on each. Each pair of adjecent ports belong to the same network segment
> +# and assume interconnectivity. There is no direct interconnectivity between
> +# ports located on chassis attached to different segments. (It is assumed that
> +# in real life external fabric L3 routing will deliver packets between the
> +# segments as needed.)
> +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
> +
> +for tag in 10 20; do
> +    net_add n-$tag
> +done
> +
> +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
> +}
> +
> +# 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
> +
> +# 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
> +
> +# 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
>
diff mbox series

Patch

diff --git a/controller/binding.c b/controller/binding.c
index 5ea12a8be..f4ae42806 100644
--- a/controller/binding.c
+++ b/controller/binding.c
@@ -680,12 +680,28 @@  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");
+    if (!network) {
+        return false;
+    }
+    return shash_find_data(bridge_mappings, network);
+}
+
 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..52255cc3a 100644
--- a/controller/patch.c
+++ b/controller/patch.c
@@ -198,9 +198,9 @@  add_bridge_mappings(struct ovsdb_idl_txn *ovs_idl_txn,
             continue;
         }
 
-        const char *patch_port_id;
+        bool is_localnet = false;
         if (!strcmp(binding->type, "localnet")) {
-            patch_port_id = "ovn-localnet-port";
+            is_localnet = true;
         } else if (!strcmp(binding->type, "l2gateway")) {
             if (!binding->chassis
                 || strcmp(chassis->name, binding->chassis->name)) {
@@ -208,7 +208,6 @@  add_bridge_mappings(struct ovsdb_idl_txn *ovs_idl_txn,
                  * so we should not create any patch ports for it. */
                 continue;
             }
-            patch_port_id = "ovn-l2gateway-port";
         } else {
             /* not a localnet or L2 gateway port. */
             continue;
@@ -224,12 +223,25 @@  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;
         }
 
+        const char *patch_port_id;
+        if (is_localnet) {
+            patch_port_id = "ovn-localnet-port";
+        } else {
+            patch_port_id = "ovn-l2gateway-port";
+        }
+
         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(ovs_idl_txn, patch_port_id, binding->logical_port,
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index 076278197..91730f7f3 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);
 
@@ -2053,7 +2056,11 @@  join_logical_ports(struct northd_context *ctx,
                 }
 
                 if (!strcmp(nbsp->type, "localnet")) {
-                   od->localnet_port = op;
+                   od->localnet_ports = xrealloc(
+                       od->localnet_ports,
+                       sizeof *od->localnet_ports * (od->n_localnet_ports + 1)
+                   );
+                   od->localnet_ports[od->n_localnet_ports++] = op;
                 }
 
                 op->lsp_addrs
@@ -2974,7 +2981,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->localnet_ports) {
                 add_router_port_garp = true;
             }
 
@@ -4662,23 +4669,25 @@  build_pre_acls(struct ovn_datapath *od, struct hmap *lflows)
             ds_destroy(&match_in);
             ds_destroy(&match_out);
         }
-        if (od->localnet_port) {
-            struct ds match_in = DS_EMPTY_INITIALIZER;
-            struct ds match_out = DS_EMPTY_INITIALIZER;
+        if (od->localnet_ports) {
+            for (size_t i = 0; i < od->n_localnet_ports; i++) {
+                struct ds match_in = DS_EMPTY_INITIALIZER;
+                struct ds match_out = DS_EMPTY_INITIALIZER;
 
-            ds_put_format(&match_in, "ip && inport == %s",
-                          od->localnet_port->json_key);
-            ds_put_format(&match_out, "ip && outport == %s",
-                          od->localnet_port->json_key);
-            ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_PRE_ACL, 110,
-                                    ds_cstr(&match_in), "next;",
-                                    &od->localnet_port->nbsp->header_);
-            ovn_lflow_add_with_hint(lflows, od, S_SWITCH_OUT_PRE_ACL, 110,
-                                    ds_cstr(&match_out), "next;",
-                                    &od->localnet_port->nbsp->header_);
+                ds_put_format(&match_in, "ip && inport == %s",
+                              od->localnet_ports[i]->json_key);
+                ds_put_format(&match_out, "ip && outport == %s",
+                              od->localnet_ports[i]->json_key);
+                ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_PRE_ACL, 110,
+                                        ds_cstr(&match_in), "next;",
+                                        &od->localnet_ports[i]->nbsp->header_);
+                ovn_lflow_add_with_hint(lflows, od, S_SWITCH_OUT_PRE_ACL, 110,
+                                        ds_cstr(&match_out), "next;",
+                                        &od->localnet_ports[i]->nbsp->header_);
 
-            ds_destroy(&match_in);
-            ds_destroy(&match_out);
+                ds_destroy(&match_in);
+                ds_destroy(&match_out);
+            }
         }
 
         /* Ingress and Egress Pre-ACL Table (Priority 110).
@@ -5919,9 +5928,11 @@  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) {
-        ds_put_format(&actions, "clone { outport = %s; output; }; ",
-                      od->localnet_port->json_key);
+    if (od->localnet_ports) {
+        for (size_t i = 0; i < od->n_localnet_ports; i++) {
+            ds_put_format(&actions, "clone { outport = %s; output; }; ",
+                          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,
@@ -6323,9 +6334,9 @@  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->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;
         }
@@ -6339,84 +6350,93 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                 stage_hint = NULL;
             }
 
-            for (size_t j = 0; j < op->lsp_addrs[i].n_ipv4_addrs; j++) {
-                struct ds options_action = DS_EMPTY_INITIALIZER;
-                struct ds response_action = DS_EMPTY_INITIALIZER;
-                struct ds ipv4_addr_match = DS_EMPTY_INITIALIZER;
-                if (build_dhcpv4_action(
-                        op, op->lsp_addrs[i].ipv4_addrs[j].addr,
-                        &options_action, &response_action, &ipv4_addr_match)) {
-                    ds_clear(&match);
-                    ds_put_format(
-                        &match, "inport == %s && eth.src == %s && "
-                        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
-                        "udp.src == 68 && udp.dst == 67",
-                        is_external ? op->od->localnet_port->json_key :
-                            op->json_key,
-                        op->lsp_addrs[i].ea_s);
-
-                    if (is_external) {
-                        ds_put_format(&match, " && is_chassis_resident(%s)",
-                                      op->json_key);
-                    }
+            size_t jmax = is_external? op->od->n_localnet_ports: 1;
+            for (size_t j = 0; j < jmax; j++) {
+                for (size_t k = 0; k < op->lsp_addrs[i].n_ipv4_addrs; k++) {
+                    struct ds options_action = DS_EMPTY_INITIALIZER;
+                    struct ds response_action = DS_EMPTY_INITIALIZER;
+                    struct ds ipv4_addr_match = DS_EMPTY_INITIALIZER;
+                    if (build_dhcpv4_action(
+                            op, op->lsp_addrs[i].ipv4_addrs[k].addr,
+                            &options_action, &response_action,
+                            &ipv4_addr_match)) {
+                        ds_clear(&match);
+                        ds_put_format(
+                            &match,
+                            "inport == %s && eth.src == %s && "
+                            "ip4.src == 0.0.0.0 && "
+                            "ip4.dst == 255.255.255.255 && "
+                            "udp.src == 68 && udp.dst == 67",
+                            is_external ? op->od->localnet_ports[j]->json_key :
+                                op->json_key,
+                            op->lsp_addrs[i].ea_s);
 
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_DHCP_OPTIONS, 100,
-                                            ds_cstr(&match),
-                                            ds_cstr(&options_action),
-                                            stage_hint);
-                    ds_clear(&match);
-                    /* Allow ip4.src = OFFER_IP and
-                     * ip4.dst = {SERVER_IP, 255.255.255.255} for the below
-                     * cases
-                     *  -  When the client wants to renew the IP by sending
-                     *     the DHCPREQUEST to the server ip.
-                     *  -  When the client wants to renew the IP by
-                     *     broadcasting the DHCPREQUEST.
-                     */
-                    ds_put_format(
-                        &match, "inport == %s && eth.src == %s && "
-                        "%s && udp.src == 68 && udp.dst == 67",
-                        is_external ? op->od->localnet_port->json_key :
-                            op->json_key,
-                        op->lsp_addrs[i].ea_s, ds_cstr(&ipv4_addr_match));
+                        if (is_external) {
+                            ds_put_format(
+                                &match, " && is_chassis_resident(%s)",
+                                op->json_key);
+                        }
 
-                    if (is_external) {
-                        ds_put_format(&match, " && is_chassis_resident(%s)",
-                                      op->json_key);
-                    }
+                        ovn_lflow_add_with_hint(lflows, op->od,
+                                                S_SWITCH_IN_DHCP_OPTIONS, 100,
+                                                ds_cstr(&match),
+                                                ds_cstr(&options_action),
+                                                stage_hint);
+                        ds_clear(&match);
+                        /* Allow ip4.src = OFFER_IP and
+                         * ip4.dst = {SERVER_IP, 255.255.255.255} for the below
+                         * cases
+                         *  -  When the client wants to renew the IP by sending
+                         *     the DHCPREQUEST to the server ip.
+                         *  -  When the client wants to renew the IP by
+                         *     broadcasting the DHCPREQUEST.
+                         */
+                        ds_put_format(
+                            &match, "inport == %s && eth.src == %s && "
+                            "%s && udp.src == 68 && udp.dst == 67",
+                            is_external ? op->od->localnet_ports[j]->json_key :
+                                op->json_key,
+                            op->lsp_addrs[i].ea_s, ds_cstr(&ipv4_addr_match));
+
+                        if (is_external) {
+                            ds_put_format(
+                                &match, " && is_chassis_resident(%s)",
+                                op->json_key);
+                        }
 
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_DHCP_OPTIONS, 100,
-                                            ds_cstr(&match),
-                                            ds_cstr(&options_action),
-                                            stage_hint);
-                    ds_clear(&match);
+                        ovn_lflow_add_with_hint(lflows, op->od,
+                                                S_SWITCH_IN_DHCP_OPTIONS, 100,
+                                                ds_cstr(&match),
+                                                ds_cstr(&options_action),
+                                                stage_hint);
+                        ds_clear(&match);
 
-                    /* If REGBIT_DHCP_OPTS_RESULT is set, it means the
-                     * put_dhcp_opts action  is successful. */
-                    ds_put_format(
-                        &match, "inport == %s && eth.src == %s && "
-                        "ip4 && udp.src == 68 && udp.dst == 67"
-                        " && "REGBIT_DHCP_OPTS_RESULT,
-                        is_external ? op->od->localnet_port->json_key :
-                            op->json_key,
-                        op->lsp_addrs[i].ea_s);
-
-                    if (is_external) {
-                        ds_put_format(&match, " && is_chassis_resident(%s)",
-                                      op->json_key);
-                    }
+                        /* If REGBIT_DHCP_OPTS_RESULT is set, it means the
+                         * put_dhcp_opts action  is successful. */
+                        ds_put_format(
+                            &match, "inport == %s && eth.src == %s && "
+                            "ip4 && udp.src == 68 && udp.dst == 67"
+                            " && "REGBIT_DHCP_OPTS_RESULT,
+                            is_external ? op->od->localnet_ports[j]->json_key :
+                                op->json_key,
+                            op->lsp_addrs[i].ea_s);
 
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_DHCP_RESPONSE, 100,
-                                            ds_cstr(&match),
-                                            ds_cstr(&response_action),
-                                            stage_hint);
-                    ds_destroy(&options_action);
-                    ds_destroy(&response_action);
-                    ds_destroy(&ipv4_addr_match);
-                    break;
+                        if (is_external) {
+                            ds_put_format(
+                                &match, " && is_chassis_resident(%s)",
+                                op->json_key);
+                        }
+
+                        ovn_lflow_add_with_hint(lflows, op->od,
+                                                S_SWITCH_IN_DHCP_RESPONSE, 100,
+                                                ds_cstr(&match),
+                                                ds_cstr(&response_action),
+                                                stage_hint);
+                        ds_destroy(&options_action);
+                        ds_destroy(&response_action);
+                        ds_destroy(&ipv4_addr_match);
+                        break;
+                    }
                 }
             }
 
@@ -6426,43 +6446,46 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                 stage_hint = NULL;
             }
 
-            for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
-                struct ds options_action = DS_EMPTY_INITIALIZER;
-                struct ds response_action = DS_EMPTY_INITIALIZER;
-                if (build_dhcpv6_action(
-                        op, &op->lsp_addrs[i].ipv6_addrs[j].addr,
-                        &options_action, &response_action)) {
-                    ds_clear(&match);
-                    ds_put_format(
-                        &match, "inport == %s && eth.src == %s"
-                        " && ip6.dst == ff02::1:2 && udp.src == 546 &&"
-                        " udp.dst == 547",
-                        is_external ? op->od->localnet_port->json_key :
-                            op->json_key,
-                        op->lsp_addrs[i].ea_s);
-
-                    if (is_external) {
-                        ds_put_format(&match, " && is_chassis_resident(%s)",
-                                      op->json_key);
-                    }
+            for (size_t j = 0; j < jmax; j++) {
+                for (size_t k = 0; k < op->lsp_addrs[i].n_ipv6_addrs; k++) {
+                    struct ds options_action = DS_EMPTY_INITIALIZER;
+                    struct ds response_action = DS_EMPTY_INITIALIZER;
+                    if (build_dhcpv6_action(
+                            op, &op->lsp_addrs[i].ipv6_addrs[k].addr,
+                            &options_action, &response_action)) {
+                        ds_clear(&match);
+                        ds_put_format(
+                            &match, "inport == %s && eth.src == %s"
+                            " && ip6.dst == ff02::1:2 && udp.src == 546 &&"
+                            " udp.dst == 547",
+                            is_external ? op->od->localnet_ports[j]->json_key :
+                                op->json_key,
+                            op->lsp_addrs[i].ea_s);
 
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_DHCP_OPTIONS, 100,
-                                            ds_cstr(&match),
-                                            ds_cstr(&options_action),
-                                            stage_hint);
+                        if (is_external) {
+                            ds_put_format(
+                                &match, " && is_chassis_resident(%s)",
+                                op->json_key);
+                        }
 
-                    /* If REGBIT_DHCP_OPTS_RESULT is set to 1, it means the
-                     * put_dhcpv6_opts action is successful */
-                    ds_put_cstr(&match, " && "REGBIT_DHCP_OPTS_RESULT);
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_DHCP_RESPONSE, 100,
-                                            ds_cstr(&match),
-                                            ds_cstr(&response_action),
-                                            stage_hint);
-                    ds_destroy(&options_action);
-                    ds_destroy(&response_action);
-                    break;
+                        ovn_lflow_add_with_hint(lflows, op->od,
+                                                S_SWITCH_IN_DHCP_OPTIONS, 100,
+                                                ds_cstr(&match),
+                                                ds_cstr(&options_action),
+                                                stage_hint);
+
+                        /* If REGBIT_DHCP_OPTS_RESULT is set to 1, it means the
+                         * put_dhcpv6_opts action is successful */
+                        ds_put_cstr(&match, " && "REGBIT_DHCP_OPTS_RESULT);
+                        ovn_lflow_add_with_hint(lflows, op->od,
+                                                S_SWITCH_IN_DHCP_RESPONSE, 100,
+                                                ds_cstr(&match),
+                                                ds_cstr(&response_action),
+                                                stage_hint);
+                        ds_destroy(&options_action);
+                        ds_destroy(&response_action);
+                        break;
+                    }
                 }
             }
         }
@@ -6521,7 +6544,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) {
+            !op->od->localnet_ports) {
            continue;
         }
 
@@ -6536,36 +6559,42 @@  build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                 for (size_t k = 0; k < rp->n_lsp_addrs; k++) {
                     for (size_t l = 0; l < rp->lsp_addrs[k].n_ipv4_addrs;
                          l++) {
-                        ds_clear(&match);
-                        ds_put_format(
-                            &match, "inport == %s && eth.src == %s"
-                            " && !is_chassis_resident(%s)"
-                            " && arp.tpa == %s && arp.op == 1",
-                            op->od->localnet_port->json_key,
-                            op->lsp_addrs[i].ea_s, op->json_key,
-                            rp->lsp_addrs[k].ipv4_addrs[l].addr_s);
-                        ovn_lflow_add_with_hint(lflows, op->od,
-                                                S_SWITCH_IN_EXTERNAL_PORT,
-                                                100, ds_cstr(&match), "drop;",
-                                                &op->nbsp->header_);
+                        for (size_t m = 0; m < op->od->n_localnet_ports; m++) {
+                            ds_clear(&match);
+                            ds_put_format(
+                                &match, "inport == %s && eth.src == %s"
+                                " && !is_chassis_resident(%s)"
+                                " && arp.tpa == %s && arp.op == 1",
+                                op->od->localnet_ports[m]->json_key,
+                                op->lsp_addrs[i].ea_s, op->json_key,
+                                rp->lsp_addrs[k].ipv4_addrs[l].addr_s);
+                            ovn_lflow_add_with_hint(
+                                lflows, op->od,
+                                S_SWITCH_IN_EXTERNAL_PORT, 100,
+                                ds_cstr(&match), "drop;",
+                                &op->nbsp->header_);
+                        }
                     }
                     for (size_t l = 0; l < rp->lsp_addrs[k].n_ipv6_addrs;
                          l++) {
-                        ds_clear(&match);
-                        ds_put_format(
-                            &match, "inport == %s && eth.src == %s"
-                            " && !is_chassis_resident(%s)"
-                            " && nd_ns && ip6.dst == {%s, %s} && "
-                            "nd.target == %s",
-                            op->od->localnet_port->json_key,
-                            op->lsp_addrs[i].ea_s, op->json_key,
-                            rp->lsp_addrs[k].ipv6_addrs[l].addr_s,
-                            rp->lsp_addrs[k].ipv6_addrs[l].sn_addr_s,
-                            rp->lsp_addrs[k].ipv6_addrs[l].addr_s);
-                        ovn_lflow_add_with_hint(lflows, op->od,
-                                                S_SWITCH_IN_EXTERNAL_PORT, 100,
-                                                ds_cstr(&match), "drop;",
-                                                &op->nbsp->header_);
+                        for (size_t m = 0; m < op->od->n_localnet_ports; m++) {
+                            ds_clear(&match);
+                            ds_put_format(
+                                &match, "inport == %s && eth.src == %s"
+                                " && !is_chassis_resident(%s)"
+                                " && nd_ns && ip6.dst == {%s, %s} && "
+                                "nd.target == %s",
+                                op->od->localnet_ports[m]->json_key,
+                                op->lsp_addrs[i].ea_s, op->json_key,
+                                rp->lsp_addrs[k].ipv6_addrs[l].addr_s,
+                                rp->lsp_addrs[k].ipv6_addrs[l].sn_addr_s,
+                                rp->lsp_addrs[k].ipv6_addrs[l].addr_s);
+                            ovn_lflow_add_with_hint(
+                                lflows, op->od,
+                                S_SWITCH_IN_EXTERNAL_PORT, 100,
+                                ds_cstr(&match), "drop;",
+                                &op->nbsp->header_);
+                        }
                     }
                 }
             }
@@ -6787,7 +6816,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->localnet_ports) {
                     bool add_chassis_resident_check = false;
                     if (op->peer == op->peer->od->l3dgw_port) {
                         /* The peer of this port represents a distributed
@@ -8084,7 +8113,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->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 533ae716d..88edb6f32 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>
@@ -1895,13 +1894,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
@@ -1916,11 +1915,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.
@@ -1935,13 +1934,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
@@ -1957,7 +1956,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>
 
@@ -1967,7 +1966,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
@@ -1978,12 +1977,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 541ec20c1..6f84c427c 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,13 @@ 
 
           <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 corresponding
+            bridge mapping.  A logical switch can have multiple
+            <code>localnet</code> ports attached, as long as each
+            <code>ovn-controller</code> is plugged to a single local network
+            only.  In this case, each hypervisor implements part of switch
+            external network connectivity.
           </dd>
 
           <dt><code>localport</code></dt>
@@ -721,7 +724,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 3ae9d4f92..2d2d08027 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -2606,10 +2606,13 @@  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 some or all
+            <code>ovn-controller</code> instances.  This is used
+            to model direct connectivity to existing networks.  A logical
+            switch can have multiple <code>localnet</code> ports attached, as
+            long as each <code>ovn-controller</code> is plugged to a single
+            local network only.  In this case, each hypervisor implements part
+            of switch external network connectivity.
           </dd>
 
           <dt><code>localport</code></dt>
@@ -2754,10 +2757,12 @@  tcp.flags = RST;
         <p>
           When a logical switch has a <code>localnet</code> port attached,
           every chassis that may have a local vif attached to that logical
-          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.
+          switch that needs this external connectivity must have a bridge
+          mapping configured to reach that <code>localnet</code>.  If the
+          mapping is missing, the vif won't be plugged to this network.  It may
+          still reach the other network if routing is implemented by fabric.
+          Traffic that arrives on a <code>localnet</code> port is never
+          forwarded over a tunnel to another chassis.
         </p>
       </column>
 
diff --git a/tests/ovn.at b/tests/ovn.at
index 013583826..42089763a 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -2438,6 +2438,118 @@  OVN_CLEANUP([hv1],[hv2])
 
 AT_CLEANUP
 
+AT_SETUP([ovn -- 2 HVs, multiple localnet ports])
+ovn_start
+
+# In this test case we create a single switch connected to two physical
+# networks via multiple localnet ports. Then we create two hypervisors, with 2
+# ports on each. Each pair of adjecent ports belong to the same network segment
+# and assume interconnectivity. There is no direct interconnectivity between
+# ports located on chassis attached to different segments. (It is assumed that
+# in real life external fabric L3 routing will deliver packets between the
+# segments as needed.)
+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
+
+for tag in 10 20; do
+    net_add n-$tag
+done
+
+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
+}
+
+# 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
+
+# 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
+
+# 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