diff mbox series

[ovs-dev,v5,5/5] northd: Refactor port binding pairing.

Message ID 20250512205815.870519-6-mmichels@redhat.com
State Changes Requested
Delegated to: Ales Musil
Headers show
Series Datapath and Port Sync Refactor | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_ovn-kubernetes success github build: passed
ovsrobot/github-robot-_Build_and_Test fail github build: failed

Commit Message

Mark Michelson May 12, 2025, 8:58 p.m. UTC
This is similar to a recent change that refactored datapath syncing.
This works in a very similar way.

* Input nodes create type-agnostic ovn_unpaired_port_bindings. These are
  fed into the en-port-binding-pair node.
* The en-port-binding-pair node ensures that a southbound Port_Binding
  is created for each unpaired port binding. Any remaining soutbound
  Port_Bindings are deleted.
* Type-specific nodes then convert the paired port bindings into
  type-specific paired port bindings that can be consumed by other
  nodes.

However, there are some important differences to note between this and
the datapath sync patch:

* This patch opts for the word "pair" instead of "sync". This is because
  en-port-binding-pair ensures that all southbound Port_Bindings are
  paired with some northbound structure. However, the columns in the
  southobund Port_Binding are not all synced with their northd
  counterpart. Other engine nodes are responsible for fully syncing the
  data.
* Not all southbound Port_Bindings have a corresponding northbound row.
  Therefore, the engine nodes that create unpaired port bindings pass an
  opaque cookie pointer to the pairing node instead of a database row.
* Port bindings tend to be identified by name. This means the code has
  to have several safeguards in place to catch scenarios such as a port
  "moving" from one datapath to another, or a port being deleted and
  re-added quickly.
* Unlike with the datapath syncing code, this required changes to
  northd's incremental processing since northd was already incrementally
  processing some northbound logical switch port changes.

Signed-off-by: Mark Michelson <mmichels@redhat.com>
---
v4 -> v5:
 * Rebased.
 * Fixed some formatting anomalies in mirror port syncing.
 * Fixed checkpatch warnings.

v3 -> v4:
 * Rebased.
 * Addressed newly-added mirror port functionality.
 * Fixed many memory leaks.

v3: This is the first version with this patch present.
---
 TODO.rst                                     |  12 +
 northd/automake.mk                           |  14 +-
 northd/en-global-config.c                    |   3 +
 northd/en-global-config.h                    |   1 +
 northd/en-northd.c                           |  49 +-
 northd/en-northd.h                           |   2 -
 northd/en-port-binding-chassisredirect.c     | 319 ++++++++
 northd/en-port-binding-chassisredirect.h     |  53 ++
 northd/en-port-binding-logical-router-port.c | 178 +++++
 northd/en-port-binding-logical-router-port.h |  47 ++
 northd/en-port-binding-logical-switch-port.c | 231 ++++++
 northd/en-port-binding-logical-switch-port.h |  48 ++
 northd/en-port-binding-mirror.c              | 191 +++++
 northd/en-port-binding-mirror.h              |  48 ++
 northd/en-port-binding-pair.c                | 467 ++++++++++++
 northd/en-port-binding-pair.h                |  34 +
 northd/inc-proc-northd.c                     |  54 +-
 northd/northd.c                              | 746 +++++--------------
 northd/northd.h                              |  17 +-
 northd/port_binding_pair.c                   |  81 ++
 northd/port_binding_pair.h                   | 117 +++
 21 files changed, 2124 insertions(+), 588 deletions(-)
 create mode 100644 northd/en-port-binding-chassisredirect.c
 create mode 100644 northd/en-port-binding-chassisredirect.h
 create mode 100644 northd/en-port-binding-logical-router-port.c
 create mode 100644 northd/en-port-binding-logical-router-port.h
 create mode 100644 northd/en-port-binding-logical-switch-port.c
 create mode 100644 northd/en-port-binding-logical-switch-port.h
 create mode 100644 northd/en-port-binding-mirror.c
 create mode 100644 northd/en-port-binding-mirror.h
 create mode 100644 northd/en-port-binding-pair.c
 create mode 100644 northd/en-port-binding-pair.h
 create mode 100644 northd/port_binding_pair.c
 create mode 100644 northd/port_binding_pair.h

Comments

Ales Musil May 26, 2025, 8:42 a.m. UTC | #1
On Mon, May 12, 2025 at 10:59 PM Mark Michelson via dev <
ovs-dev@openvswitch.org> wrote:

> This is similar to a recent change that refactored datapath syncing.
> This works in a very similar way.
>
> * Input nodes create type-agnostic ovn_unpaired_port_bindings. These are
>   fed into the en-port-binding-pair node.
> * The en-port-binding-pair node ensures that a southbound Port_Binding
>   is created for each unpaired port binding. Any remaining soutbound
>   Port_Bindings are deleted.
> * Type-specific nodes then convert the paired port bindings into
>   type-specific paired port bindings that can be consumed by other
>   nodes.
>
> However, there are some important differences to note between this and
> the datapath sync patch:
>
> * This patch opts for the word "pair" instead of "sync". This is because
>   en-port-binding-pair ensures that all southbound Port_Bindings are
>   paired with some northbound structure. However, the columns in the
>   southobund Port_Binding are not all synced with their northd
>   counterpart. Other engine nodes are responsible for fully syncing the
>   data.
> * Not all southbound Port_Bindings have a corresponding northbound row.
>   Therefore, the engine nodes that create unpaired port bindings pass an
>   opaque cookie pointer to the pairing node instead of a database row.
> * Port bindings tend to be identified by name. This means the code has
>   to have several safeguards in place to catch scenarios such as a port
>   "moving" from one datapath to another, or a port being deleted and
>   re-added quickly.
> * Unlike with the datapath syncing code, this required changes to
>   northd's incremental processing since northd was already incrementally
>   processing some northbound logical switch port changes.
>
> Signed-off-by: Mark Michelson <mmichels@redhat.com>
> ---
>

Hi Mark,
thank you for this. I like the general approach, there are still
some comments/suggestions down below.

The xzalloc comments from 3/5 apply here too.


> v4 -> v5:
>  * Rebased.
>  * Fixed some formatting anomalies in mirror port syncing.
>  * Fixed checkpatch warnings.
>
> v3 -> v4:
>  * Rebased.
>  * Addressed newly-added mirror port functionality.
>  * Fixed many memory leaks.
>
> v3: This is the first version with this patch present.
> ---
>  TODO.rst                                     |  12 +
>  northd/automake.mk                           |  14 +-
>  northd/en-global-config.c                    |   3 +
>  northd/en-global-config.h                    |   1 +
>  northd/en-northd.c                           |  49 +-
>  northd/en-northd.h                           |   2 -
>  northd/en-port-binding-chassisredirect.c     | 319 ++++++++
>  northd/en-port-binding-chassisredirect.h     |  53 ++
>  northd/en-port-binding-logical-router-port.c | 178 +++++
>  northd/en-port-binding-logical-router-port.h |  47 ++
>  northd/en-port-binding-logical-switch-port.c | 231 ++++++
>  northd/en-port-binding-logical-switch-port.h |  48 ++
>  northd/en-port-binding-mirror.c              | 191 +++++
>  northd/en-port-binding-mirror.h              |  48 ++
>  northd/en-port-binding-pair.c                | 467 ++++++++++++
>  northd/en-port-binding-pair.h                |  34 +
>  northd/inc-proc-northd.c                     |  54 +-
>  northd/northd.c                              | 746 +++++--------------
>  northd/northd.h                              |  17 +-
>  northd/port_binding_pair.c                   |  81 ++
>  northd/port_binding_pair.h                   | 117 +++
>  21 files changed, 2124 insertions(+), 588 deletions(-)
>  create mode 100644 northd/en-port-binding-chassisredirect.c
>  create mode 100644 northd/en-port-binding-chassisredirect.h
>  create mode 100644 northd/en-port-binding-logical-router-port.c
>  create mode 100644 northd/en-port-binding-logical-router-port.h
>  create mode 100644 northd/en-port-binding-logical-switch-port.c
>  create mode 100644 northd/en-port-binding-logical-switch-port.h
>  create mode 100644 northd/en-port-binding-mirror.c
>  create mode 100644 northd/en-port-binding-mirror.h
>  create mode 100644 northd/en-port-binding-pair.c
>  create mode 100644 northd/en-port-binding-pair.h
>  create mode 100644 northd/port_binding_pair.c
>  create mode 100644 northd/port_binding_pair.h
>
> diff --git a/TODO.rst b/TODO.rst
> index 78962bb92..60ae155c5 100644
> --- a/TODO.rst
> +++ b/TODO.rst
> @@ -168,3 +168,15 @@ OVN To-do List
>      ovn\_synced\_logical_router and ovn\_synced\_logical\_switch. This
> will
>      allow for the eventual removal of the ovn\_datapath structure from the
>      codebase.
> +
> +* Port Binding sync nodes
> +
> +  * Southbound Port bindings are synced across three engine nodes:
> +    - en_port_binding_pair
> +    - en_northd
> +    - en_sync_to_sb
> +    It would be easier to work with if these were combined into a
> +    single node instead.
> +
> +  * Add incremental processing to the en-port-binding-pair node, as
> +    well as derivative nodes.
> diff --git a/northd/automake.mk b/northd/automake.mk
> index bf9978dd2..f475e0cd9 100644
> --- a/northd/automake.mk
> +++ b/northd/automake.mk
> @@ -54,6 +54,16 @@ northd_ovn_northd_SOURCES = \
>         northd/en-learned-route-sync.h \
>         northd/en-group-ecmp-route.c \
>         northd/en-group-ecmp-route.h \
> +       northd/en-port-binding-logical-router-port.c \
> +       northd/en-port-binding-logical-router-port.h \
> +       northd/en-port-binding-logical-switch-port.c \
> +       northd/en-port-binding-logical-switch-port.h \
> +       northd/en-port-binding-chassisredirect.c \
> +       northd/en-port-binding-chassisredirect.h \
> +       northd/en-port-binding-mirror.c \
> +       northd/en-port-binding-mirror.h \
> +       northd/en-port-binding-pair.c \
> +       northd/en-port-binding-pair.h \
>         northd/inc-proc-northd.c \
>         northd/inc-proc-northd.h \
>         northd/ipam.c \
> @@ -61,7 +71,9 @@ northd_ovn_northd_SOURCES = \
>         northd/lflow-mgr.c \
>         northd/lflow-mgr.h \
>         northd/lb.c \
> -       northd/lb.h
> +       northd/lb.h \
> +       northd/port_binding_pair.c \
> +       northd/port_binding_pair.h
>  northd_ovn_northd_LDADD = \
>         lib/libovn.la \
>         $(OVSDB_LIBDIR)/libovsdb.la \
> diff --git a/northd/en-global-config.c b/northd/en-global-config.c
> index 7204462ee..3a4bdbf87 100644
> --- a/northd/en-global-config.c
> +++ b/northd/en-global-config.c
> @@ -148,6 +148,9 @@ en_global_config_run(struct engine_node *node , void
> *data)
>      config_data->max_dp_tunnel_id =
>          get_ovn_max_dp_key_local(config_data->vxlan_mode, ic_vxlan_mode);
>
> +    uint8_t pb_tunnel_bits = config_data->vxlan_mode ? 12 : 16;
> +    config_data->max_pb_tunnel_id = (1u << (pb_tunnel_bits - 1)) - 1;
> +
>      char *max_tunid = xasprintf("%d", config_data->max_dp_tunnel_id);
>      smap_replace(options, "max_tunid", max_tunid);
>      free(max_tunid);
> diff --git a/northd/en-global-config.h b/northd/en-global-config.h
> index 55a1e420b..dbb06151c 100644
> --- a/northd/en-global-config.h
> +++ b/northd/en-global-config.h
> @@ -51,6 +51,7 @@ struct ed_type_global_config {
>
>      bool vxlan_mode;
>      uint32_t max_dp_tunnel_id;
> +    uint32_t max_pb_tunnel_id;
>
>      bool tracked;
>      struct global_config_tracked_data tracked_data;
> diff --git a/northd/en-northd.c b/northd/en-northd.c
> index c4573f88f..15840e361 100644
> --- a/northd/en-northd.c
> +++ b/northd/en-northd.c
> @@ -123,6 +123,19 @@ northd_get_input_data(struct engine_node *node,
>
>      input_data->synced_lrs =
>          engine_get_input_data("datapath_synced_logical_router", node);
> +
> +    input_data->paired_lsps =
> +        engine_get_input_data("port_binding_paired_logical_switch_port",
> node);
> +
> +    input_data->paired_lrps =
> +        engine_get_input_data("port_binding_paired_logical_router_port",
> node);
> +
> +    input_data->paired_crps =
> +        engine_get_input_data("port_binding_paired_chassisredirect_port",
> +                              node);
> +
> +    input_data->paired_mirrors =
> +        engine_get_input_data("port_binding_paired_mirror", node);
>  }
>
>  enum engine_node_state
> @@ -477,42 +490,6 @@ en_northd_clear_tracked_data(void *data_)
>      destroy_northd_data_tracked_changes(data);
>  }
>
> -enum engine_input_handler_result
> -northd_sb_fdb_change_handler(struct engine_node *node, void *data)
> -{
> -    struct northd_data *nd = data;
> -    const struct sbrec_fdb_table *sbrec_fdb_table =
> -        EN_OVSDB_GET(engine_get_input("SB_fdb", node));
> -
> -    /* check if changed rows are stale and delete them */
> -    const struct sbrec_fdb *fdb_e, *fdb_prev_del = NULL;
> -    SBREC_FDB_TABLE_FOR_EACH_TRACKED (fdb_e, sbrec_fdb_table) {
> -        if (sbrec_fdb_is_deleted(fdb_e)) {
> -            continue;
> -        }
> -
> -        if (fdb_prev_del) {
> -            sbrec_fdb_delete(fdb_prev_del);
> -        }
> -
> -        fdb_prev_del = fdb_e;
> -        struct ovn_datapath *od
> -            = ovn_datapath_find_by_key(&nd->ls_datapaths.datapaths,
> -                                       fdb_e->dp_key);
> -        if (od) {
> -            if (ovn_tnlid_present(&od->port_tnlids, fdb_e->port_key)) {
> -                fdb_prev_del = NULL;
> -            }
> -        }
> -    }
> -
> -    if (fdb_prev_del) {
> -        sbrec_fdb_delete(fdb_prev_del);
> -    }
> -
> -    return EN_HANDLED_UNCHANGED;
> -}
> -
>  void
>  en_route_policies_cleanup(void *data)
>  {
> diff --git a/northd/en-northd.h b/northd/en-northd.h
> index b19b73270..58a524c5c 100644
> --- a/northd/en-northd.h
> +++ b/northd/en-northd.h
> @@ -25,8 +25,6 @@ enum engine_input_handler_result
>  northd_sb_port_binding_handler(struct engine_node *, void *data);
>  enum engine_input_handler_result northd_lb_data_handler(struct
> engine_node *,
>                                                          void *data);
> -enum engine_input_handler_result
> -northd_sb_fdb_change_handler(struct engine_node *node, void *data);
>  void *en_routes_init(struct engine_node *node OVS_UNUSED,
>                              struct engine_arg *arg OVS_UNUSED);
>  void en_route_policies_cleanup(void *data);
> diff --git a/northd/en-port-binding-chassisredirect.c
> b/northd/en-port-binding-chassisredirect.c
> new file mode 100644
> index 000000000..5b2e7a9e4
> --- /dev/null
> +++ b/northd/en-port-binding-chassisredirect.c
> @@ -0,0 +1,319 @@
> +/*
> + * Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +#include <config.h>
> +
> +#include "inc-proc-eng.h"
> +#include "ovn-nb-idl.h"
> +#include "en-datapath-logical-router.h"
> +#include "en-datapath-logical-switch.h"
> +#include "en-port-binding-chassisredirect.h"
> +#include "port_binding_pair.h"
> +#include "ovn-util.h"
> +
> +#include "openvswitch/vlog.h"
> +
> +#include "hmapx.h"
> +
> +#define CR_SWITCH_PORT_TYPE "chassisredirect-logical-switch-port"
> +#define CR_ROUTER_PORT_TYPE "chassisredirect-logical-router-port"
> +
> +VLOG_DEFINE_THIS_MODULE(en_port_binding_chassisredirect_port);
> +
> +struct router_dgps {
> +    const struct ovn_synced_logical_router *lr;
> +    const struct nbrec_logical_router_port **dgps;
> +    size_t n_dgps;
> +};
> +
> +struct switch_localnets {
> +    const struct ovn_synced_logical_switch *ls;
> +    size_t n_localnet_ports;
> +};
> +
> +struct port_router_dgps {
> +    const struct nbrec_logical_router_port *nbrp;
> +    struct router_dgps *r;
> +};
> +
> +static struct port_router_dgps *
> +port_router_dgps_alloc(const struct nbrec_logical_router_port *nbrp,
> +                       struct router_dgps *r)
> +{
> +    struct port_router_dgps *p_dgps = xmalloc(sizeof *p_dgps);
> +    p_dgps->nbrp = nbrp;
> +    p_dgps->r = r;
> +
> +    return p_dgps;
> +}
> +
> +void *
> +en_port_binding_chassisredirect_port_init(struct engine_node *node
> OVS_UNUSED,
> +                                         struct engine_arg *args
> OVS_UNUSED)
> +{
> +    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
> +    ovn_unpaired_port_binding_map_init(map, NULL);
> +    return map;
> +}
> +
> +struct chassisredirect_router_port {
> +    char *name;
> +    const struct nbrec_logical_router_port *nbrp;
> +};
> +
> +static struct chassisredirect_router_port *
> +chassisredirect_router_port_alloc(const struct nbrec_logical_router_port
> *nbrp)
> +{
> +    struct chassisredirect_router_port *crp = xmalloc(sizeof *crp);
> +    crp->name = ovn_chassis_redirect_name(nbrp->name);
> +    crp->nbrp = nbrp;
> +
> +    return crp;
> +}
> +
> +static void
> +chassisredirect_router_port_free(struct chassisredirect_router_port *crp)
> +{
> +    free(crp->name);
> +    free(crp);
> +}
> +
> +struct chassisredirect_switch_port {
> +    char *name;
> +    const struct nbrec_logical_switch_port *nbsp;
> +};
> +
> +static struct chassisredirect_switch_port *
> +chassisredirect_switch_port_alloc(const struct nbrec_logical_switch_port
> *nbsp)
> +{
> +    struct chassisredirect_switch_port *crp = xmalloc(sizeof *crp);
> +    crp->name = ovn_chassis_redirect_name(nbsp->name);
> +    crp->nbsp = nbsp;
> +
> +    return crp;
> +}
> +
> +static void
> +chassisredirect_switch_port_free(struct chassisredirect_switch_port *csp)
> +{
> +    free(csp->name);
> +    free(csp);
> +}
> +
> +static void
> +chassisredirect_port_binding_map_destroy(
> +    struct ovn_unpaired_port_binding_map *map)
> +{
> +    struct shash_node *node;
> +    SHASH_FOR_EACH (node, &map->ports) {
> +        struct ovn_unpaired_port_binding *upb = node->data;
> +        if (!strcmp(upb->type, CR_ROUTER_PORT_TYPE)) {
> +            chassisredirect_router_port_free(upb->cookie);
> +        } else {
> +            chassisredirect_switch_port_free(upb->cookie);
> +        }
> +    }
> +    ovn_unpaired_port_binding_map_destroy(map);
> +}
> +
> +enum engine_node_state
> +en_port_binding_chassisredirect_port_run(struct engine_node *node, void
> *data)
> +{
> +    const struct ovn_synced_logical_router_map *lr_map =
> +        engine_get_input_data("datapath_synced_logical_router", node);
> +    const struct ovn_synced_logical_switch_map *ls_map =
> +        engine_get_input_data("datapath_synced_logical_switch", node);
> +
> +    struct ovn_unpaired_port_binding_map *map = data;
> +
> +    chassisredirect_port_binding_map_destroy(map);
> +    ovn_unpaired_port_binding_map_init(map, NULL);
> +
> +    struct shash ports = SHASH_INITIALIZER(&ports);
> +    const struct ovn_synced_logical_router *lr;
> +    struct hmapx all_rdgps = HMAPX_INITIALIZER(&all_rdgps);
> +    HMAP_FOR_EACH (lr, hmap_node, &lr_map->synced_routers) {
> +        if (smap_get(&lr->nb->options, "chassis")) {
> +            /* If the logical router has the chassis option set,
> +             * then we ignore any ports that have gateway_chassis
> +             * or ha_chassis_group options set.
> +             */
> +            continue;
> +        }
> +        struct router_dgps *rdgps = xzalloc(sizeof *rdgps);
> +        rdgps->lr = lr;
> +        rdgps->dgps = xzalloc(sizeof(*rdgps->dgps) * lr->nb->n_ports);
> +        hmapx_add(&all_rdgps, rdgps);
> +        const struct nbrec_logical_router_port *nbrp;
> +        for (size_t i = 0; i < lr->nb->n_ports; i++) {
> +            nbrp = lr->nb->ports[i];
> +            if (nbrp->ha_chassis_group || nbrp->n_gateway_chassis) {
> +                rdgps->dgps[rdgps->n_dgps++] = nbrp;
> +                shash_add(&ports, nbrp->name,
> +                          port_router_dgps_alloc(nbrp, rdgps));
> +            }
> +        }
> +    }
> +
> +    struct hmapx all_localnets = HMAPX_INITIALIZER(&all_localnets);
> +    const struct ovn_synced_logical_switch *ls;
> +    HMAP_FOR_EACH (ls, hmap_node, &ls_map->synced_switches) {
> +        struct switch_localnets *localnets = xzalloc(sizeof *localnets);
> +        localnets->ls = ls;
> +        hmapx_add(&all_localnets, localnets);
> +        for (size_t i = 0; i < ls->nb->n_ports; i++) {
> +            const struct nbrec_logical_switch_port *nbsp =
> ls->nb->ports[i];
> +            if (!strcmp(nbsp->type, "localnet")) {
> +                localnets->n_localnet_ports++;
> +            }
> +        }
> +    }
> +
> +    /* All logical router DGPs need corresponding chassisredirect ports
> +     * made
> +     */
> +    struct hmapx_node *hmapx_node;
> +    HMAPX_FOR_EACH (hmapx_node, &all_rdgps) {
> +        struct router_dgps *rdgps = hmapx_node->data;
> +        struct ovn_unpaired_port_binding *upb;
> +        for (size_t i = 0; i < rdgps->n_dgps; i++) {
> +            const struct nbrec_logical_router_port *nbrp = rdgps->dgps[i];
> +            struct chassisredirect_router_port *crp =
> +                chassisredirect_router_port_alloc(nbrp);
> +            upb = ovn_unpaired_port_binding_alloc(0, crp->name,
> +                                                  CR_ROUTER_PORT_TYPE,
> +                                                  crp, rdgps->lr->sb);
> +            shash_add(&map->ports, crp->name, upb);
> +        }
> +    }
> +
> +    /* Logical switch ports that are peered with DGPs need chassisredirect
> +     * ports created if
> +     * 1. The DGP it is paired with is the only one on its router, and
> +     * 2. There are no localnet ports on the switch
> +     */
> +    HMAPX_FOR_EACH (hmapx_node, &all_localnets) {
> +        struct switch_localnets *localnets = hmapx_node->data;
> +        if (localnets->n_localnet_ports > 0) {
> +            continue;
> +        }
> +        for (size_t i = 0; i < localnets->ls->nb->n_ports; i++) {
> +            const struct nbrec_logical_switch_port *nbsp =
> +                localnets->ls->nb->ports[i];
> +            if (strcmp(nbsp->type, "router")) {
> +                continue;
> +            }
> +            const char *peer_name = smap_get(&nbsp->options,
> "router-port");
> +            if (!peer_name) {
> +                continue;
> +            }
> +            struct port_router_dgps *prdgps = shash_find_data(&ports,
> +                                                              peer_name);
> +            if (!prdgps) {
> +                continue;
> +            }
> +            if (prdgps->r->n_dgps > 1) {
> +                continue;
> +            }
> +            struct ovn_unpaired_port_binding *upb;
> +            struct chassisredirect_switch_port *crp =
> +                chassisredirect_switch_port_alloc(nbsp);
> +            upb = ovn_unpaired_port_binding_alloc(0, crp->name,
> +                                                  CR_SWITCH_PORT_TYPE,
> +                                                  crp, localnets->ls->sb);
> +            shash_add(&map->ports, crp->name, upb);
> +        }
> +    }
> +
> +    return EN_UPDATED;
> +}
> +
> +void
> +en_port_binding_chassisredirect_port_cleanup(void *data)
> +{
> +    struct ovn_unpaired_port_binding_map *map = data;
> +    chassisredirect_port_binding_map_destroy(map);
> +}
> +
> +
> +static void
> +ovn_paired_chassisredirect_port_map_init(
> +    struct ovn_paired_chassisredirect_port_map *map)
> +{
> +    shash_init(&map->paired_chassisredirect_router_ports);
> +    shash_init(&map->paired_chassisredirect_switch_ports);
> +}
> +
> +static void
> +ovn_paired_chassisredirect_port_map_destroy(
> +    struct ovn_paired_chassisredirect_port_map *map)
> +{
> +    shash_destroy_free_data(&map->paired_chassisredirect_switch_ports);
> +    shash_destroy_free_data(&map->paired_chassisredirect_router_ports);
> +}
> +
> +void *
> +en_port_binding_paired_chassisredirect_port_init(
> +    struct engine_node *node OVS_UNUSED, struct engine_arg *args
> OVS_UNUSED)
> +{
> +    struct ovn_paired_chassisredirect_port_map *map = xzalloc(sizeof
> *map);
> +    ovn_paired_chassisredirect_port_map_init(map);
> +    return map;
> +}
> +
> +enum engine_node_state
> +en_port_binding_paired_chassisredirect_port_run(struct engine_node *node,
> +                                                void *data)
> +{
> +    const struct ovn_paired_port_bindings *pbs =
> +        engine_get_input_data("port_binding_pair", node);
> +    struct ovn_paired_chassisredirect_port_map *map = data;
> +
> +    ovn_paired_chassisredirect_port_map_destroy(map);
> +    ovn_paired_chassisredirect_port_map_init(map);
> +
> +    struct ovn_paired_port_binding *port;
> +    LIST_FOR_EACH (port, list_node, &pbs->paired_pbs) {
> +        if (!strcmp(port->type, CR_SWITCH_PORT_TYPE)) {
> +            const struct chassisredirect_switch_port *cr_port =
> port->cookie;
> +            struct ovn_paired_chassisredirect_switch_port *paired_cr_port;
> +            paired_cr_port = xmalloc(sizeof *cr_port);
>

The sizes of  'struct chassisredirect_switch_port' and
'struct ovn_paired_chassisredirect_switch_port' are different.
It really should be 'xmalloc(sizeof *paired_cr_port)'.

+            paired_cr_port->name = cr_port->name;
> +            paired_cr_port->sb = port->sb_pb;
> +            paired_cr_port->primary_port = cr_port->nbsp;
> +            shash_add(&map->paired_chassisredirect_switch_ports,
> cr_port->name,
> +                      paired_cr_port);
> +        } else if (!strcmp(port->type, CR_ROUTER_PORT_TYPE)) {
> +            const struct chassisredirect_router_port *cr_port =
> port->cookie;
> +            struct ovn_paired_chassisredirect_router_port *paired_cr_port;
> +            paired_cr_port = xmalloc(sizeof *cr_port);
>

Same here.

+            paired_cr_port->name = cr_port->name;
> +            paired_cr_port->sb = port->sb_pb;
> +            paired_cr_port->primary_port = cr_port->nbrp;
> +            shash_add(&map->paired_chassisredirect_router_ports,
> cr_port->name,
> +                      paired_cr_port);
> +        }


Both branches are almost identical, as a matter of fact both struct
are almost identical. Could we reuse one struct with indication if
it's router or switch? This is used to lookup 'struct ovn_port'
so it's just a matter of looking it up in proper hmap.


+    }
> +
> +    return EN_UPDATED;
> +}
> +
> +void
> +en_port_binding_paired_chassisredirect_port_cleanup(void *data)
> +{
> +    struct ovn_paired_chassisredirect_port_map *map = data;
> +
> +    ovn_paired_chassisredirect_port_map_destroy(map);
> +}
> diff --git a/northd/en-port-binding-chassisredirect.h
> b/northd/en-port-binding-chassisredirect.h
> new file mode 100644
> index 000000000..bbea31993
> --- /dev/null
> +++ b/northd/en-port-binding-chassisredirect.h
> @@ -0,0 +1,53 @@
> +/*
> + * Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#ifndef EN_PORT_BINDING_CHASSISREDIRECT_PORT_H
> +#define EN_PORT_BINDING_CHASSISREDIRECT_PORT_H
> +
> +#include "lib/inc-proc-eng.h"
> +#include "openvswitch/shash.h"
> +
> +
> +void *en_port_binding_chassisredirect_port_init(struct engine_node *,
> +                                           struct engine_arg *);
> +
> +enum engine_node_state en_port_binding_chassisredirect_port_run(
> +    struct engine_node *, void *data);
> +void en_port_binding_chassisredirect_port_cleanup(void *data);
> +
> +struct ovn_paired_chassisredirect_switch_port {
> +    const char *name;
> +    const struct nbrec_logical_switch_port *primary_port;
> +    const struct sbrec_port_binding *sb;
> +};
> +
> +struct ovn_paired_chassisredirect_router_port {
> +    const char *name;
> +    const struct nbrec_logical_router_port *primary_port;
> +    const struct sbrec_port_binding *sb;
> +};
> +
> +struct ovn_paired_chassisredirect_port_map {
> +    struct shash paired_chassisredirect_router_ports;
> +    struct shash paired_chassisredirect_switch_ports;
> +};
> +
> +void *en_port_binding_paired_chassisredirect_port_init(struct engine_node
> *,
> +                                                       struct engine_arg
> *);
> +enum engine_node_state en_port_binding_paired_chassisredirect_port_run(
> +    struct engine_node *, void *data);
> +void en_port_binding_paired_chassisredirect_port_cleanup(void *data);
> +#endif /* EN_PORT_BINDING_CHASSISREDIRECT_PORT_H */
> diff --git a/northd/en-port-binding-logical-router-port.c
> b/northd/en-port-binding-logical-router-port.c
> new file mode 100644
> index 000000000..07cd9ac26
> --- /dev/null
> +++ b/northd/en-port-binding-logical-router-port.c
> @@ -0,0 +1,178 @@
> +/*
> + * Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#include <config.h>
> +
> +#include "openvswitch/hmap.h"
> +#include "openvswitch/vlog.h"
> +#include "util.h"
> +
> +#include "inc-proc-eng.h"
> +#include "ovn-nb-idl.h"
> +#include "port_binding_pair.h"
> +#include "en-datapath-logical-router.h"
> +#include "en-port-binding-logical-router-port.h"
> +
> +#define LRP_TYPE_NAME "logical-router-port"
> +
> +VLOG_DEFINE_THIS_MODULE(en_port_binding_logical_router_port);
> +
> +struct logical_router_port_cookie {
> +    const struct nbrec_logical_router_port *nbrp;
> +    const struct ovn_synced_logical_router *router;
> +};
> +
> +static struct logical_router_port_cookie *
> +logical_router_port_cookie_alloc(const struct nbrec_logical_router_port
> *nbrp,
> +                                 const struct ovn_synced_logical_router
> *lr)
> +{
> +    struct logical_router_port_cookie *cookie = xmalloc(sizeof *cookie);
> +    cookie->nbrp = nbrp;
> +    cookie->router = lr;
> +
> +    return cookie;
> +}
> +
> +static void
> +logical_router_port_cookie_free(struct logical_router_port_cookie *cookie)
> +{
> +    free(cookie);
> +}
> +
> +static void
> +unpaired_logical_router_port_map_destroy(
> +    struct ovn_unpaired_port_binding_map *map)
> +{
> +    struct shash_node *node;
> +    SHASH_FOR_EACH (node, &map->ports) {
> +        struct ovn_unpaired_port_binding *upb = node->data;
> +        logical_router_port_cookie_free(upb->cookie);
> +    }
> +    ovn_unpaired_port_binding_map_destroy(map);
> +}
> +
> +void *
> +en_port_binding_logical_router_port_init(struct engine_node *node
> OVS_UNUSED,
> +                                         struct engine_arg *args
> OVS_UNUSED)
> +{
> +    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
> +    ovn_unpaired_port_binding_map_init(map, NULL);
> +    return map;
> +}
> +
> +enum engine_node_state
> +en_port_binding_logical_router_port_run(struct engine_node *node, void
> *data)
> +{
> +    const struct ovn_synced_logical_router_map *lr_map =
> +        engine_get_input_data("datapath_synced_logical_router", node);
> +
> +    struct ovn_unpaired_port_binding_map *map = data;
> +
> +    unpaired_logical_router_port_map_destroy(map);
> +    ovn_unpaired_port_binding_map_init(map, NULL);
> +
> +    const struct ovn_synced_logical_router *paired_lr;
> +    HMAP_FOR_EACH (paired_lr, hmap_node, &lr_map->synced_routers) {
> +        const struct nbrec_logical_router_port *nbrp;
> +        for (size_t i = 0; i < paired_lr->nb->n_ports; i++) {
> +            nbrp = paired_lr->nb->ports[i];
> +            uint32_t requested_tunnel_key = smap_get_int(&nbrp->options,
> +
>  "requested-tnl-key",
> +                                                         0);
> +            struct logical_router_port_cookie *cookie =
> +                logical_router_port_cookie_alloc(nbrp, paired_lr);
> +            struct ovn_unpaired_port_binding *upb;
> +            upb = ovn_unpaired_port_binding_alloc(requested_tunnel_key,
> +                                                  nbrp->name,
> +                                                  LRP_TYPE_NAME,
> +                                                  cookie,
> +                                                  paired_lr->sb);
> +            smap_clone(&upb->external_ids, &nbrp->external_ids);
> +            if (!shash_add_once(&map->ports, nbrp->name, upb)) {
> +                static struct vlog_rate_limit rl =
> VLOG_RATE_LIMIT_INIT(5, 1);
> +                VLOG_WARN_RL(&rl, "duplicate logical router port %s",
> +                             nbrp->name);
> +            }
> +        }
> +    }
> +
> +    return EN_UPDATED;
> +}
> +
> +void
> +en_port_binding_logical_router_port_cleanup(void *data)
> +{
> +    struct ovn_unpaired_port_binding_map *map = data;
> +    unpaired_logical_router_port_map_destroy(map);
> +}
> +
> +static void
> +paired_logical_router_port_map_init(
> +    struct ovn_paired_logical_router_port_map *router_port_map)
> +{
> +    shash_init(&router_port_map->paired_router_ports);
> +}
> +
> +static void
> +paired_logical_router_port_map_destroy(
> +    struct ovn_paired_logical_router_port_map *router_port_map)
> +{
> +    shash_destroy_free_data(&router_port_map->paired_router_ports);
> +}
> +
> +void *
> +en_port_binding_paired_logical_router_port_init(
> +    struct engine_node *node OVS_UNUSED, struct engine_arg *args
> OVS_UNUSED)
> +{
> +    struct ovn_paired_logical_router_port_map *router_port_map;
> +    router_port_map = xzalloc(sizeof *router_port_map);
> +    paired_logical_router_port_map_init(router_port_map);
> +
> +    return router_port_map;
> +}
> +
> +enum engine_node_state
> +en_port_binding_paired_logical_router_port_run(struct engine_node *node,
> +                                               void *data)
> +{
> +    const struct ovn_paired_port_bindings *pbs =
> +        engine_get_input_data("port_binding_pair", node);
> +    struct ovn_paired_logical_router_port_map *router_port_map = data;
> +
> +    paired_logical_router_port_map_destroy(router_port_map);
> +    paired_logical_router_port_map_init(router_port_map);
> +
> +    struct ovn_paired_port_binding *spb;
> +    LIST_FOR_EACH (spb, list_node, &pbs->paired_pbs) {
> +        if (strcmp(spb->type, LRP_TYPE_NAME)) {
> +            continue;
> +        }
> +        const struct logical_router_port_cookie *cookie = spb->cookie;
> +        struct ovn_paired_logical_router_port *lrp = xzalloc(sizeof *lrp);
> +        lrp->nb = cookie->nbrp;
> +        lrp->router = cookie->router;
> +        lrp->sb = spb->sb_pb;
> +        shash_add(&router_port_map->paired_router_ports, lrp->nb->name,
> lrp);
> +    }
> +
> +    return EN_UPDATED;
> +}
> +
> +void en_port_binding_paired_logical_router_port_cleanup(void *data)
> +{
> +    struct ovn_paired_logical_router_port_map *map = data;
> +    paired_logical_router_port_map_destroy(map);
> +}
> diff --git a/northd/en-port-binding-logical-router-port.h
> b/northd/en-port-binding-logical-router-port.h
> new file mode 100644
> index 000000000..156a25da6
> --- /dev/null
> +++ b/northd/en-port-binding-logical-router-port.h
> @@ -0,0 +1,47 @@
> +/*
> + * Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#ifndef EN_PORT_BINDING_LOGICAL_ROUTER_PORT_H
> +#define EN_PORT_BINDING_LOGICAL_ROUTER_PORT_H
> +
> +#include "lib/inc-proc-eng.h"
> +#include "openvswitch/shash.h"
> +
> +void *en_port_binding_logical_router_port_init(struct engine_node *,
> +                                           struct engine_arg *);
> +
> +enum engine_node_state en_port_binding_logical_router_port_run(
> +    struct engine_node *, void *data);
> +void en_port_binding_logical_router_port_cleanup(void *data);
> +
> +struct ovn_paired_logical_router_port {
> +    const struct nbrec_logical_router_port *nb;
> +    const struct sbrec_port_binding *sb;
> +    const struct ovn_synced_logical_router *router;
> +};
> +
> +struct ovn_paired_logical_router_port_map {
> +    struct shash paired_router_ports;
> +};
> +
> +void *en_port_binding_paired_logical_router_port_init(struct engine_node
> *,
> +                                                      struct engine_arg
> *);
> +
> +enum engine_node_state en_port_binding_paired_logical_router_port_run(
> +    struct engine_node *, void *data);
> +void en_port_binding_paired_logical_router_port_cleanup(void *data);
> +
> +#endif /* EN_PORT_BINDING_LOGICAL_ROUTER_PORT_H */
> diff --git a/northd/en-port-binding-logical-switch-port.c
> b/northd/en-port-binding-logical-switch-port.c
> new file mode 100644
> index 000000000..d4fcece5f
> --- /dev/null
> +++ b/northd/en-port-binding-logical-switch-port.c
> @@ -0,0 +1,231 @@
> +/*
> + * Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#include <config.h>
> +
> +#include "openvswitch/hmap.h"
> +#include "openvswitch/vlog.h"
> +#include "util.h"
> +
> +#include "inc-proc-eng.h"
> +#include "ovn-nb-idl.h"
> +#include "ovn-sb-idl.h"
> +#include "port_binding_pair.h"
> +#include "en-datapath-logical-switch.h"
> +#include "en-port-binding-logical-switch-port.h"
> +#include "northd.h"
> +
> +#define LSP_TYPE_NAME "logical-switch-port"
> +
> +VLOG_DEFINE_THIS_MODULE(en_port_binding_logical_switch_port);
> +
> +struct logical_switch_port_cookie {
> +    const struct nbrec_logical_switch_port *nbsp;
> +    const struct ovn_synced_logical_switch *sw;
> +};
> +
> +static struct logical_switch_port_cookie *
> +logical_switch_port_cookie_alloc(const struct nbrec_logical_switch_port
> *nbsp,
> +                                 const struct ovn_synced_logical_switch
> *sw)
> +{
> +    struct logical_switch_port_cookie *cookie = xmalloc(sizeof *cookie);
> +    cookie->nbsp = nbsp;
> +    cookie->sw = sw;
> +    return cookie;
> +}
> +
> +static void
> +logical_switch_port_cookie_free(struct logical_switch_port_cookie *cookie)
> +{
> +    free(cookie);
> +}
> +
> +static bool
> +switch_port_sb_is_valid(const struct sbrec_port_binding *sb_pb,
> +                        const struct ovn_unpaired_port_binding *upb)
> +{
> +    const struct logical_switch_port_cookie *cookie = upb->cookie;
> +
> +    bool update_sbrec = false;
> +    if (lsp_is_type_changed(sb_pb, cookie->nbsp, &update_sbrec)
> +        && update_sbrec) {
> +        return false;
> +    }
> +
> +    return true;
> +}
> +
> +struct ovn_unpaired_port_binding_map_callbacks switch_port_callbacks = {
> +    .sb_is_valid = switch_port_sb_is_valid,
> +};
> +
> +static void
> +unpaired_logical_switch_port_map_destroy(
> +    struct ovn_unpaired_port_binding_map *map)
> +{
> +    struct shash_node *node;
> +    SHASH_FOR_EACH (node, &map->ports) {
> +        struct ovn_unpaired_port_binding *upb = node->data;
> +        logical_switch_port_cookie_free(upb->cookie);
> +    }
> +    ovn_unpaired_port_binding_map_destroy(map);
> +}
> +
> +void *
> +en_port_binding_logical_switch_port_init(struct engine_node *node
> OVS_UNUSED,
> +                                         struct engine_arg *args
> OVS_UNUSED)
> +{
> +    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
> +    ovn_unpaired_port_binding_map_init(map, &switch_port_callbacks);
> +    return map;
> +}
> +
> +enum engine_node_state
> +en_port_binding_logical_switch_port_run(struct engine_node *node, void
> *data)
> +{
> +    const struct ovn_synced_logical_switch_map *ls_map =
> +        engine_get_input_data("datapath_synced_logical_switch", node);
> +
> +    struct ovn_unpaired_port_binding_map *map = data;
> +
> +    unpaired_logical_switch_port_map_destroy(map);
> +    ovn_unpaired_port_binding_map_init(map, &switch_port_callbacks);
> +
> +    const struct ovn_synced_logical_switch *paired_ls;
> +    HMAP_FOR_EACH (paired_ls, hmap_node, &ls_map->synced_switches) {
> +        const struct nbrec_logical_switch_port *nbsp;
> +        for (size_t i = 0; i < paired_ls->nb->n_ports; i++) {
> +            nbsp = paired_ls->nb->ports[i];
> +            uint32_t requested_tunnel_key = smap_get_int(&nbsp->options,
> +
>  "requested-tnl-key",
> +                                                         0);
> +            struct logical_switch_port_cookie *cookie =
> +                logical_switch_port_cookie_alloc(nbsp, paired_ls);
> +            struct ovn_unpaired_port_binding *upb;
> +            upb = ovn_unpaired_port_binding_alloc(requested_tunnel_key,
> +                                                  nbsp->name,
> +                                                  LSP_TYPE_NAME,
> +                                                  cookie,
> +                                                  paired_ls->sb);
> +            smap_clone(&upb->external_ids, &nbsp->external_ids);
> +            const char *name = smap_get(&nbsp->external_ids,
> +                                        "neutron:port_name");
> +            if (name && name[0]) {
> +                smap_add(&upb->external_ids, "name", name);
> +            }
> +            if (!shash_add_once(&map->ports, nbsp->name, upb)) {
> +                static struct vlog_rate_limit rl =
> VLOG_RATE_LIMIT_INIT(5, 1);
> +                VLOG_WARN_RL(&rl, "duplicate logical port %s",
> nbsp->name);
> +            }
> +        }
> +    }
> +    return EN_UPDATED;
> +}
> +
> +void
> +en_port_binding_logical_switch_port_cleanup(void *data)
> +{
> +    struct ovn_unpaired_port_binding_map *map = data;
> +    unpaired_logical_switch_port_map_destroy(map);
> +}
> +
> +static void
> +paired_logical_switch_port_map_init(
> +    struct ovn_paired_logical_switch_port_map *switch_port_map)
> +{
> +    shash_init(&switch_port_map->paired_switch_ports);
> +}
> +
> +static void
> +paired_logical_switch_port_map_destroy(
> +    struct ovn_paired_logical_switch_port_map *switch_port_map)
> +{
> +    shash_destroy_free_data(&switch_port_map->paired_switch_ports);
> +}
> +
> +void *
> +en_port_binding_paired_logical_switch_port_init(
> +    struct engine_node *node OVS_UNUSED, struct engine_arg *args
> OVS_UNUSED)
> +{
> +    struct ovn_paired_logical_switch_port_map *switch_port_map;
> +    switch_port_map = xzalloc(sizeof *switch_port_map);
> +    paired_logical_switch_port_map_init(switch_port_map);
> +
> +    return switch_port_map;
> +}
> +
> +enum engine_node_state
> +en_port_binding_paired_logical_switch_port_run(struct engine_node *node,
> +                                               void *data)
> +{
> +    const struct ovn_paired_port_bindings *pbs =
> +        engine_get_input_data("port_binding_pair", node);
> +    struct ovn_paired_logical_switch_port_map *switch_port_map = data;
> +
> +    paired_logical_switch_port_map_destroy(switch_port_map);
> +    paired_logical_switch_port_map_init(switch_port_map);
> +
> +    struct ovn_paired_port_binding *spb;
> +    LIST_FOR_EACH (spb, list_node, &pbs->paired_pbs) {
> +        if (strcmp(spb->type, LSP_TYPE_NAME)) {
> +            continue;
> +        }
> +        const struct logical_switch_port_cookie *cookie = spb->cookie;
> +        struct ovn_paired_logical_switch_port *lsw = xzalloc(sizeof *lsw);
> +        lsw->nb = cookie->nbsp;
> +        lsw->sw = cookie->sw;
> +        lsw->sb = spb->sb_pb;
> +        shash_add(&switch_port_map->paired_switch_ports, lsw->nb->name,
> lsw);
> +
> +        /* This deals with a special case where a logical switch port is
> +         * removed and added back very quickly. The sequence of events is
> as
> +         * follows:
> +         * 1) NB Logical_Switch_Port "lsp" is added to the NB DB.
> +         * 2) en-port-binding-pair creates a corresponding SB
> Port_Binding.
> +         * 3) The user binds the port to a hypervisor.
> +         * 4) ovn-controller on the hypervisor sets the SB Port_Binding
> "up"
> +         *    column to "true".
> +         * 5) ovn-northd sets the Logical_Switch_Port "up" column to
> "true".
> +         * 6) A user deletes and then re-adds "lsp" back to the NB
> +         *    Logical_Switch_Port column very quickly, so quickly that we
> +         *    do not detect the deletion at all.
> +         * 7) The new northbound Logical_Switch_Port has its "up" column
> +         *    empty (i.e. not "true") since it is new.
> +         * 8) The pairing code matches the new Logical_Switch_Port with
> the
> +         *    existing Port_Binding for "lsp" since the pairing code
> matches
> +         *    using the name of the Logical_Switch_Port.
> +         *
> +         * At this point, the SB Port_Binding's "up" column is set "true",
> +         * but the NB Logical_Switch_Port's "up" column is not. We need to
> +         * ensure the NB Logical_Switch_Port's "up" column is set to
> "true"
> +         * as well.
> +         *
> +         * In most cases, setting the NB Logical_Switch_Port "up" column
> to
> +         * true is accomplished when changes on the SB Port_Binding are
> +         * detected. But in this rare case, there is no SB Port_Binding
> +         * change, so the "up" column is unserviced.
> +         */
> +        lsp_set_up(lsw->sb, lsw->nb);
> +    }
> +
> +    return EN_UPDATED;
> +}
> +
> +void en_port_binding_paired_logical_switch_port_cleanup(void *data)
> +{
> +    struct ovn_paired_logical_switch_port_map *map = data;
> +    paired_logical_switch_port_map_destroy(map);
> +}
> diff --git a/northd/en-port-binding-logical-switch-port.h
> b/northd/en-port-binding-logical-switch-port.h
> new file mode 100644
> index 000000000..9ef32ce88
> --- /dev/null
> +++ b/northd/en-port-binding-logical-switch-port.h
> @@ -0,0 +1,48 @@
> +/*
> + * Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#ifndef EN_PORT_BINDING_LOGICAL_SWITCH_PORT_H
> +#define EN_PORT_BINDING_LOGICAL_SWITCH_PORT_H
> +
> +#include "lib/inc-proc-eng.h"
> +#include "openvswitch/shash.h"
> +
> +
> +void *en_port_binding_logical_switch_port_init(struct engine_node *,
> +                                           struct engine_arg *);
> +
> +enum engine_node_state en_port_binding_logical_switch_port_run(
> +    struct engine_node *, void *data);
> +void en_port_binding_logical_switch_port_cleanup(void *data);
> +
> +struct ovn_paired_logical_switch_port {
> +    const struct nbrec_logical_switch_port *nb;
> +    const struct sbrec_port_binding *sb;
> +    const struct ovn_synced_logical_switch *sw;
> +};
> +
> +struct ovn_paired_logical_switch_port_map {
> +    struct shash paired_switch_ports;
> +};
> +
> +void *en_port_binding_paired_logical_switch_port_init(struct engine_node
> *,
> +                                                      struct engine_arg
> *);
> +
> +enum engine_node_state en_port_binding_paired_logical_switch_port_run(
> +    struct engine_node *, void *data);
> +void en_port_binding_paired_logical_switch_port_cleanup(void *data);
> +
> +#endif /* EN_PORT_BINDING_LOGICAL_SWITCH_PORT_H */
> diff --git a/northd/en-port-binding-mirror.c
> b/northd/en-port-binding-mirror.c
> new file mode 100644
> index 000000000..6e8647dba
> --- /dev/null
> +++ b/northd/en-port-binding-mirror.c
> @@ -0,0 +1,191 @@
> +/*
> + * Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +#include <config.h>
> +#include "ovn-util.h"
> +#include "lib/inc-proc-eng.h"
> +#include "ovn-nb-idl.h"
> +#include "en-datapath-logical-switch.h"
> +#include "en-port-binding-mirror.h"
> +#include "port_binding_pair.h"
> +#include "northd.h"
> +#include "openvswitch/vlog.h"
> +
> +#define MIRROR_PORT_TYPE "mirror"
> +
> +VLOG_DEFINE_THIS_MODULE(en_port_binding_mirror);
> +
> +struct mirror_port {
> +    char *name;
> +    const char *sink;
> +    const struct nbrec_logical_switch_port *nbsp;
> +};
> +
> +static struct mirror_port *
> +mirror_port_alloc(const struct sbrec_datapath_binding *sb, const char
> *sink,
> +                  const struct nbrec_logical_switch_port *nbsp)
> +{
> +    struct mirror_port *mp = xzalloc(sizeof *mp);
> +    mp->name = ovn_mirror_port_name(ovn_datapath_name(sb), sink);
> +    mp->sink = sink;
> +    mp->nbsp = nbsp;
> +
> +    return mp;
> +}
> +
> +static void
> +mirror_port_free(struct mirror_port *mp)
> +{
> +    free(mp->name);
> +    free(mp);
> +}
> +
> +static void
> +unpaired_mirror_map_destroy(struct ovn_unpaired_port_binding_map *map)
> +{
> +    struct shash_node *node;
> +    SHASH_FOR_EACH (node, &map->ports) {
> +        struct ovn_unpaired_port_binding *upb = node->data;
> +        mirror_port_free(upb->cookie);
> +    }
> +    ovn_unpaired_port_binding_map_destroy(map);
> +}
> +
> +void *
> +en_port_binding_mirror_init(struct engine_node *node OVS_UNUSED,
> +                            struct engine_arg *arg OVS_UNUSED)
> +{
> +    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
> +    ovn_unpaired_port_binding_map_init(map, NULL);
> +    return map;
> +}
> +
> +enum engine_node_state
> +en_port_binding_mirror_run(struct engine_node *node, void *data)
> +{
> +    const struct ovn_synced_logical_switch_map *ls_map =
> +        engine_get_input_data("datapath_synced_logical_switch", node);
> +    struct ovn_unpaired_port_binding_map *map = data;
> +
> +    unpaired_mirror_map_destroy(map);
> +    ovn_unpaired_port_binding_map_init(map, NULL);
> +
> +    /* Typically, we'd use an ovsdb_idl_index to search for a specific
> record
> +     * based on a column value. However, we currently are not monitoring
> +     * the Logical_Switch_Port table at all in ovn-northd. Introducing
> +     * this monitoring is likely more computationally intensive than
> +     * making an on-the-fly sset of logical switch port names.
> +     */
> +    struct sset all_switch_ports = SSET_INITIALIZER(&all_switch_ports);
> +    const struct ovn_synced_logical_switch *ls;
> +    HMAP_FOR_EACH (ls, hmap_node, &ls_map->synced_switches) {
> +        for (size_t i = 0; i < ls->nb->n_ports; i++) {
> +            sset_add(&all_switch_ports, ls->nb->ports[i]->name);
> +        }
> +    }
> +
> +    HMAP_FOR_EACH (ls, hmap_node, &ls_map->synced_switches) {
> +        for (size_t i = 0; i < ls->nb->n_ports; i++) {
> +            const struct nbrec_logical_switch_port *nbsp =
> ls->nb->ports[i];
> +            for (size_t j = 0; j < nbsp->n_mirror_rules; j++) {
> +                struct nbrec_mirror *nb_mirror = nbsp->mirror_rules[j];
> +                if (strcmp(nb_mirror->type, "lport")) {
> +                    continue;
> +                }
> +                if (!sset_find(&all_switch_ports, nb_mirror->sink)) {
> +                    continue;
> +                }
> +                struct mirror_port *mp = mirror_port_alloc(ls->sb,
> +
>  nb_mirror->sink,
> +                                                           nbsp);
> +                struct ovn_unpaired_port_binding *upb;
> +                upb = ovn_unpaired_port_binding_alloc(0, mp->name,
> +                                                      MIRROR_PORT_TYPE,
> mp,
> +                                                      ls->sb);
> +                shash_add(&map->ports, mp->name, upb);
> +            }
> +        }
> +    }
> +    sset_destroy(&all_switch_ports);
> +
> +    return EN_UPDATED;
> +}
> +
> +void
> +en_port_binding_mirror_cleanup(void *data)
> +{
> +    struct ovn_unpaired_port_binding_map *map = data;
> +    unpaired_mirror_map_destroy(map);
> +}
> +
> +static void
> +ovn_paired_mirror_map_init(
> +    struct ovn_paired_mirror_map *map)
> +{
> +    shash_init(&map->paired_mirror_ports);
> +}
> +
> +static void
> +ovn_paired_mirror_map_destroy(
> +    struct ovn_paired_mirror_map *map)
> +{
> +    shash_destroy_free_data(&map->paired_mirror_ports);
> +}
> +
> +void *
> +en_port_binding_paired_mirror_init(struct engine_node *node OVS_UNUSED,
> +                                   struct engine_arg *arg OVS_UNUSED)
> +{
> +    struct ovn_paired_mirror_map *map = xzalloc(sizeof *map);
> +    ovn_paired_mirror_map_init(map);
> +    return map;
> +}
> +
> +enum engine_node_state
> +en_port_binding_paired_mirror_run(struct engine_node *node,
> +                                  void *data)
> +{
> +    const struct ovn_paired_port_bindings *pbs =
> +        engine_get_input_data("port_binding_pair", node);
> +    struct ovn_paired_mirror_map *map = data;
> +
> +    ovn_paired_mirror_map_destroy(map);
> +    ovn_paired_mirror_map_init(map);
> +
> +    struct ovn_paired_port_binding *port;
> +    LIST_FOR_EACH (port, list_node, &pbs->paired_pbs) {
> +        if (strcmp(port->type, MIRROR_PORT_TYPE)) {
> +            continue;
> +        }
> +        const struct mirror_port *mp = port->cookie;
> +        struct ovn_paired_mirror *opm = xmalloc(sizeof *opm);
> +        opm->name = mp->name;
> +        opm->sink = mp->sink;
> +        opm->sb = port->sb_pb;
> +        opm->nbsp = mp->nbsp;
> +        shash_add(&map->paired_mirror_ports, opm->name, opm);
> +    }
> +
> +    return EN_UPDATED;
> +}
> +
> +void
> +en_port_binding_paired_mirror_cleanup(void *data)
> +{
> +    struct ovn_paired_mirror_map *map = data;
> +
> +    ovn_paired_mirror_map_destroy(map);
> +}
> +
> diff --git a/northd/en-port-binding-mirror.h
> b/northd/en-port-binding-mirror.h
> new file mode 100644
> index 000000000..a4bf2645a
> --- /dev/null
> +++ b/northd/en-port-binding-mirror.h
> @@ -0,0 +1,48 @@
> +/*
> + * Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#ifndef EN_PORT_BINDING_MIRROR_H
> +#define EN_PORT_BINDING_MIRROR_H
> +
> +#include "lib/inc-proc-eng.h"
> +#include "openvswitch/shash.h"
> +
> +void *en_port_binding_mirror_init(struct engine_node *,
> +                                  struct engine_arg *);
> +
> +enum engine_node_state en_port_binding_mirror_run(struct engine_node *,
> +                                                  void *data);
> +void en_port_binding_mirror_cleanup(void *data);
> +
> +struct ovn_paired_mirror {
> +    const char *name;
> +    const char *sink;
> +    const struct nbrec_logical_switch_port *nbsp;
> +    const struct sbrec_port_binding *sb;
> +};
> +
> +struct ovn_paired_mirror_map {
> +    struct shash paired_mirror_ports;
> +};
> +
> +void *en_port_binding_paired_mirror_init(struct engine_node *,
> +                                         struct engine_arg *);
> +
> +enum engine_node_state en_port_binding_paired_mirror_run(struct
> engine_node *,
> +                                                         void *data);
> +void en_port_binding_paired_mirror_cleanup(void *data);
> +
> +#endif /* EN_PORT_BINDING_MIRROR_H */
> diff --git a/northd/en-port-binding-pair.c b/northd/en-port-binding-pair.c
> new file mode 100644
> index 000000000..21f7fefc8
> --- /dev/null
> +++ b/northd/en-port-binding-pair.c
> @@ -0,0 +1,467 @@
> +/*
> + * Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#include <config.h>
> +
> +#include "en-port-binding-pair.h"
> +#include "en-global-config.h"
> +#include "port_binding_pair.h"
> +#include "ovn-sb-idl.h"
> +#include "mcast-group-index.h"
> +
> +#include "openvswitch/vlog.h"
> +
> +VLOG_DEFINE_THIS_MODULE(port_binding_pair);
> +
> +void *
> +en_port_binding_pair_init(struct engine_node *node OVS_UNUSED,
> +                      struct engine_arg *args OVS_UNUSED)
> +{
> +    struct ovn_paired_port_bindings *paired_port_bindings
> +        = xzalloc(sizeof *paired_port_bindings);
> +    ovs_list_init(&paired_port_bindings->paired_pbs);
> +    hmap_init(&paired_port_bindings->tunnel_key_maps);
> +
> +    return paired_port_bindings;
> +}
> +
> +static struct ovn_unpaired_port_binding *
> +find_unpaired_port_binding(const struct ovn_unpaired_port_binding_map
> **maps,
> +                           size_t n_maps,
> +                           const struct sbrec_port_binding *sb_pb)
> +{
> +    const struct ovn_unpaired_port_binding_map *map;
> +
> +    for (size_t i = 0; i < n_maps; i++) {
> +        map = maps[i];
> +        struct ovn_unpaired_port_binding *upb;
> +        upb = shash_find_data(&map->ports, sb_pb->logical_port);
> +        if (upb && map->cb->sb_is_valid(sb_pb, upb)) {
> +            return upb;
> +        }
> +    }
> +
> +    return NULL;
> +}
> +
> +struct tunnel_key_map {
> +    struct hmap_node hmap_node;
> +    uint32_t datapath_tunnel_key;
> +    struct hmap port_tunnel_keys;
> +};
> +
> +static struct tunnel_key_map *
> +find_tunnel_key_map(uint32_t datapath_tunnel_key,
> +                    const struct hmap *tunnel_key_maps)
> +{
> +    uint32_t hash = hash_int(datapath_tunnel_key, 0);
> +    struct tunnel_key_map *key_map;
> +    HMAP_FOR_EACH_WITH_HASH (key_map, hmap_node, hash, tunnel_key_maps) {
> +        if (key_map->datapath_tunnel_key == datapath_tunnel_key) {
> +            return key_map;
> +        }
> +    }
> +    return NULL;
> +}
> +
> +static struct tunnel_key_map *
> +alloc_tunnel_key_map(uint32_t datapath_tunnel_key,
> +                     struct hmap *tunnel_key_maps)
> +{
> +    uint32_t hash = hash_int(datapath_tunnel_key, 0);
> +    struct tunnel_key_map *key_map;
> +
> +    key_map = xzalloc(sizeof *key_map);
> +    key_map->datapath_tunnel_key = datapath_tunnel_key;
> +    hmap_init(&key_map->port_tunnel_keys);
> +    hmap_insert(tunnel_key_maps, &key_map->hmap_node, hash);
> +
> +    return key_map;
> +
> +}
> +
> +static struct tunnel_key_map *
> +find_or_alloc_tunnel_key_map(const struct sbrec_datapath_binding *sb_dp,
> +                             struct hmap *tunnel_key_maps)
> +{
> +    struct tunnel_key_map *key_map =
> find_tunnel_key_map(sb_dp->tunnel_key,
> +                                                         tunnel_key_maps);
> +    if (!key_map) {
> +        key_map = alloc_tunnel_key_map(sb_dp->tunnel_key,
> tunnel_key_maps);
> +    }
> +    return key_map;
> +}
> +
> +static void
> +tunnel_key_maps_destroy(struct hmap *tunnel_key_maps)
> +{
> +    struct tunnel_key_map *key_map;
> +    HMAP_FOR_EACH_POP (key_map, hmap_node, tunnel_key_maps) {
> +        hmap_destroy(&key_map->port_tunnel_keys);
> +        free(key_map);
> +    }
> +    hmap_destroy(tunnel_key_maps);
> +}
> +
> +struct candidate_spb {
> +    struct ovs_list list_node;
> +    struct ovn_paired_port_binding *spb;
> +    uint32_t requested_tunnel_key;
> +    uint32_t existing_tunnel_key;
> +    struct tunnel_key_map *tunnel_key_map;
> +};
> +
> +static void
> +reset_port_binding_pair_data(
> +    struct ovn_paired_port_bindings *paired_port_bindings)
> +{
> +    /* Free the old paired port_bindings */
> +    struct ovn_paired_port_binding *spb;
> +    LIST_FOR_EACH_POP (spb, list_node, &paired_port_bindings->paired_pbs)
> {
> +        free(spb);
> +    }
> +    tunnel_key_maps_destroy(&paired_port_bindings->tunnel_key_maps);
> +
> +    hmap_init(&paired_port_bindings->tunnel_key_maps);
> +    ovs_list_init(&paired_port_bindings->paired_pbs);
> +}
> +
> +static struct candidate_spb *
> +candidate_spb_alloc(const struct ovn_unpaired_port_binding *upb,
> +                    const struct sbrec_port_binding *sb_pb,
> +                    struct hmap *tunnel_key_maps)
> +{
> +    struct ovn_paired_port_binding *spb;
> +    spb = xzalloc(sizeof *spb);
> +    spb->sb_pb = sb_pb;
> +    spb->cookie = upb->cookie;
> +    spb->type = upb->type;
> +    sbrec_port_binding_set_external_ids(sb_pb, &upb->external_ids);
> +    sbrec_port_binding_set_logical_port(sb_pb, upb->name);
> +
> +    struct candidate_spb *candidate;
> +    candidate = xzalloc(sizeof *candidate);
> +    candidate->spb = spb;
> +    candidate->requested_tunnel_key = upb->requested_tunnel_key;
> +    candidate->existing_tunnel_key = spb->sb_pb->tunnel_key;
> +    candidate->tunnel_key_map = find_or_alloc_tunnel_key_map(upb->sb_dp,
> +
>  tunnel_key_maps);
> +
> +    return candidate;
> +}
> +
> +static void
> +get_candidate_pbs_from_sb(
> +    const struct sbrec_port_binding_table *sb_pb_table,
> +    const struct ovn_unpaired_port_binding_map **input_maps,
> +    size_t n_input_maps, struct hmap *tunnel_key_maps,
> +    struct ovs_list *candidate_spbs, struct smap *visited)
> +{
> +    const struct sbrec_port_binding *sb_pb;
> +    const struct ovn_unpaired_port_binding *upb;
> +    SBREC_PORT_BINDING_TABLE_FOR_EACH_SAFE (sb_pb, sb_pb_table) {
> +        upb = find_unpaired_port_binding(input_maps, n_input_maps, sb_pb);
> +        if (!upb) {
> +            sbrec_port_binding_delete(sb_pb);
> +            continue;
> +        }
> +
> +        if (!uuid_equals(&upb->sb_dp->header_.uuid,
> +            &sb_pb->datapath->header_.uuid)) {
> +            /* A matching unpaired port was found for this port binding,
> but it
> +             * has moved to a different datapath. Delete the old SB port
> +             * binding so that a new one will be created later when we
> traverse
> +             * unpaired port bindings later.
> +             */
> +            sbrec_port_binding_delete(sb_pb);
> +            continue;
> +        }
> +
> +        if (!smap_add_once(visited, sb_pb->logical_port, upb->type)) {
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
> +            VLOG_INFO_RL(
> +                &rl, "deleting port_binding "UUID_FMT" with "
> +                "duplicate name %s",
> +                UUID_ARGS(&sb_pb->header_.uuid), sb_pb->logical_port);
> +            sbrec_port_binding_delete(sb_pb);
> +            continue;
> +        }
> +        struct candidate_spb *candidate;
> +        candidate = candidate_spb_alloc(upb, sb_pb, tunnel_key_maps);
> +        ovs_list_push_back(candidate_spbs, &candidate->list_node);
> +    }
> +}
> +
> +static void
> +get_candidate_pbs_from_nb(
> +    struct ovsdb_idl_txn *ovnsb_idl_txn,
> +    const struct ovn_unpaired_port_binding_map **input_maps,
> +    uint32_t n_input_maps,
> +    struct hmap *tunnel_key_maps,
> +    struct ovs_list *candidate_spbs,
> +    struct smap *visited)
> +{
> +    for (size_t i = 0; i < n_input_maps; i++) {
> +        const struct ovn_unpaired_port_binding_map *map = input_maps[i];
> +        struct shash_node *shash_node;
> +        SHASH_FOR_EACH (shash_node, &map->ports) {
> +            const struct ovn_unpaired_port_binding *upb =
> shash_node->data;
> +            const char *visited_type = smap_get(visited, upb->name);
> +            if (visited_type) {
> +                if (strcmp(upb->type, visited_type)) {
> +                    static struct vlog_rate_limit rl =
> VLOG_RATE_LIMIT_INIT(5,
> +
>   1);
> +                    VLOG_WARN_RL(&rl, "duplicate logical port %s",
> upb->name);
> +                }
> +                continue;
> +            } else {
> +                /* Add the port to "visited" to help with detection of
> +                 * duplicated port names across different types of ports.
> +                 */
> +                smap_add_once(visited, upb->name, upb->type);
> +            }
> +            const struct sbrec_port_binding *sb_pb;
> +            sb_pb = sbrec_port_binding_insert(ovnsb_idl_txn);
> +
> +            struct candidate_spb *candidate;
> +            candidate = candidate_spb_alloc(upb, sb_pb, tunnel_key_maps);
> +            ovs_list_push_back(candidate_spbs, &candidate->list_node);
> +        }
> +    }
> +}
> +
> +static void
> +pair_requested_tunnel_keys(struct ovs_list *candidate_spbs,
> +                           struct ovs_list *paired_pbs)
> +{
> +    struct candidate_spb *candidate;
> +    LIST_FOR_EACH_SAFE (candidate, list_node, candidate_spbs) {
> +        if (!candidate->requested_tunnel_key) {
> +            continue;
> +        }
> +        if (candidate->requested_tunnel_key >= OVN_VXLAN_MIN_MULTICAST) {
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> +            VLOG_WARN_RL(&rl, "Tunnel key %"PRIu32" for port %s"
> +                         " is incompatible with VXLAN",
> +                         candidate->requested_tunnel_key,
> +                         candidate->spb->sb_pb->logical_port);
> +            continue;
> +        }
> +
> +        if (ovn_add_tnlid(&candidate->tunnel_key_map->port_tunnel_keys,
> +                          candidate->requested_tunnel_key)) {
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> +            VLOG_WARN_RL(&rl, "Logical port_binding %s requests same "
> +                         "tunnel key %"PRIu32" as another logical "
> +                         "port_binding on the same datapath",
> +                         candidate->spb->sb_pb->logical_port,
> +                         candidate->requested_tunnel_key);
> +        }
> +        sbrec_port_binding_set_tunnel_key(candidate->spb->sb_pb,
> +
> candidate->requested_tunnel_key);
> +        ovs_list_remove(&candidate->list_node);
> +        ovs_list_push_back(paired_pbs, &candidate->spb->list_node);
> +        free(candidate);
> +    }
> +}
> +
> +static void
> +pair_existing_tunnel_keys(struct ovs_list *candidate_spbs,
> +                          struct ovs_list *paired_pbs)
> +{
> +    struct candidate_spb *candidate;
> +    LIST_FOR_EACH_SAFE (candidate, list_node, candidate_spbs) {
> +        if (!candidate->existing_tunnel_key) {
> +            continue;
> +        }
> +        /* Existing southbound pb. If this key is available,
> +         * reuse it.
> +         */
> +        if (ovn_add_tnlid(&candidate->tunnel_key_map->port_tunnel_keys,
> +                          candidate->existing_tunnel_key)) {
> +            ovs_list_remove(&candidate->list_node);
> +            ovs_list_push_back(paired_pbs, &candidate->spb->list_node);
> +            free(candidate);
> +        }
> +    }
> +}
> +
> +static void
> +pair_new_tunnel_keys(struct ovs_list *candidate_spbs,
> +                     struct ovs_list *paired_pbs,
> +                     uint32_t max_pb_tunnel_id)
> +{
> +    uint32_t hint = 0;
> +    struct candidate_spb *candidate;
> +    LIST_FOR_EACH_SAFE (candidate, list_node, candidate_spbs) {
> +        uint32_t tunnel_key =
> +
> ovn_allocate_tnlid(&candidate->tunnel_key_map->port_tunnel_keys,
> +                               "port", 1, max_pb_tunnel_id,
> +                               &hint);
> +        if (!tunnel_key) {
> +            continue;
> +        }
> +        sbrec_port_binding_set_tunnel_key(candidate->spb->sb_pb,
> +                                              tunnel_key);
> +        ovs_list_remove(&candidate->list_node);
> +        ovs_list_push_back(paired_pbs, &candidate->spb->list_node);
> +        free(candidate);
> +    }
> +
> +}
> +
> +static void
> +free_unpaired_candidates(struct ovs_list *candidate_spbs)
> +{
> +    struct candidate_spb *candidate;
> +    /* Anything from this list represents a port_binding where a tunnel ID
> +     * could not be allocated. Delete the SB port_binding binding for
> these.
> +     */
> +    LIST_FOR_EACH_POP (candidate, list_node, candidate_spbs) {
> +        sbrec_port_binding_delete(candidate->spb->sb_pb);
> +        free(candidate->spb);
> +        free(candidate);
> +    }
> +}
> +
> +static void
> +cleanup_stale_fdb_entries(const struct sbrec_fdb_table *sbrec_fdb_table,
> +                          struct hmap *tunnel_key_maps)
> +{
> +    const struct sbrec_fdb *fdb_e;
> +    SBREC_FDB_TABLE_FOR_EACH_SAFE (fdb_e, sbrec_fdb_table) {
> +        bool delete = true;
> +        struct tunnel_key_map *map = find_tunnel_key_map(fdb_e->dp_key,
> +                                                         tunnel_key_maps);
> +        if (map) {
> +            if (ovn_tnlid_present(&map->port_tunnel_keys,
> fdb_e->port_key)) {
> +                delete = false;
> +            }
> +        }
> +
> +        if (delete) {
> +            sbrec_fdb_delete(fdb_e);
> +        }
> +    }
> +}
> +
> +enum engine_node_state
> +en_port_binding_pair_run(struct engine_node *node , void *data)
> +{
> +    const struct sbrec_port_binding_table *sb_pb_table =
> +        EN_OVSDB_GET(engine_get_input("SB_port_binding", node));
> +    const struct sbrec_fdb_table *sb_fdb_table =
> +        EN_OVSDB_GET(engine_get_input("SB_fdb", node));
> +    const struct ed_type_global_config *global_config =
> +        engine_get_input_data("global_config", node);
> +    /* The inputs are:
> +     * * Some number of input maps.
> +     * * Southbound Port Binding table.
> +     * * Global config data.
> +     * * FDB Table.
> +     *
> +     * Therefore, the number of inputs - 3 is the number of input
> +     * maps from the port_binding-specific nodes.
> +     */
> +    size_t n_input_maps = node->n_inputs - 3;
> +    const struct ovn_unpaired_port_binding_map **input_maps =
> +        xmalloc(n_input_maps *sizeof *input_maps);
> +    struct ovn_paired_port_bindings *paired_port_bindings = data;
>

IMO we should change the approach here too, the advantage is that
we actually have the type already present for port bindings.


> +
> +    for (size_t i = 0; i < n_input_maps; i++) {
> +        input_maps[i] = engine_get_data(node->inputs[i].node);
> +    }
> +
> +    reset_port_binding_pair_data(paired_port_bindings);
> +
> +    struct smap visited = SMAP_INITIALIZER(&visited);
> +    struct ovs_list candidate_spbs =
> OVS_LIST_INITIALIZER(&candidate_spbs);
> +    get_candidate_pbs_from_sb(sb_pb_table, input_maps, n_input_maps,
> +                              &paired_port_bindings->tunnel_key_maps,
> +                              &candidate_spbs, &visited);
> +
> +    const struct engine_context *eng_ctx = engine_get_context();
> +    get_candidate_pbs_from_nb(eng_ctx->ovnsb_idl_txn, input_maps,
> +                              n_input_maps,
> +                              &paired_port_bindings->tunnel_key_maps,
> +                              &candidate_spbs, &visited);
> +
> +    smap_destroy(&visited);
> +
> +    pair_requested_tunnel_keys(&candidate_spbs,
> +                               &paired_port_bindings->paired_pbs);
> +    pair_existing_tunnel_keys(&candidate_spbs,
> +                              &paired_port_bindings->paired_pbs);
> +    pair_new_tunnel_keys(&candidate_spbs,
> &paired_port_bindings->paired_pbs,
> +                         global_config->max_pb_tunnel_id);
> +
> +    cleanup_stale_fdb_entries(sb_fdb_table,
> +                              &paired_port_bindings->tunnel_key_maps);
> +
> +    free_unpaired_candidates(&candidate_spbs);
> +    free(input_maps);
> +
> +    return EN_UPDATED;
> +}
> +
> +void
> +en_port_binding_pair_cleanup(void *data)
> +{
> +    struct ovn_paired_port_bindings *paired_port_bindings = data;
> +    struct ovn_paired_port_binding *spb;
> +
> +    LIST_FOR_EACH_POP (spb, list_node, &paired_port_bindings->paired_pbs)
> {
> +        free(spb);
> +    }
> +    tunnel_key_maps_destroy(&paired_port_bindings->tunnel_key_maps);
> +}
> +
> +enum engine_input_handler_result
> +port_binding_fdb_change_handler(struct engine_node *node, void *data)
> +{
> +    struct ovn_paired_port_bindings *paired_port_bindings = data;
> +    const struct sbrec_fdb_table *sbrec_fdb_table =
> +        EN_OVSDB_GET(engine_get_input("SB_fdb", node));
> +
> +    /* check if changed rows are stale and delete them */
> +    const struct sbrec_fdb *fdb_e, *fdb_prev_del = NULL;
> +    SBREC_FDB_TABLE_FOR_EACH_TRACKED (fdb_e, sbrec_fdb_table) {
> +        if (sbrec_fdb_is_deleted(fdb_e)) {
> +            continue;
> +        }
> +
> +        if (fdb_prev_del) {
> +            sbrec_fdb_delete(fdb_prev_del);
> +        }
> +
> +        fdb_prev_del = fdb_e;
> +        struct tunnel_key_map *tunnel_key_map =
> +            find_tunnel_key_map(fdb_e->dp_key,
> +                                &paired_port_bindings->tunnel_key_maps);
> +        if (tunnel_key_map) {
> +            if (ovn_tnlid_present(&tunnel_key_map->port_tunnel_keys,
> +                                  fdb_e->port_key)) {
> +                fdb_prev_del = NULL;
> +            }
> +        }
> +    }
> +
> +    if (fdb_prev_del) {
> +        sbrec_fdb_delete(fdb_prev_del);
> +    }
> +
> +    return EN_HANDLED_UNCHANGED;
> +}
> diff --git a/northd/en-port-binding-pair.h b/northd/en-port-binding-pair.h
> new file mode 100644
> index 000000000..9b9417487
> --- /dev/null
> +++ b/northd/en-port-binding-pair.h
> @@ -0,0 +1,34 @@
> +/*
> + * Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#ifndef EN_PORT_BINDING_PAIR_H
> +#define EN_PORT_BINDING_PAIR_H
> +
> +#include "inc-proc-eng.h"
> +
> +void *en_port_binding_pair_init(struct engine_node *node,
> +                            struct engine_arg *args);
> +
> +
> +enum engine_node_state en_port_binding_pair_run(struct engine_node *node,
> +                                                void *data);
> +
> +void en_port_binding_pair_cleanup(void *data);
> +
> +enum engine_input_handler_result
> +port_binding_fdb_change_handler(struct engine_node *, void *data);
> +
> +#endif /* EN_PORT_BINDING_PAIR_H */
> diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
> index bdc6c48df..752f1e9dc 100644
> --- a/northd/inc-proc-northd.c
> +++ b/northd/inc-proc-northd.c
> @@ -50,6 +50,11 @@
>  #include "en-datapath-logical-router.h"
>  #include "en-datapath-logical-switch.h"
>  #include "en-datapath-sync.h"
> +#include "en-port-binding-logical-router-port.h"
> +#include "en-port-binding-logical-switch-port.h"
> +#include "en-port-binding-chassisredirect.h"
> +#include "en-port-binding-mirror.h"
> +#include "en-port-binding-pair.h"
>  #include "unixctl.h"
>  #include "util.h"
>
> @@ -187,6 +192,15 @@ static ENGINE_NODE(datapath_logical_switch);
>  static ENGINE_NODE(datapath_synced_logical_router);
>  static ENGINE_NODE(datapath_synced_logical_switch);
>  static ENGINE_NODE(datapath_sync);
> +static ENGINE_NODE(port_binding_logical_router_port);
> +static ENGINE_NODE(port_binding_logical_switch_port);
> +static ENGINE_NODE(port_binding_chassisredirect_port);
> +static ENGINE_NODE(port_binding_mirror);
> +static ENGINE_NODE(port_binding_paired_logical_router_port);
> +static ENGINE_NODE(port_binding_paired_logical_switch_port);
> +static ENGINE_NODE(port_binding_paired_chassisredirect_port);
> +static ENGINE_NODE(port_binding_paired_mirror);
> +static ENGINE_NODE(port_binding_pair);
>
>  void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
>                            struct ovsdb_idl_loop *sb)
> @@ -232,6 +246,36 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
>      engine_add_input(&en_datapath_synced_logical_switch,
> &en_datapath_sync,
>                       NULL);
>
> +    engine_add_input(&en_port_binding_logical_switch_port,
> +                     &en_datapath_synced_logical_switch, NULL);
> +    engine_add_input(&en_port_binding_logical_router_port,
> +                     &en_datapath_synced_logical_router, NULL);
> +    engine_add_input(&en_port_binding_chassisredirect_port,
> +                     &en_datapath_synced_logical_switch, NULL);
> +    engine_add_input(&en_port_binding_chassisredirect_port,
> +                     &en_datapath_synced_logical_router, NULL);
> +    engine_add_input(&en_port_binding_mirror,
> +                     &en_datapath_synced_logical_switch, NULL);
> +    engine_add_input(&en_port_binding_pair,
> +                     &en_port_binding_logical_switch_port, NULL);
> +    engine_add_input(&en_port_binding_pair,
> +                     &en_port_binding_logical_router_port, NULL);
> +    engine_add_input(&en_port_binding_pair,
> +                     &en_port_binding_chassisredirect_port, NULL);
> +    engine_add_input(&en_port_binding_pair, &en_port_binding_mirror,
> NULL);
> +    engine_add_input(&en_port_binding_pair, &en_sb_port_binding, NULL);
> +    engine_add_input(&en_port_binding_pair, &en_global_config, NULL);
> +    engine_add_input(&en_port_binding_pair, &en_sb_fdb,
> +                     port_binding_fdb_change_handler);
> +    engine_add_input(&en_port_binding_paired_logical_router_port,
> +                     &en_port_binding_pair, NULL);
> +    engine_add_input(&en_port_binding_paired_logical_switch_port,
> +                     &en_port_binding_pair, NULL);
> +    engine_add_input(&en_port_binding_paired_chassisredirect_port,
> +                     &en_port_binding_pair, NULL);
> +    engine_add_input(&en_port_binding_paired_mirror,
> &en_port_binding_pair,
> +                     NULL);
> +
>      engine_add_input(&en_northd, &en_nb_mirror, NULL);
>      engine_add_input(&en_northd, &en_nb_mirror_rule, NULL);
>      engine_add_input(&en_northd, &en_nb_static_mac_binding, NULL);
> @@ -247,7 +291,7 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
>      engine_add_input(&en_northd, &en_sb_service_monitor, NULL);
>      engine_add_input(&en_northd, &en_sb_static_mac_binding, NULL);
>      engine_add_input(&en_northd, &en_sb_chassis_template_var, NULL);
> -    engine_add_input(&en_northd, &en_sb_fdb,
> northd_sb_fdb_change_handler);
> +    engine_add_input(&en_northd, &en_sb_fdb, engine_noop_handler);
>      engine_add_input(&en_northd, &en_global_config,
>                       northd_global_config_handler);
>
> @@ -286,6 +330,14 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
>                       engine_noop_handler);
>      engine_add_input(&en_northd, &en_datapath_synced_logical_switch,
>                       engine_noop_handler);
> +    engine_add_input(&en_northd,
> &en_port_binding_paired_logical_router_port,
> +                     engine_noop_handler);
> +    engine_add_input(&en_northd,
> &en_port_binding_paired_logical_switch_port,
> +                     engine_noop_handler);
> +    engine_add_input(&en_northd,
> &en_port_binding_paired_chassisredirect_port,
> +                     engine_noop_handler);
> +    engine_add_input(&en_northd, &en_port_binding_paired_mirror,
> +                     engine_noop_handler);
>
>      engine_add_input(&en_lr_nat, &en_northd, lr_nat_northd_handler);
>
> diff --git a/northd/northd.c b/northd/northd.c
> index 47b54dbd9..51f1c3eb6 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -54,6 +54,10 @@
>  #include "en-sampling-app.h"
>  #include "en-datapath-logical-switch.h"
>  #include "en-datapath-logical-router.h"
> +#include "en-port-binding-logical-switch-port.h"
> +#include "en-port-binding-logical-router-port.h"
> +#include "en-port-binding-chassisredirect.h"
> +#include "en-port-binding-mirror.h"
>  #include "lib/ovn-parallel-hmap.h"
>  #include "ovn/actions.h"
>  #include "ovn/features.h"
> @@ -464,7 +468,7 @@ od_has_lb_vip(const struct ovn_datapath *od)
>      }
>  }
>
> -static const char *
> +const char *
>  ovn_datapath_name(const struct sbrec_datapath_binding *sb)
>  {
>      return smap_get_def(&sb->external_ids, "name", "");
> @@ -495,8 +499,6 @@ ovn_datapath_create(struct hmap *datapaths, const
> struct uuid *key,
>      od->sb = sb;
>      od->nbs = nbs;
>      od->nbr = nbr;
> -    hmap_init(&od->port_tnlids);
> -    od->port_key_hint = 0;
>      hmap_insert(datapaths, &od->key_node, uuid_hash(&od->key));
>      od->lr_group = NULL;
>      hmap_init(&od->ports);
> @@ -527,7 +529,6 @@ ovn_datapath_destroy(struct hmap *datapaths, struct
> ovn_datapath *od)
>           * private list and once we've exited that function it is not
> safe to
>           * use it. */
>          hmap_remove(datapaths, &od->key_node);
> -        ovn_destroy_tnlids(&od->port_tnlids);
>          destroy_ipam_info(&od->ipam_info);
>          vector_destroy(&od->router_ports);
>          vector_destroy(&od->ls_peers);
> @@ -986,6 +987,7 @@ ovn_port_create(struct hmap *ports, const char *key,
>      op->sb = sb;
>      ovn_port_set_nb(op, nbsp, nbrp);
>      op->primary_port = op->cr_port = NULL;
> +    op->tunnel_key = sb->tunnel_key;
>      hmap_insert(ports, &op->key_node, hash_string(op->key, 0));
>
>      op->lflow_ref = lflow_ref_create();
> @@ -997,10 +999,6 @@ ovn_port_create(struct hmap *ports, const char *key,
>  static void
>  ovn_port_cleanup(struct ovn_port *port)
>  {
> -    if (port->tunnel_key) {
> -        ovs_assert(port->od);
> -        ovn_free_tnlid(&port->od->port_tnlids, port->tunnel_key);
> -    }
>      for (int i = 0; i < port->n_lsp_addrs; i++) {
>          destroy_lport_addresses(&port->lsp_addrs[i]);
>      }
> @@ -1077,12 +1075,6 @@ ovn_port_find(const struct hmap *ports, const char
> *name)
>      return ovn_port_find__(ports, name, false);
>  }
>
> -static struct ovn_port *
> -ovn_port_find_bound(const struct hmap *ports, const char *name)
> -{
> -    return ovn_port_find__(ports, name, true);
> -}
> -
>  static bool
>  lsp_is_clone_to_unknown(const struct nbrec_logical_switch_port *nbsp)
>  {
> @@ -1147,7 +1139,7 @@ lsp_disable_arp_nd_rsp(const struct
> nbrec_logical_switch_port *nbsp)
>      return smap_get_bool(&nbsp->options, "disable_arp_nd_rsp", false);
>  }
>
> -static bool
> +bool
>  lsp_is_type_changed(const struct sbrec_port_binding *sb,
>                  const struct nbrec_logical_switch_port *nbsp,
>                  bool *update_sbrec)
> @@ -1883,104 +1875,35 @@ parse_lsp_addrs(struct ovn_port *op)
>      }
>  }
>
> -static void
> -create_mirror_port(struct ovn_port *op, struct hmap *ports,
> -                   struct ovs_list *both_dbs, struct ovs_list *nb_only,
> -                   const struct nbrec_mirror *nb_mirror)
> -{
> -    char *mp_name = ovn_mirror_port_name(ovn_datapath_name(op->od->sb),
> -                                         nb_mirror->sink);
> -    struct ovn_port *mp = ovn_port_find(ports, mp_name);
> -    struct ovn_port *target_port = ovn_port_find(ports, nb_mirror->sink);
> -
> -    if (!target_port) {
> -        goto clear;
> -    }
> -
> -    if (!mp) {
> -        mp = ovn_port_create(ports, mp_name, op->nbsp, NULL, NULL);
> -        ovs_list_push_back(nb_only, &mp->list);
> -    } else if (mp->sb) {
> -        ovn_port_set_nb(mp, op->nbsp, NULL);
> -        ovs_list_remove(&mp->list);
> -        ovs_list_push_back(both_dbs, &mp->list);
> -    } else {
> -        goto clear;
> -    }
> -
> -    mp->mirror_target_port = target_port;
> -    mp->od = op->od;
> +static struct ovn_port *
> +create_mirror_port(const struct ovn_port *source,
> +                   struct ovn_port *sink, const char *mirror_port_name,
> +                   struct hmap *ports,
> +                   const struct sbrec_port_binding *sb_pb)
> +{
> +    struct ovn_port *mp = ovn_port_create(ports, mirror_port_name,
> +                                          source->nbsp, NULL, sb_pb);
> +    ovn_port_set_nb(mp, source->nbsp, NULL);
> +    mp->mirror_target_port = sink;
> +    mp->od = source->od;
>
> -clear:
> -    free(mp_name);
> +    return mp;
>  }
>
>  static struct ovn_port *
>  join_logical_ports_lsp(struct hmap *ports,
> -                       struct ovs_list *nb_only, struct ovs_list *both,
>                         struct ovn_datapath *od,
>                         const struct nbrec_logical_switch_port *nbsp,
> +                       const struct sbrec_port_binding *sb_pb,
>                         const char *name,
>                         unsigned long *queue_id_bitmap,
> -                       struct hmap *tag_alloc_table,
> -                       struct hmapx *mirror_attached_ports)
> -{
> -    struct ovn_port *op = ovn_port_find_bound(ports, name);
> -    if (op && (op->od || op->nbsp || op->nbrp)) {
> -        static struct vlog_rate_limit rl
> -            = VLOG_RATE_LIMIT_INIT(5, 1);
> -        VLOG_WARN_RL(&rl, "duplicate logical port %s", name);
> -        return NULL;
> -    } else if (op && (!op->sb || op->sb->datapath == od->sb)) {
> -        /*
> -         * Handle cases where lport type was explicitly changed
> -         * in the NBDB, in such cases:
> -         * 1. remove the current sbrec of the affected lport from
> -         *    the port_binding table.
> -         *
> -         * 2. create a new sbrec with the same logical_port as the
> -         *    deleted lport and add it to the nb_only list which
> -         *    will make the northd handle this lport as a new
> -         *    created one and recompute everything that is needed
> -         *    for this lport.
> -         *
> -         * This change will affect container/virtual lport type
> -         * changes only for now, this change is needed in
> -         * contaier/virtual lport cases to avoid port type
> -         * conflicts in the ovn-controller when the user clears
> -         * the parent_port field in the container lport or updated
> -         * the lport type.
> -         *
> -         */
> -        bool update_sbrec = false;
> -        if (op->sb && lsp_is_type_changed(op->sb, nbsp,
> -                                          &update_sbrec)
> -                       && update_sbrec) {
> -            ovs_list_remove(&op->list);
> -            sbrec_port_binding_delete(op->sb);
> -            ovn_port_destroy(ports, op);
> -            op = ovn_port_create(ports, name, nbsp,
> -                                 NULL, NULL);
> -            ovs_list_push_back(nb_only, &op->list);
> -        } else {
> -            ovn_port_set_nb(op, nbsp, NULL);
> -            ovs_list_remove(&op->list);
> -
> -            uint32_t queue_id = smap_get_int(&op->sb->options,
> -                                             "qdisc_queue_id", 0);
> -            if (queue_id) {
> -                bitmap_set1(queue_id_bitmap, queue_id);
> -            }
> -
> -            ovs_list_push_back(both, &op->list);
> -
> -            /* This port exists due to a SB binding, but should
> -             * not have been initialized fully. */
> -            ovs_assert(!op->n_lsp_addrs && !op->n_ps_addrs);
> -        }
> -    } else {
> -        op = ovn_port_create(ports, name, nbsp, NULL, NULL);
> -        ovs_list_push_back(nb_only, &op->list);
> +                       struct hmap *tag_alloc_table)
> +{
> +    struct ovn_port *op = ovn_port_create(ports, name, nbsp, NULL, sb_pb);
> +    uint32_t queue_id = smap_get_int(&op->sb->options,
> +                                     "qdisc_queue_id", 0);
> +    if (queue_id) {
> +        bitmap_set1(queue_id_bitmap, queue_id);
>      }
>
>      if (lsp_is_localnet(nbsp)) {
> @@ -2000,47 +1923,23 @@ join_logical_ports_lsp(struct hmap *ports,
>      hmap_insert(&od->ports, &op->dp_node,
>                  hmap_node_hash(&op->key_node));
>
> -    if (nbsp->n_mirror_rules) {
> -        hmapx_add(mirror_attached_ports, op);
> -    }
> -
>      tag_alloc_add_existing_tags(tag_alloc_table, nbsp);
>      return op;
>  }
>
>  static struct ovn_port*
>  join_logical_ports_lrp(struct hmap *ports,
> -                       struct ovs_list *nb_only, struct ovs_list *both,
>                         struct hmapx *dgps,
>                         struct ovn_datapath *od,
>                         const struct nbrec_logical_router_port *nbrp,
> +                       const struct sbrec_port_binding *sb_pb,
>                         const char *name, struct lport_addresses
> *lrp_networks)
>  {
>      if (!lrp_networks->n_ipv4_addrs && !lrp_networks->n_ipv6_addrs) {
>        return NULL;
>      }
>
> -    struct ovn_port *op = ovn_port_find_bound(ports, name);
> -    if (op && (op->od || op->nbsp || op->nbrp)) {
> -        static struct vlog_rate_limit rl
> -            = VLOG_RATE_LIMIT_INIT(5, 1);
> -        VLOG_WARN_RL(&rl, "duplicate logical router port %s",
> -                     name);
> -        destroy_lport_addresses(lrp_networks);
> -        return NULL;
> -    } else if (op && (!op->sb || op->sb->datapath == od->sb)) {
> -        ovn_port_set_nb(op, NULL, nbrp);
> -        ovs_list_remove(&op->list);
> -        ovs_list_push_back(both, &op->list);
> -
> -        /* This port exists but should not have been
> -         * initialized fully. */
> -        ovs_assert(!op->lrp_networks.n_ipv4_addrs
> -                   && !op->lrp_networks.n_ipv6_addrs);
> -    } else {
> -        op = ovn_port_create(ports, name, NULL, nbrp, NULL);
> -        ovs_list_push_back(nb_only, &op->list);
> -    }
> +    struct ovn_port *op = ovn_port_create(ports, name, NULL, nbrp, sb_pb);
>
>      op->lrp_networks = *lrp_networks;
>      op->od = od;
> @@ -2094,128 +1993,126 @@ join_logical_ports_lrp(struct hmap *ports,
>
>
>  static struct ovn_port *
> -create_cr_port(struct ovn_port *op, struct hmap *ports,
> -               struct ovs_list *both_dbs, struct ovs_list *nb_only)
> +create_cr_port(struct ovn_port *op, const char *name, struct hmap *ports,
> +               const struct sbrec_port_binding *sb_pb)
>  {
> -    char *redirect_name = ovn_chassis_redirect_name(
> -        op->nbsp ? op->nbsp->name : op->nbrp->name);
> -
> -    struct ovn_port *crp = ovn_port_find(ports, redirect_name);
> -    if (crp && crp->sb && crp->sb->datapath == op->od->sb) {
> -        ovn_port_set_nb(crp, op->nbsp, op->nbrp);
> -        ovs_list_remove(&crp->list);
> -        ovs_list_push_back(both_dbs, &crp->list);
> -    } else {
> -        crp = ovn_port_create(ports, redirect_name,
> -                              op->nbsp, op->nbrp, NULL);
> -        ovs_list_push_back(nb_only, &crp->list);
> -    }
> +    struct ovn_port *crp = ovn_port_create(ports, name, op->nbsp,
> op->nbrp,
> +                                           sb_pb);
>
>      crp->primary_port = op;
>      op->cr_port = crp;
>      crp->od = op->od;
> -    free(redirect_name);
>
>      return crp;
>  }
>
> -/* Returns true if chassis resident port needs to be created for
> - * op's peer logical switch.  False otherwise.
> - *
> - * Chassis resident port needs to be created if the following
> - * conditionsd are met:
> - *   - op is a distributed gateway port
> - *   - op is the only distributed gateway port attached to its
> - *     router
> - *   - op's peer logical switch has no localnet ports.
> - */
> -static bool
> -peer_needs_cr_port_creation(struct ovn_port *op)
> -{
> -    if ((op->nbrp->n_gateway_chassis || op->nbrp->ha_chassis_group)
> -        && vector_len(&op->od->l3dgw_ports) == 1 && op->peer &&
> op->peer->nbsp
> -        && vector_is_empty(&op->peer->od->localnet_ports)) {
> -        return true;
> -    }
> -
> -    return false;
> -}
> -
>  static void
> -join_mirror_ports(struct ovn_port *op,
> -                  const struct nbrec_logical_switch_port *nbsp,
> -                  struct hmap *ports, struct ovs_list *both,
> -                  struct ovs_list *nb_only)
> +join_logical_ports(
> +    struct hmap *ls_datapaths, struct hmap *lr_datapaths,
> +    const struct ovn_paired_logical_switch_port_map *paired_lsps,
> +    const struct ovn_paired_logical_router_port_map *paired_lrps,
> +    const struct ovn_paired_chassisredirect_port_map *paired_crps,
> +    const struct ovn_paired_mirror_map *paired_mirrors,
> +    struct hmap *ls_ports, struct hmap *lr_ports,
> +    unsigned long *queue_id_bitmap,
> +    struct hmap *tag_alloc_table)
>  {
> -    /* Create mirror targets port bindings if there any mirror
> -     * with lport type attached to this port. */
> -    for (size_t j = 0; j < op->nbsp->n_mirror_rules; j++) {
> -        struct nbrec_mirror *mirror = nbsp->mirror_rules[j];
> -        if (!strcmp(mirror->type, "lport")) {
> -            create_mirror_port(op, ports, both, nb_only, mirror);
> +    struct ovn_datapath *od;
> +    struct hmapx dgps = HMAPX_INITIALIZER(&dgps);
> +
> +    struct shash_node *node;
> +    SHASH_FOR_EACH (node, &paired_lrps->paired_router_ports) {
> +        struct ovn_paired_logical_router_port *slrp = node->data;
> +        od = ovn_datapath_from_sbrec(ls_datapaths, lr_datapaths,
> +                                     slrp->router->sb);
> +        if (!od) {
> +            /* This can happen if the router is not enabled */
> +            continue;
>          }
> +        struct lport_addresses lrp_networks;
> +        if (!extract_lrp_networks(slrp->nb, &lrp_networks)) {
> +            static struct vlog_rate_limit rl
> +                = VLOG_RATE_LIMIT_INIT(5, 1);
> +            VLOG_WARN_RL(&rl, "bad 'mac' %s", slrp->nb->mac);
> +            continue;
> +        }
> +
> +        join_logical_ports_lrp(lr_ports, &dgps, od, slrp->nb, slrp->sb,
> +                               slrp->nb->name, &lrp_networks);
>      }
> -}
>
> -static void
> -join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table,
> -                   struct hmap *ls_datapaths, struct hmap *lr_datapaths,
> -                   struct hmap *ports, unsigned long *queue_id_bitmap,
> -                   struct hmap *tag_alloc_table, struct ovs_list *sb_only,
> -                   struct ovs_list *nb_only, struct ovs_list *both)
> -{
> -    ovs_list_init(sb_only);
> -    ovs_list_init(nb_only);
> -    ovs_list_init(both);
> +    SHASH_FOR_EACH (node, &paired_lsps->paired_switch_ports) {
> +        struct ovn_paired_logical_switch_port *slsp = node->data;
> +        od = ovn_datapath_from_sbrec(ls_datapaths, lr_datapaths,
> +                                     slsp->sw->sb);
> +        if (!od) {
> +            /* This should not happen, but we'll be defensive just in
> case */
> +            continue;
> +        }
> +        join_logical_ports_lsp(ls_ports, od, slsp->nb, slsp->sb,
> +                               slsp->nb->name, queue_id_bitmap,
> +                               tag_alloc_table);
> +    }
>
> -    const struct sbrec_port_binding *sb;
> -    SBREC_PORT_BINDING_TABLE_FOR_EACH (sb, sbrec_pb_table) {
> -        struct ovn_port *op = ovn_port_create(ports, sb->logical_port,
> -                                              NULL, NULL, sb);
> -        ovs_list_push_back(sb_only, &op->list);
> +    SHASH_FOR_EACH (node,
> &paired_crps->paired_chassisredirect_router_ports) {
> +        struct ovn_paired_chassisredirect_router_port *crp = node->data;
> +        struct ovn_port *primary_port =
> +            ovn_port_find(lr_ports, crp->primary_port->name);
> +        create_cr_port(primary_port, crp->name, lr_ports, crp->sb);
>      }
>
> -    struct ovn_datapath *od;
> -    struct hmapx dgps = HMAPX_INITIALIZER(&dgps);
> -    struct hmapx mirror_attached_ports =
> -                    HMAPX_INITIALIZER(&mirror_attached_ports);
> -    HMAP_FOR_EACH (od, key_node, lr_datapaths) {
> -        ovs_assert(od->nbr);
> -        for (size_t i = 0; i < od->nbr->n_ports; i++) {
> -            const struct nbrec_logical_router_port *nbrp
> -                = od->nbr->ports[i];
> -
> -            struct lport_addresses lrp_networks;
> -            if (!extract_lrp_networks(nbrp, &lrp_networks)) {
> -                static struct vlog_rate_limit rl
> -                    = VLOG_RATE_LIMIT_INIT(5, 1);
> -                VLOG_WARN_RL(&rl, "bad 'mac' %s", nbrp->mac);
> -                continue;
> -            }
> -            join_logical_ports_lrp(ports, nb_only, both, &dgps,
> -                                   od, nbrp,
> -                                   nbrp->name, &lrp_networks);
> -        }
> +    SHASH_FOR_EACH (node,
> &paired_crps->paired_chassisredirect_switch_ports) {
> +        struct ovn_paired_chassisredirect_switch_port *crp = node->data;
> +        struct ovn_port *primary_port =
> +            ovn_port_find(ls_ports, crp->primary_port->name);
> +        create_cr_port(primary_port, crp->name, ls_ports, crp->sb);
>      }
>
> -    HMAP_FOR_EACH (od, key_node, ls_datapaths) {
> -        ovs_assert(od->nbs);
> -        for (size_t i = 0; i < od->nbs->n_ports; i++) {
> -            const struct nbrec_logical_switch_port *nbsp
> -                = od->nbs->ports[i];
> -            join_logical_ports_lsp(ports, nb_only, both, od, nbsp,
> -                                   nbsp->name, queue_id_bitmap,
> -                                   tag_alloc_table,
> &mirror_attached_ports);
> +    SHASH_FOR_EACH (node, &paired_mirrors->paired_mirror_ports) {
> +        struct ovn_paired_mirror *mirror = node->data;
> +        struct ovn_port *source_port =
> +            ovn_port_find(ls_ports, mirror->nbsp->name);
> +        struct ovn_port *sink_port =
> +            ovn_port_find(ls_ports, mirror->sink);
> +        if (!sink_port) {
> +            continue;
>          }
> +        create_mirror_port(source_port, sink_port, mirror->name, ls_ports,
> +                           mirror->sb);
>      }
>
>      /* Connect logical router ports, and logical switch ports of type
> "router",
>       * to their peers.  As well as logical switch ports of type "switch"
> to
>       * theirs. */
> +
>      struct ovn_port *op;
> -    HMAP_FOR_EACH (op, key_node, ports) {
> -        if (op->nbsp && lsp_is_router(op->nbsp) && !op->primary_port) {
> -            struct ovn_port *peer = ovn_port_get_peer(ports, op);
> +    HMAP_FOR_EACH (op, key_node, lr_ports) {
> +        if (op->nbrp->peer && !is_cr_port(op)) {
> +            struct ovn_port *peer = ovn_port_find(lr_ports,
> op->nbrp->peer);
> +            if (peer) {
> +                if (peer->nbrp && peer->nbrp->peer &&
> +                        !strcmp(op->nbrp->name, peer->nbrp->peer)) {
> +                    /* We only configure LRP peers if each LRP has the
> other as
> +                     * its peer. */
> +                    op->peer = peer;
> +                } else if (peer->nbsp) {
> +                    /* An ovn_port for a switch port of type "router"
> does have
> +                     * a router port as its peer (see the case above for
> +                     * "router" ports), but this is set via
> options:router-port
> +                     * in Logical_Switch_Port and does not involve the
> +                     * Logical_Router_Port's 'peer' column. */
> +                    static struct vlog_rate_limit rl =
> +                            VLOG_RATE_LIMIT_INIT(5, 1);
> +                    VLOG_WARN_RL(&rl, "Bad configuration: The peer of
> router "
> +                                 "port %s is a switch port", op->key);
> +                }
> +            }
> +        }
> +    }
> +
> +    HMAP_FOR_EACH (op, key_node, ls_ports) {
> +        if (lsp_is_router(op->nbsp) && !op->primary_port) {
> +            struct ovn_port *peer = ovn_port_get_peer(lr_ports, op);
>              if (!peer || !peer->nbrp) {
>                  continue;
>              }
> @@ -2271,21 +2168,21 @@ join_logical_ports(const struct
> sbrec_port_binding_table *sbrec_pb_table,
>                          arp_proxy, op->nbsp->name);
>                  }
>              }
> -        } else if (op->nbsp && op->nbsp->peer && lsp_is_switch(op->nbsp))
> {
> +        } else if (op->nbsp->peer && lsp_is_switch(op->nbsp)) {
>              static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
> -            struct ovn_port *peer = ovn_port_find(ports, op->nbsp->peer);
> +            struct ovn_port *peer = ovn_port_find(ls_ports,
> op->nbsp->peer);
>
>              if (!peer) {
>                  continue;
>              }
>
> -            if (peer->nbrp || (peer->nbsp && lsp_is_router(peer->nbsp))) {
> +            if (lsp_is_router(peer->nbsp)) {
>                  VLOG_WARN_RL(&rl, "Bad configuration: The peer of switch "
>                                    "port %s is a router port", op->key);
>                  continue;
>              }
>
> -            if (!peer->nbsp || !lsp_is_switch(peer->nbsp)) {
> +            if (!lsp_is_switch(peer->nbsp)) {
>                  /* Common case.  Likely the manual configuration is not
>                   * finished yet. */
>                  continue;
> @@ -2300,26 +2197,6 @@ join_logical_ports(const struct
> sbrec_port_binding_table *sbrec_pb_table,
>              }
>
>              op->peer = peer;
> -        } else if (op->nbrp && op->nbrp->peer && !is_cr_port(op)) {
> -            struct ovn_port *peer = ovn_port_find(ports, op->nbrp->peer);
> -            if (peer) {
> -                if (peer->nbrp && peer->nbrp->peer &&
> -                        !strcmp(op->nbrp->name, peer->nbrp->peer)) {
> -                    /* We only configure LRP peers if each LRP has the
> other as
> -                     * its peer. */
> -                    op->peer = peer;
> -                } else if (peer->nbsp) {
> -                    /* An ovn_port for a switch port of type "router"
> does have
> -                     * a router port as its peer (see the case above for
> -                     * "router" ports), but this is set via
> options:router-port
> -                     * in Logical_Switch_Port and does not involve the
> -                     * Logical_Router_Port's 'peer' column. */
> -                    static struct vlog_rate_limit rl =
> -                            VLOG_RATE_LIMIT_INIT(5, 1);
> -                    VLOG_WARN_RL(&rl, "Bad configuration: The peer of
> router "
> -                                 "port %s is a switch port", op->key);
> -                }
> -            }
>          }
>      }
>
> @@ -2330,11 +2207,6 @@ join_logical_ports(const struct
> sbrec_port_binding_table *sbrec_pb_table,
>          ovs_assert(op->nbrp);
>          ovs_assert(op->nbrp->ha_chassis_group ||
> op->nbrp->n_gateway_chassis);
>
> -        /* Additional "derived" ovn_port crp represents the instance of
> op on
> -         * the gateway chassis. */
> -        struct ovn_port *crp = create_cr_port(op, ports, both, nb_only);
> -        ovs_assert(crp);
> -
>          /* Add to l3dgw_ports in od, for later use during flow creation.
> */
>          vector_push(&od->l3dgw_ports, &op);
>
> @@ -2345,41 +2217,16 @@ join_logical_ports(const struct
> sbrec_port_binding_table *sbrec_pb_table,
>          }
>      }
>
> -
> -    /* Create chassisredirect port for the distributed gateway port's
> (DGP)
> -     * peer if
> -     *  - DGP's router has only one DGP and
> -     *  - Its peer is a logical switch port and
> -     *  - Its peer's logical switch has no localnet ports
> -     *
> -     * This is required to support
> -     *   - NAT via geneve (for the overlay provider networks) and
> -     *   - to centralize routing on the gateway chassis for the traffic
> -     *     destined to the DGP's networks.
> -     *
> -     * Future enhancement: Support 'centralizerouting' for all the DGP's
> -     * of a logical router.
> -     * */
> -    HMAPX_FOR_EACH (hmapx_node, &dgps) {
> -        op = hmapx_node->data;
> -        if (peer_needs_cr_port_creation(op)) {
> -            create_cr_port(op->peer, ports, both, nb_only);
> -        }
> -    }
>      hmapx_destroy(&dgps);
>
> -    HMAPX_FOR_EACH (hmapx_node, &mirror_attached_ports) {
> -        op = hmapx_node->data;
> -        if (op && op->nbsp) {
> -            join_mirror_ports(op, op->nbsp, ports, both, nb_only);
> -        }
> -    }
> -    hmapx_destroy(&mirror_attached_ports);
> -
>      /* Wait until all ports have been connected to add to IPAM since
>       * it relies on proper peers to be set
>       */
> -    HMAP_FOR_EACH (op, key_node, ports) {
> +    HMAP_FOR_EACH (op, key_node, ls_ports) {
> +        ipam_add_port_addresses(op->od, op);
> +    }
> +
> +    HMAP_FOR_EACH (op, key_node, lr_ports) {
>          ipam_add_port_addresses(op->od, op);
>      }
>  }
> @@ -2783,15 +2630,6 @@ copy_gw_chassis_from_nbrp_to_sbpb(
>      free(sb_ha_chassis);
>  }
>
> -static const char*
> -op_get_name(const struct ovn_port *op)
> -{
> -    ovs_assert(op->nbsp || op->nbrp);
> -    const char *name = op->nbsp ? op->nbsp->name
> -                                : op->nbrp->name;
> -    return name;
> -}
> -
>  static void
>  ovn_update_ipv6_prefix(struct hmap *lr_ports)
>  {
> @@ -3052,8 +2890,6 @@ ovn_port_update_sbrec(struct ovsdb_idl_txn
> *ovnsb_txn,
>          const char *addresses = ds_cstr(&s);
>          sbrec_port_binding_set_mac(op->sb, &addresses, 1);
>          ds_destroy(&s);
> -
> -        sbrec_port_binding_set_external_ids(op->sb,
> &op->nbrp->external_ids);
>      } else {
>          if (op->mirror_target_port) {
>              /* In case of using a lport mirror, we establish a port
> binding
> @@ -3262,15 +3098,6 @@ ovn_port_update_sbrec(struct ovsdb_idl_txn
> *ovnsb_txn,
>              op->sb, (const char **) op->nbsp->port_security,
>              op->nbsp->n_port_security);
>
> -        struct smap ids = SMAP_INITIALIZER(&ids);
> -        smap_clone(&ids, &op->nbsp->external_ids);
> -        const char *name = smap_get(&ids, "neutron:port_name");
> -        if (name && name[0]) {
> -            smap_add(&ids, "name", name);
> -        }
> -        sbrec_port_binding_set_external_ids(op->sb, &ids);
> -        smap_destroy(&ids);
> -
>          if (!op->nbsp->n_mirror_rules) {
>              /* Nothing is set. Clear mirror_rules from pb. */
>              sbrec_port_binding_set_mirror_rules(op->sb, NULL, 0);
> @@ -3328,27 +3155,6 @@ cleanup_sb_ha_chassis_groups(
>      }
>  }
>
> -static void
> -cleanup_stale_fdb_entries(const struct sbrec_fdb_table *sbrec_fdb_table,
> -                          struct hmap *ls_datapaths)
> -{
> -    const struct sbrec_fdb *fdb_e;
> -    SBREC_FDB_TABLE_FOR_EACH_SAFE (fdb_e, sbrec_fdb_table) {
> -        bool delete = true;
> -        struct ovn_datapath *od
> -            = ovn_datapath_find_by_key(ls_datapaths, fdb_e->dp_key);
> -        if (od) {
> -            if (ovn_tnlid_present(&od->port_tnlids, fdb_e->port_key)) {
> -                delete = false;
> -            }
> -        }
> -
> -        if (delete) {
> -            sbrec_fdb_delete(fdb_e);
> -        }
> -    }
> -}
> -
>  static void
>  delete_fdb_entries(struct ovsdb_idl_index *sbrec_fdb_by_dp_and_port,
>                   uint32_t dp_key, uint32_t port_key)
> @@ -4122,64 +3928,6 @@ sync_pbs_for_northd_changed_ovn_ports(
>      return true;
>  }
>
> -static bool
> -ovn_port_add_tnlid(struct ovn_port *op, uint32_t tunnel_key)
> -{
> -    bool added = ovn_add_tnlid(&op->od->port_tnlids, tunnel_key);
> -    if (added) {
> -        op->tunnel_key = tunnel_key;
> -        if (tunnel_key > op->od->port_key_hint) {
> -            op->od->port_key_hint = tunnel_key;
> -        }
> -    }
> -    return added;
> -}
> -
> -/* Returns false if the requested key is confict with another allocated
> key, so
> - * that the I-P engine can fallback to recompute if needed; otherwise
> return
> - * true (even if the key is not allocated). */
> -static bool
> -ovn_port_assign_requested_tnl_id(struct ovn_port *op)
> -{
> -    const struct smap *options = (op->nbsp
> -                                  ? &op->nbsp->options
> -                                  : &op->nbrp->options);
> -    uint32_t tunnel_key = smap_get_int(options, "requested-tnl-key", 0);
> -    if (tunnel_key) {
> -        if (vxlan_mode && tunnel_key >= OVN_VXLAN_MIN_MULTICAST) {
> -            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> -            VLOG_WARN_RL(&rl, "Tunnel key %"PRIu32" for port %s "
> -                         "is incompatible with VXLAN",
> -                         tunnel_key, op_get_name(op));
> -            return true;
> -        }
> -        if (!ovn_port_add_tnlid(op, tunnel_key)) {
> -            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> -            VLOG_WARN_RL(&rl, "Logical %s port %s requests same tunnel
> key "
> -                         "%"PRIu32" as another LSP or LRP",
> -                         op->nbsp ? "switch" : "router",
> -                         op_get_name(op), tunnel_key);
> -            return false;
> -        }
> -    }
> -    return true;
> -}
> -
> -static bool
> -ovn_port_allocate_key(struct ovn_port *op)
> -{
> -    if (!op->tunnel_key) {
> -        uint8_t key_bits = vxlan_mode ? 12 : 16;
> -        op->tunnel_key = ovn_allocate_tnlid(&op->od->port_tnlids, "port",
> -                                            1, (1u << (key_bits - 1)) - 1,
> -                                            &op->od->port_key_hint);
> -        if (!op->tunnel_key) {
> -            return false;
> -        }
> -    }
> -    return true;
> -}
> -
>  /* Updates the southbound Port_Binding table so that it contains the
> logical
>   * switch ports specified by the northbound database.
>   *
> @@ -4188,17 +3936,19 @@ ovn_port_allocate_key(struct ovn_port *op)
>   * datapaths. */
>  static void
>  build_ports(struct ovsdb_idl_txn *ovnsb_txn,
> -    const struct sbrec_port_binding_table *sbrec_port_binding_table,
>      const struct sbrec_mirror_table *sbrec_mirror_table,
>      const struct sbrec_mac_binding_table *sbrec_mac_binding_table,
>      const struct sbrec_ha_chassis_group_table
> *sbrec_ha_chassis_group_table,
>      struct ovsdb_idl_index *sbrec_chassis_by_name,
>      struct ovsdb_idl_index *sbrec_chassis_by_hostname,
>      struct ovsdb_idl_index *sbrec_ha_chassis_grp_by_name,
> +    const struct ovn_paired_logical_switch_port_map *paired_lsps,
> +    const struct ovn_paired_logical_router_port_map *paired_lrps,
> +    const struct ovn_paired_chassisredirect_port_map *paired_crps,
> +    const struct ovn_paired_mirror_map *paired_mirrors,
>      struct hmap *ls_datapaths, struct hmap *lr_datapaths,
>      struct hmap *ls_ports, struct hmap *lr_ports)
>  {
> -    struct ovs_list sb_only, nb_only, both;
>      /* XXX: Add tag_alloc_table and queue_id_bitmap as part of northd_data
>       * to improve I-P. */
>      struct hmap tag_alloc_table = HMAP_INITIALIZER(&tag_alloc_table);
> @@ -4209,107 +3959,37 @@ build_ports(struct ovsdb_idl_txn *ovnsb_txn,
>      struct sset active_ha_chassis_grps =
>          SSET_INITIALIZER(&active_ha_chassis_grps);
>
> -    /* Borrow ls_ports for joining NB and SB for both LSPs and LRPs.
> -     * We will split them later. */
> -    struct hmap *ports = ls_ports;
> -    join_logical_ports(sbrec_port_binding_table, ls_datapaths,
> lr_datapaths,
> -                       ports, queue_id_bitmap,
> -                       &tag_alloc_table, &sb_only, &nb_only, &both);
> +    join_logical_ports(ls_datapaths, lr_datapaths,
> +                       paired_lsps, paired_lrps, paired_crps,
> +                       paired_mirrors, ls_ports, lr_ports,
> queue_id_bitmap,
> +                       &tag_alloc_table);
>
> -    /* Purge stale Mac_Bindings if ports are deleted. */
> -    bool remove_mac_bindings = !ovs_list_is_empty(&sb_only);
> -
> -    /* Assign explicitly requested tunnel ids first. */
>      struct ovn_port *op;
> -    LIST_FOR_EACH (op, list, &both) {
> -        ovn_port_assign_requested_tnl_id(op);
> -    }
> -    LIST_FOR_EACH (op, list, &nb_only) {
> -        ovn_port_assign_requested_tnl_id(op);
> -    }
> -
> -    /* Keep nonconflicting tunnel IDs that are already assigned. */
> -    LIST_FOR_EACH (op, list, &both) {
> -        if (!op->tunnel_key) {
> -            ovn_port_add_tnlid(op, op->sb->tunnel_key);
> -        }
> -    }
> -
> -    /* Assign new tunnel ids where needed. */
> -    LIST_FOR_EACH_SAFE (op, list, &both) {
> -        if (!ovn_port_allocate_key(op)) {
> -            sbrec_port_binding_delete(op->sb);
> -            ovs_list_remove(&op->list);
> -            ovn_port_destroy(ports, op);
> -        }
> -    }
> -    LIST_FOR_EACH_SAFE (op, list, &nb_only) {
> -        if (!ovn_port_allocate_key(op)) {
> -            ovs_list_remove(&op->list);
> -            ovn_port_destroy(ports, op);
> -        }
> -    }
> -
> -    /* For logical ports that are in both databases, update the southbound
> -     * record based on northbound data.
> -     * For logical ports that are in NB database, do any tag allocation
> -     * needed. */
> -    LIST_FOR_EACH_SAFE (op, list, &both) {
> -        /* When reusing stale Port_Bindings, make sure that stale
> -         * Mac_Bindings are purged.
> -         */
> -        if (op->od->sb != op->sb->datapath) {
> -            remove_mac_bindings = true;
> -        }
> -        if (op->nbsp) {
> -            tag_alloc_create_new_tag(&tag_alloc_table, op->nbsp);
> -        }
> +    /* For logical ports, update the southbound record based on northbound
> +     * data.
> +     * For logical switch ports, do any tag allocation needed.
> +     */
> +    HMAP_FOR_EACH (op, key_node, ls_ports) {
> +        tag_alloc_create_new_tag(&tag_alloc_table, op->nbsp);
>          ovn_port_update_sbrec(ovnsb_txn, sbrec_chassis_by_name,
>                                sbrec_chassis_by_hostname,
>                                sbrec_ha_chassis_grp_by_name,
>                                sbrec_mirror_table,
>                                op, queue_id_bitmap,
>                                &active_ha_chassis_grps);
> -        op->od->is_transit_router |= is_transit_router_port(op);
> -        ovs_list_remove(&op->list);
>      }
>
> -    /* Add southbound record for each unmatched northbound record. */
> -    LIST_FOR_EACH_SAFE (op, list, &nb_only) {
> -        op->sb = sbrec_port_binding_insert(ovnsb_txn);
> +    HMAP_FOR_EACH (op, key_node, lr_ports) {
>          ovn_port_update_sbrec(ovnsb_txn, sbrec_chassis_by_name,
>                                sbrec_chassis_by_hostname,
>                                sbrec_ha_chassis_grp_by_name,
>                                sbrec_mirror_table,
>                                op, queue_id_bitmap,
>                                &active_ha_chassis_grps);
> -        sbrec_port_binding_set_logical_port(op->sb, op->key);
>          op->od->is_transit_router |= is_transit_router_port(op);
> -        ovs_list_remove(&op->list);
> -    }
> -
> -    /* Delete southbound records without northbound matches. */
> -    if (!ovs_list_is_empty(&sb_only)) {
> -        LIST_FOR_EACH_SAFE (op, list, &sb_only) {
> -            ovs_list_remove(&op->list);
> -            sbrec_port_binding_delete(op->sb);
> -            ovn_port_destroy(ports, op);
> -        }
>      }
>
> -    /* Move logical router ports to lr_ports, and logical switch ports
> will
> -     * remain in ports/ls_ports. */
> -    HMAP_FOR_EACH_SAFE (op, key_node, ports) {
> -        if (!op->nbrp) {
> -            continue;
> -        }
> -        hmap_remove(ports, &op->key_node);
> -        hmap_insert(lr_ports, &op->key_node, op->key_node.hash);
> -    }
> -
> -    if (remove_mac_bindings) {
> -        cleanup_mac_bindings(sbrec_mac_binding_table, lr_datapaths,
> lr_ports);
> -    }
> +    cleanup_mac_bindings(sbrec_mac_binding_table, lr_datapaths, lr_ports);
>
>      tag_alloc_destroy(&tag_alloc_table);
>      bitmap_free(queue_id_bitmap);
> @@ -4453,67 +4133,39 @@ ovn_port_find_in_datapath(struct ovn_datapath *od,
>      return NULL;
>  }
>
> -static bool
> +static void
>  ls_port_init(struct ovn_port *op, struct ovsdb_idl_txn *ovnsb_txn,
>               struct ovn_datapath *od,
> -             const struct sbrec_port_binding *sb,
>               const struct sbrec_mirror_table *sbrec_mirror_table,
>               struct ovsdb_idl_index *sbrec_chassis_by_name,
>               struct ovsdb_idl_index *sbrec_chassis_by_hostname)
>  {
>      op->od = od;
>      parse_lsp_addrs(op);
> -    /* Assign explicitly requested tunnel ids first. */
> -    if (!ovn_port_assign_requested_tnl_id(op)) {
> -        return false;
> -    }
> -    /* Keep nonconflicting tunnel IDs that are already assigned. */
> -    if (sb) {
> -        if (!op->tunnel_key) {
> -            ovn_port_add_tnlid(op, sb->tunnel_key);
> -        }
> -    }
> -    /* Assign new tunnel ids where needed. */
> -    if (!ovn_port_allocate_key(op)) {
> -        return false;
> -    }
> -    /* Create new binding, if needed. */
> -    if (sb) {
> -        op->sb = sb;
> -    } else {
> -        /* XXX: the new SB port_binding will change in IDL, so need to
> handle
> -         * SB port_binding updates incrementally to achieve end-to-end
> -         * incremental processing. */
> -        op->sb = sbrec_port_binding_insert(ovnsb_txn);
> -        sbrec_port_binding_set_logical_port(op->sb, op->key);
> -    }
>      ovn_port_update_sbrec(ovnsb_txn, sbrec_chassis_by_name,
>                            sbrec_chassis_by_hostname, NULL,
> sbrec_mirror_table,
>                            op, NULL, NULL);
> -    return true;
>  }
>
>  static struct ovn_port *
>  ls_port_create(struct ovsdb_idl_txn *ovnsb_txn, struct hmap *ls_ports,
>                 const char *key, const struct nbrec_logical_switch_port
> *nbsp,
>                 struct ovn_datapath *od,
> +               const struct sbrec_port_binding *sb,
>                 const struct sbrec_mirror_table *sbrec_mirror_table,
>                 struct ovsdb_idl_index *sbrec_chassis_by_name,
>                 struct ovsdb_idl_index *sbrec_chassis_by_hostname)
>  {
>      struct ovn_port *op = ovn_port_create(ls_ports, key, nbsp, NULL,
> -                                          NULL);
> +                                          sb);
>      hmap_insert(&od->ports, &op->dp_node, hmap_node_hash(&op->key_node));
> -    if (!ls_port_init(op, ovnsb_txn, od, NULL, sbrec_mirror_table,
> -                      sbrec_chassis_by_name, sbrec_chassis_by_hostname)) {
> -        ovn_port_destroy(ls_ports, op);
> -        return NULL;
> -    }
> +    ls_port_init(op, ovnsb_txn, od, sbrec_mirror_table,
> +                 sbrec_chassis_by_name, sbrec_chassis_by_hostname);
>
>      return op;
>  }
>
> -static bool
> +static void
>  ls_port_reinit(struct ovn_port *op, struct ovsdb_idl_txn *ovnsb_txn,
>                  const struct nbrec_logical_switch_port *nbsp,
>                  struct ovn_datapath *od,
> @@ -4524,10 +4176,11 @@ ls_port_reinit(struct ovn_port *op, struct
> ovsdb_idl_txn *ovnsb_txn,
>  {
>      ovn_port_cleanup(op);
>      op->sb = sb;
> +    op->tunnel_key = sb->tunnel_key;
>      ovn_port_set_nb(op, nbsp, NULL);
>      op->primary_port = op->cr_port = NULL;
> -    return ls_port_init(op, ovnsb_txn, od, sb, sbrec_mirror_table,
> -                        sbrec_chassis_by_name, sbrec_chassis_by_hostname);
> +    ls_port_init(op, ovnsb_txn, od, sbrec_mirror_table,
> +                 sbrec_chassis_by_name, sbrec_chassis_by_hostname);
>  }
>
>  /* Returns true if the logical switch has changes which can be
> @@ -4544,6 +4197,7 @@ ls_changes_can_be_handled(
>  {
>      /* Check if the columns are changed in this row. */
>      enum nbrec_logical_switch_column_id col;
> +
>      for (col = 0; col < NBREC_LOGICAL_SWITCH_N_COLUMNS; col++) {
>          if (nbrec_logical_switch_is_updated(ls, col)) {
>              if (col == NBREC_LOGICAL_SWITCH_COL_ACLS ||
> @@ -4693,15 +4347,18 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn
> *ovnsb_idl_txn,
>
>      /* Compare the individual ports in the old and new Logical Switches */
>      for (size_t j = 0; j < changed_ls->n_ports; ++j) {
> -        struct nbrec_logical_switch_port *new_nbsp = changed_ls->ports[j];
> -        op = ovn_port_find_in_datapath(od, new_nbsp);
> +        const struct ovn_paired_logical_switch_port *paired_lsp =
> +            shash_find_data(&ni->paired_lsps->paired_switch_ports,
> +                            changed_ls->ports[j]->name);
> +        op = ovn_port_find_in_datapath(od, paired_lsp->nb);
>
>          if (!op) {
> -            if (!lsp_can_be_inc_processed(new_nbsp)) {
> +            if (!lsp_can_be_inc_processed(paired_lsp->nb)) {
>                  goto fail;
>              }
>              op = ls_port_create(ovnsb_idl_txn, &nd->ls_ports,
> -                                new_nbsp->name, new_nbsp, od,
> +                                paired_lsp->nb->name, paired_lsp->nb, od,
> +                                paired_lsp->sb,
>                                  ni->sbrec_mirror_table,
>                                  ni->sbrec_chassis_by_name,
>                                  ni->sbrec_chassis_by_hostname);
> @@ -4709,28 +4366,27 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn
> *ovnsb_idl_txn,
>                  goto fail;
>              }
>              add_op_to_northd_tracked_ports(&trk_lsps->created, op);
> -        } else if (ls_port_has_changed(new_nbsp)) {
> +        } else if (ls_port_has_changed(paired_lsp->nb)) {
>              /* Existing port updated */
>              bool temp = false;
> -            if (lsp_is_type_changed(op->sb, new_nbsp, &temp) ||
> +            if (lsp_is_type_changed(op->sb, paired_lsp->nb, &temp) ||
>                  !op->lsp_can_be_inc_processed ||
> -                !lsp_can_be_inc_processed(new_nbsp)) {
> +                !lsp_can_be_inc_processed(paired_lsp->nb)) {
>                  goto fail;
>              }
> -            const struct sbrec_port_binding *sb = op->sb;
> -            if (sset_contains(&nd->svc_monitor_lsps, new_nbsp->name)) {
> +            if (sset_contains(&nd->svc_monitor_lsps,
> paired_lsp->nb->name)) {
>                  /* This port is used for svc monitor, which may be
> impacted
>                   * by this change. Fallback to recompute. */
>                  goto fail;
>              }
> -            if (!lsp_handle_mirror_rules_changes(new_nbsp) ||
> +            if (!lsp_handle_mirror_rules_changes(paired_lsp->nb) ||
>
> is_lsp_mirror_target_port(ni->nbrec_mirror_by_type_and_sink,
>                                             op)) {
>                  /* Fallback to recompute. */
>                  goto fail;
>              }
>              if (!check_lsp_is_up &&
> -                !check_lsp_changes_other_than_up(new_nbsp)) {
> +                !check_lsp_changes_other_than_up(paired_lsp->nb)) {
>                  /* If the only change is the "up" column while the
>                   * "ignore_lsp_down" is set to true, just ignore this
>                   * change. */
> @@ -4739,17 +4395,11 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn
> *ovnsb_idl_txn,
>              }
>
>              uint32_t old_tunnel_key = op->tunnel_key;
> -            if (!ls_port_reinit(op, ovnsb_idl_txn,
> -                                new_nbsp,
> -                                od, sb, ni->sbrec_mirror_table,
> -                                ni->sbrec_chassis_by_name,
> -                                ni->sbrec_chassis_by_hostname)) {
> -                if (sb) {
> -                    sbrec_port_binding_delete(sb);
> -                }
> -                ovn_port_destroy(&nd->ls_ports, op);
> -                goto fail;
> -            }
> +            ls_port_reinit(op, ovnsb_idl_txn,
> +                           paired_lsp->nb,
> +                           od, paired_lsp->sb, ni->sbrec_mirror_table,
> +                           ni->sbrec_chassis_by_name,
> +                           ni->sbrec_chassis_by_hostname);
>              add_op_to_northd_tracked_ports(&trk_lsps->updated, op);
>
>              if (old_tunnel_key != op->tunnel_key) {
> @@ -4774,7 +4424,6 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn
> *ovnsb_idl_txn,
>              add_op_to_northd_tracked_ports(&trk_lsps->deleted, op);
>              hmap_remove(&nd->ls_ports, &op->key_node);
>              hmap_remove(&od->ports, &op->dp_node);
> -            sbrec_port_binding_delete(op->sb);
>              delete_fdb_entries(ni->sbrec_fdb_by_dp_and_port,
> od->tunnel_key,
>                                  op->tunnel_key);
>              if
> (is_lsp_mirror_target_port(ni->nbrec_mirror_by_type_and_sink,
> @@ -19032,13 +18681,16 @@ ovnnb_db_run(struct northd_input *input_data,
>                         &data->ls_datapaths, &data->lr_datapaths,
>                         &data->lb_datapaths_map,
> &data->lb_group_datapaths_map);
>      build_ports(ovnsb_txn,
> -                input_data->sbrec_port_binding_table,
>                  input_data->sbrec_mirror_table,
>                  input_data->sbrec_mac_binding_table,
>                  input_data->sbrec_ha_chassis_group_table,
>                  input_data->sbrec_chassis_by_name,
>                  input_data->sbrec_chassis_by_hostname,
>                  input_data->sbrec_ha_chassis_grp_by_name,
> +                input_data->paired_lsps,
> +                input_data->paired_lrps,
> +                input_data->paired_crps,
> +                input_data->paired_mirrors,
>                  &data->ls_datapaths.datapaths,
> &data->lr_datapaths.datapaths,
>                  &data->ls_ports, &data->lr_ports);
>      build_lb_port_related_data(ovnsb_txn,
> @@ -19072,10 +18724,7 @@ ovnnb_db_run(struct northd_input *input_data,
>      sync_template_vars(ovnsb_txn,
> input_data->nbrec_chassis_template_var_table,
>                         input_data->sbrec_chassis_template_var_table);
>
> -    cleanup_stale_fdb_entries(input_data->sbrec_fdb_table,
> -                              &data->ls_datapaths.datapaths);
>      stopwatch_stop(CLEAR_LFLOWS_CTX_STOPWATCH_NAME, time_msec());
> -
>  }
>
>  /* Stores the set of chassis which references an ha_chassis_group.
> @@ -19267,6 +18916,25 @@ handle_cr_port_binding_changes(const struct
> sbrec_port_binding *sb,
>      }
>  }
>
> +void
> +lsp_set_up(const struct sbrec_port_binding *pb,
> +           const struct nbrec_logical_switch_port *lsp)
> +{
> +    bool up = false;
> +
> +    if (lsp_is_router(lsp) || lsp_is_switch(lsp)) {
> +        up = true;
> +    } else if (pb->chassis) {
> +        up = !smap_get_bool(&pb->chassis->other_config, "is-remote",
> false)
> +             ? pb->n_up && pb->up[0]
> +             : true;
> +    }
> +
> +    if (!lsp->up || *lsp->up != up) {
> +        nbrec_logical_switch_port_set_up(lsp, &up, 1);
> +    }
> +}
> +
>  /* Handle changes to the 'chassis' column of the 'Port_Binding' table.
> When
>   * this column is not empty, it means we need to set the corresponding
> logical
>   * port as 'up' in the northbound DB. */
> @@ -19315,25 +18983,13 @@ handle_port_binding_changes(struct ovsdb_idl_txn
> *ovnsb_txn,
>              continue;
>          }
>
> -        bool up = false;
> -
> -        if (lsp_is_router(op->nbsp) || lsp_is_switch(op->nbsp)) {
> -            up = true;
> -        } else if (sb->chassis) {
> -            up = !smap_get_bool(&sb->chassis->other_config, "is-remote",
> false)
> -                 ? sb->n_up && sb->up[0]
> -                 : true;
> -        }
> -
> -        if (!op->nbsp->up || *op->nbsp->up != up) {
> -            nbrec_logical_switch_port_set_up(op->nbsp, &up, 1);
> -        }
> +        lsp_set_up(sb, op->nbsp);
>
>          /* ovn-controller will update 'Port_Binding.up' only if it was
>           * explicitly set to 'false'.
>           */
>          if (!op->sb->n_up) {
> -            up = false;
> +            bool up = false;
>              sbrec_port_binding_set_up(op->sb, &up, 1);
>          }
>
> diff --git a/northd/northd.h b/northd/northd.h
> index e5a9cc775..ee34c28c0 100644
> --- a/northd/northd.h
> +++ b/northd/northd.h
> @@ -74,6 +74,12 @@ struct northd_input {
>      const struct ovn_synced_logical_switch_map *synced_lses;
>      const struct ovn_synced_logical_router_map *synced_lrs;
>
> +    /* Paired port binding inputs. */
> +    const struct ovn_paired_logical_switch_port_map *paired_lsps;
> +    const struct ovn_paired_logical_router_port_map *paired_lrps;
> +    const struct ovn_paired_chassisredirect_port_map *paired_crps;
> +    const struct ovn_paired_mirror_map *paired_mirrors;
> +
>      /* Indexes */
>      struct ovsdb_idl_index *sbrec_chassis_by_name;
>      struct ovsdb_idl_index *sbrec_chassis_by_hostname;
> @@ -376,9 +382,6 @@ struct ovn_datapath {
>      /* Logical switch data. */
>      struct vector router_ports; /* Vector of struct ovn_port *. */
>
> -    struct hmap port_tnlids;
> -    uint32_t port_key_hint;
> -
>      bool has_unknown;
>      bool has_vtep_lports;
>      bool has_arp_proxy_port;
> @@ -827,6 +830,8 @@ void ovnsb_db_run(struct ovsdb_idl_txn *ovnnb_txn,
>                    const struct sbrec_ha_chassis_group_table *,
>                    struct hmap *ls_ports,
>                    struct hmap *lr_ports);
> +void lsp_set_up(const struct sbrec_port_binding *pb,
> +                const struct nbrec_logical_switch_port *lsp);
>  bool northd_handle_ls_changes(struct ovsdb_idl_txn *,
>                                const struct northd_input *,
>                                struct northd_data *);
> @@ -1037,4 +1042,10 @@ struct ovn_port_routable_addresses get_op_addresses(
>
>  void destroy_routable_addresses(struct ovn_port_routable_addresses *ra);
>
> +bool lsp_is_type_changed(const struct sbrec_port_binding *sb,
> +                         const struct nbrec_logical_switch_port *nbsp,
> +                         bool *update_sbrec);
> +
> +const char *
> +ovn_datapath_name(const struct sbrec_datapath_binding *sb);
>  #endif /* NORTHD_H */
> diff --git a/northd/port_binding_pair.c b/northd/port_binding_pair.c
> new file mode 100644
> index 000000000..bfd3d0b42
> --- /dev/null
> +++ b/northd/port_binding_pair.c
> @@ -0,0 +1,81 @@
> +/* Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#include <config.h>
> +
> +#include "port_binding_pair.h"
> +
> +struct ovn_unpaired_port_binding *
> +ovn_unpaired_port_binding_alloc(uint32_t requested_tunnel_key,
> +                                const char *name,
> +                                const char *type,
> +                                void *cookie,
> +                                const struct sbrec_datapath_binding
> *sb_dp)
> +{
> +    struct ovn_unpaired_port_binding *pb = xzalloc(sizeof *pb);
> +    pb->requested_tunnel_key = requested_tunnel_key;
> +    pb->name = name;
> +    pb->type = type;
> +    pb->cookie = cookie;
> +    pb->sb_dp = sb_dp;
> +    smap_init(&pb->external_ids);
> +
> +    return pb;
> +}
> +
> +void
> +ovn_unpaired_port_binding_destroy(struct ovn_unpaired_port_binding *pb)
> +{
> +    smap_destroy(&pb->external_ids);
> +}
> +
> +static bool
> +default_sb_is_valid(const struct sbrec_port_binding *sb_pb OVS_UNUSED,
> +                    const struct ovn_unpaired_port_binding *upb
> OVS_UNUSED)
> +{
> +    return true;
> +}
> +
> +static struct ovn_unpaired_port_binding_map_callbacks default_callbacks =
> {
> +    .sb_is_valid = default_sb_is_valid,
> +};
> +
> +void
> +ovn_unpaired_port_binding_map_init(
> +    struct ovn_unpaired_port_binding_map *map,
> +    const struct ovn_unpaired_port_binding_map_callbacks *cb)
> +{
> +    shash_init(&map->ports);
> +    if (cb) {
> +        map->cb = cb;
> +    } else {
> +        map->cb = &default_callbacks;
> +    }
> +}
> +
> +void
> +ovn_unpaired_port_binding_map_destroy(
> +    struct ovn_unpaired_port_binding_map *map)
> +{
> +    struct ovn_unpaired_port_binding *pb;
> +    struct shash_node *node;
> +    SHASH_FOR_EACH_SAFE (node, &map->ports) {
> +        pb = node->data;
> +        shash_delete(&map->ports, node);
> +        ovn_unpaired_port_binding_destroy(pb);
> +        free(pb);
> +    }
> +    shash_destroy(&map->ports);
> +}
> diff --git a/northd/port_binding_pair.h b/northd/port_binding_pair.h
> new file mode 100644
> index 000000000..c76d30ca1
> --- /dev/null
> +++ b/northd/port_binding_pair.h
> @@ -0,0 +1,117 @@
> +/* Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#ifndef PORT_BINDING_PAIR_H
> +#define PORT_BINDING_PAIR_H 1
> +
> +#include "openvswitch/hmap.h"
> +#include "openvswitch/list.h"
> +#include "openvswitch/shash.h"
> +#include "smap.h"
> +
> +/* Port Binding pairing API. This file consists of utility functions
> + * that can be used when pairing northbound port types (e.g.
> + * Logical_Router_Port and Logical_Switch_Port) to southbound
> Port_Bindings.
> + *
> + * The basic flow of data is as such.
> + * 1. A northbound type is converted into an ovn_unpaired_port_binding.
> + * All ovn_unpaired_port_bindings are placed into an
> ovn_unpaired_datapath_map.
> + * 2. The en_port_binding_pair node takes all of the maps in as input and
> + * pairs them with southbound port bindings. This includes allocating
> + * tunnel keys across all ports. The output of this node is
> + * ovn_paired_port_bindings, which contains a list of all paired port
> bindings.
> + * 3. A northbound type-aware node then takes the
> ovn_paired_port_bindings,
> + * and decodes the generic paired port bindings back into a type-specific
> + * version (e.g. ovn_paired_logical_router_port). Later nodes can then
> consume
> + * these type-specific paired port binding types in order to perform
> + * further processing.
> + *
> + * It is important to note that this code pairs northbound ports to
> southbound
> + * port bindings, but it does not 100% sync them. The following fields are
> + * synced between the northbound port and the southbound Port_Binding:
> + * - logical_port
> + * - tunnel_key
> + * - external_ids
> + *
> + * Two later incremental engine nodes sync the rest of the fields on the
> Port
> + * Binding. en_northd syncs the vast majority of the data. Then finally,
> + * en_sync_to_sb syncs the nat_addresses of the Port_Binding.
> + */
> +
> +struct ovn_unpaired_port_binding {
> +    uint32_t requested_tunnel_key;
> +    struct smap external_ids;
> +    void *cookie;
> +    const char *name;
> +    const char *type;
> +    const struct sbrec_datapath_binding *sb_dp;
> +};
> +
> +struct sbrec_port_binding;
> +struct ovn_unpaired_port_binding_map_callbacks {
> +    bool (*sb_is_valid)(const struct sbrec_port_binding *sp_pb,
> +                        const struct ovn_unpaired_port_binding *upb);
> +};
> +
> +struct ovn_unpaired_port_binding_map {
> +    struct shash ports;
> +    const struct ovn_unpaired_port_binding_map_callbacks *cb;
> +};
> +
> +struct sbrec_port_binding;
> +struct unpaired_port_data;
>

The 'struct unpaired_port_data' and
'struct unpaired_port_data_callbacks' are not used anywhere. I
suppose it's leftover from previous versions.


> +
> +struct unpaired_port_data_callbacks {
> +    bool (*is_valid)(const struct unpaired_port_data *unpaired,
> +                     const struct sbrec_port_binding *sp_pb);
> +    struct ovn_unpaired_port_binding *
> +        (*find)(const struct unpaired_port_data *unpaired,
> +                const struct sbrec_port_binding *sb_pb);
> +    void (*get_ports)(const struct unpaired_port_data *unpaired,
> +                      struct shash *returned_ports);
> +};
> +
> +struct unpaired_port_data {
> +    void *private_data;
> +    struct unpaired_port_data_callbacks *cb;
> +};
> +
> +struct ovn_paired_port_binding {
> +    struct ovs_list list_node;
> +    const void *cookie;
> +    const char *type;
>

Those are memory only structures, it would be better to use
enum so we can avoid strcmp.

+    const struct sbrec_port_binding *sb_pb;
> +};
> +
> +struct ovn_paired_port_bindings {
> +    struct ovs_list paired_pbs;
> +    struct hmap tunnel_key_maps;
> +};
> +
> +struct ovn_unpaired_port_binding *ovn_unpaired_port_binding_alloc(
> +        uint32_t requested_tunnel_key, const char *name,
> +        const char *type,
> +        void *cookie,
> +        const struct sbrec_datapath_binding *sb_dp);
> +
> +void ovn_unpaired_port_binding_destroy(struct ovn_unpaired_port_binding
> *pb);
> +
> +void ovn_unpaired_port_binding_map_init(
> +    struct ovn_unpaired_port_binding_map *map,
> +    const struct ovn_unpaired_port_binding_map_callbacks *cb);
> +void ovn_unpaired_port_binding_map_destroy(
> +    struct ovn_unpaired_port_binding_map *map);
> +
> +#endif /* PORT_BINDING_PAIR_H */
> --
> 2.47.0
>
> _______________________________________________
> dev mailing list
> dev@openvswitch.org
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
>
>
Thanks,
Ales
diff mbox series

Patch

diff --git a/TODO.rst b/TODO.rst
index 78962bb92..60ae155c5 100644
--- a/TODO.rst
+++ b/TODO.rst
@@ -168,3 +168,15 @@  OVN To-do List
     ovn\_synced\_logical_router and ovn\_synced\_logical\_switch. This will
     allow for the eventual removal of the ovn\_datapath structure from the
     codebase.
+
+* Port Binding sync nodes
+
+  * Southbound Port bindings are synced across three engine nodes:
+    - en_port_binding_pair
+    - en_northd
+    - en_sync_to_sb
+    It would be easier to work with if these were combined into a
+    single node instead.
+
+  * Add incremental processing to the en-port-binding-pair node, as
+    well as derivative nodes.
diff --git a/northd/automake.mk b/northd/automake.mk
index bf9978dd2..f475e0cd9 100644
--- a/northd/automake.mk
+++ b/northd/automake.mk
@@ -54,6 +54,16 @@  northd_ovn_northd_SOURCES = \
 	northd/en-learned-route-sync.h \
 	northd/en-group-ecmp-route.c \
 	northd/en-group-ecmp-route.h \
+	northd/en-port-binding-logical-router-port.c \
+	northd/en-port-binding-logical-router-port.h \
+	northd/en-port-binding-logical-switch-port.c \
+	northd/en-port-binding-logical-switch-port.h \
+	northd/en-port-binding-chassisredirect.c \
+	northd/en-port-binding-chassisredirect.h \
+	northd/en-port-binding-mirror.c \
+	northd/en-port-binding-mirror.h \
+	northd/en-port-binding-pair.c \
+	northd/en-port-binding-pair.h \
 	northd/inc-proc-northd.c \
 	northd/inc-proc-northd.h \
 	northd/ipam.c \
@@ -61,7 +71,9 @@  northd_ovn_northd_SOURCES = \
 	northd/lflow-mgr.c \
 	northd/lflow-mgr.h \
 	northd/lb.c \
-	northd/lb.h
+	northd/lb.h \
+	northd/port_binding_pair.c \
+	northd/port_binding_pair.h
 northd_ovn_northd_LDADD = \
 	lib/libovn.la \
 	$(OVSDB_LIBDIR)/libovsdb.la \
diff --git a/northd/en-global-config.c b/northd/en-global-config.c
index 7204462ee..3a4bdbf87 100644
--- a/northd/en-global-config.c
+++ b/northd/en-global-config.c
@@ -148,6 +148,9 @@  en_global_config_run(struct engine_node *node , void *data)
     config_data->max_dp_tunnel_id =
         get_ovn_max_dp_key_local(config_data->vxlan_mode, ic_vxlan_mode);
 
+    uint8_t pb_tunnel_bits = config_data->vxlan_mode ? 12 : 16;
+    config_data->max_pb_tunnel_id = (1u << (pb_tunnel_bits - 1)) - 1;
+
     char *max_tunid = xasprintf("%d", config_data->max_dp_tunnel_id);
     smap_replace(options, "max_tunid", max_tunid);
     free(max_tunid);
diff --git a/northd/en-global-config.h b/northd/en-global-config.h
index 55a1e420b..dbb06151c 100644
--- a/northd/en-global-config.h
+++ b/northd/en-global-config.h
@@ -51,6 +51,7 @@  struct ed_type_global_config {
 
     bool vxlan_mode;
     uint32_t max_dp_tunnel_id;
+    uint32_t max_pb_tunnel_id;
 
     bool tracked;
     struct global_config_tracked_data tracked_data;
diff --git a/northd/en-northd.c b/northd/en-northd.c
index c4573f88f..15840e361 100644
--- a/northd/en-northd.c
+++ b/northd/en-northd.c
@@ -123,6 +123,19 @@  northd_get_input_data(struct engine_node *node,
 
     input_data->synced_lrs =
         engine_get_input_data("datapath_synced_logical_router", node);
+
+    input_data->paired_lsps =
+        engine_get_input_data("port_binding_paired_logical_switch_port", node);
+
+    input_data->paired_lrps =
+        engine_get_input_data("port_binding_paired_logical_router_port", node);
+
+    input_data->paired_crps =
+        engine_get_input_data("port_binding_paired_chassisredirect_port",
+                              node);
+
+    input_data->paired_mirrors =
+        engine_get_input_data("port_binding_paired_mirror", node);
 }
 
 enum engine_node_state
@@ -477,42 +490,6 @@  en_northd_clear_tracked_data(void *data_)
     destroy_northd_data_tracked_changes(data);
 }
 
-enum engine_input_handler_result
-northd_sb_fdb_change_handler(struct engine_node *node, void *data)
-{
-    struct northd_data *nd = data;
-    const struct sbrec_fdb_table *sbrec_fdb_table =
-        EN_OVSDB_GET(engine_get_input("SB_fdb", node));
-
-    /* check if changed rows are stale and delete them */
-    const struct sbrec_fdb *fdb_e, *fdb_prev_del = NULL;
-    SBREC_FDB_TABLE_FOR_EACH_TRACKED (fdb_e, sbrec_fdb_table) {
-        if (sbrec_fdb_is_deleted(fdb_e)) {
-            continue;
-        }
-
-        if (fdb_prev_del) {
-            sbrec_fdb_delete(fdb_prev_del);
-        }
-
-        fdb_prev_del = fdb_e;
-        struct ovn_datapath *od
-            = ovn_datapath_find_by_key(&nd->ls_datapaths.datapaths,
-                                       fdb_e->dp_key);
-        if (od) {
-            if (ovn_tnlid_present(&od->port_tnlids, fdb_e->port_key)) {
-                fdb_prev_del = NULL;
-            }
-        }
-    }
-
-    if (fdb_prev_del) {
-        sbrec_fdb_delete(fdb_prev_del);
-    }
-
-    return EN_HANDLED_UNCHANGED;
-}
-
 void
 en_route_policies_cleanup(void *data)
 {
diff --git a/northd/en-northd.h b/northd/en-northd.h
index b19b73270..58a524c5c 100644
--- a/northd/en-northd.h
+++ b/northd/en-northd.h
@@ -25,8 +25,6 @@  enum engine_input_handler_result
 northd_sb_port_binding_handler(struct engine_node *, void *data);
 enum engine_input_handler_result northd_lb_data_handler(struct engine_node *,
                                                         void *data);
-enum engine_input_handler_result
-northd_sb_fdb_change_handler(struct engine_node *node, void *data);
 void *en_routes_init(struct engine_node *node OVS_UNUSED,
                             struct engine_arg *arg OVS_UNUSED);
 void en_route_policies_cleanup(void *data);
diff --git a/northd/en-port-binding-chassisredirect.c b/northd/en-port-binding-chassisredirect.c
new file mode 100644
index 000000000..5b2e7a9e4
--- /dev/null
+++ b/northd/en-port-binding-chassisredirect.c
@@ -0,0 +1,319 @@ 
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <config.h>
+
+#include "inc-proc-eng.h"
+#include "ovn-nb-idl.h"
+#include "en-datapath-logical-router.h"
+#include "en-datapath-logical-switch.h"
+#include "en-port-binding-chassisredirect.h"
+#include "port_binding_pair.h"
+#include "ovn-util.h"
+
+#include "openvswitch/vlog.h"
+
+#include "hmapx.h"
+
+#define CR_SWITCH_PORT_TYPE "chassisredirect-logical-switch-port"
+#define CR_ROUTER_PORT_TYPE "chassisredirect-logical-router-port"
+
+VLOG_DEFINE_THIS_MODULE(en_port_binding_chassisredirect_port);
+
+struct router_dgps {
+    const struct ovn_synced_logical_router *lr;
+    const struct nbrec_logical_router_port **dgps;
+    size_t n_dgps;
+};
+
+struct switch_localnets {
+    const struct ovn_synced_logical_switch *ls;
+    size_t n_localnet_ports;
+};
+
+struct port_router_dgps {
+    const struct nbrec_logical_router_port *nbrp;
+    struct router_dgps *r;
+};
+
+static struct port_router_dgps *
+port_router_dgps_alloc(const struct nbrec_logical_router_port *nbrp,
+                       struct router_dgps *r)
+{
+    struct port_router_dgps *p_dgps = xmalloc(sizeof *p_dgps);
+    p_dgps->nbrp = nbrp;
+    p_dgps->r = r;
+
+    return p_dgps;
+}
+
+void *
+en_port_binding_chassisredirect_port_init(struct engine_node *node OVS_UNUSED,
+                                         struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+    return map;
+}
+
+struct chassisredirect_router_port {
+    char *name;
+    const struct nbrec_logical_router_port *nbrp;
+};
+
+static struct chassisredirect_router_port *
+chassisredirect_router_port_alloc(const struct nbrec_logical_router_port *nbrp)
+{
+    struct chassisredirect_router_port *crp = xmalloc(sizeof *crp);
+    crp->name = ovn_chassis_redirect_name(nbrp->name);
+    crp->nbrp = nbrp;
+
+    return crp;
+}
+
+static void
+chassisredirect_router_port_free(struct chassisredirect_router_port *crp)
+{
+    free(crp->name);
+    free(crp);
+}
+
+struct chassisredirect_switch_port {
+    char *name;
+    const struct nbrec_logical_switch_port *nbsp;
+};
+
+static struct chassisredirect_switch_port *
+chassisredirect_switch_port_alloc(const struct nbrec_logical_switch_port *nbsp)
+{
+    struct chassisredirect_switch_port *crp = xmalloc(sizeof *crp);
+    crp->name = ovn_chassis_redirect_name(nbsp->name);
+    crp->nbsp = nbsp;
+
+    return crp;
+}
+
+static void
+chassisredirect_switch_port_free(struct chassisredirect_switch_port *csp)
+{
+    free(csp->name);
+    free(csp);
+}
+
+static void
+chassisredirect_port_binding_map_destroy(
+    struct ovn_unpaired_port_binding_map *map)
+{
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &map->ports) {
+        struct ovn_unpaired_port_binding *upb = node->data;
+        if (!strcmp(upb->type, CR_ROUTER_PORT_TYPE)) {
+            chassisredirect_router_port_free(upb->cookie);
+        } else {
+            chassisredirect_switch_port_free(upb->cookie);
+        }
+    }
+    ovn_unpaired_port_binding_map_destroy(map);
+}
+
+enum engine_node_state
+en_port_binding_chassisredirect_port_run(struct engine_node *node, void *data)
+{
+    const struct ovn_synced_logical_router_map *lr_map =
+        engine_get_input_data("datapath_synced_logical_router", node);
+    const struct ovn_synced_logical_switch_map *ls_map =
+        engine_get_input_data("datapath_synced_logical_switch", node);
+
+    struct ovn_unpaired_port_binding_map *map = data;
+
+    chassisredirect_port_binding_map_destroy(map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+
+    struct shash ports = SHASH_INITIALIZER(&ports);
+    const struct ovn_synced_logical_router *lr;
+    struct hmapx all_rdgps = HMAPX_INITIALIZER(&all_rdgps);
+    HMAP_FOR_EACH (lr, hmap_node, &lr_map->synced_routers) {
+        if (smap_get(&lr->nb->options, "chassis")) {
+            /* If the logical router has the chassis option set,
+             * then we ignore any ports that have gateway_chassis
+             * or ha_chassis_group options set.
+             */
+            continue;
+        }
+        struct router_dgps *rdgps = xzalloc(sizeof *rdgps);
+        rdgps->lr = lr;
+        rdgps->dgps = xzalloc(sizeof(*rdgps->dgps) * lr->nb->n_ports);
+        hmapx_add(&all_rdgps, rdgps);
+        const struct nbrec_logical_router_port *nbrp;
+        for (size_t i = 0; i < lr->nb->n_ports; i++) {
+            nbrp = lr->nb->ports[i];
+            if (nbrp->ha_chassis_group || nbrp->n_gateway_chassis) {
+                rdgps->dgps[rdgps->n_dgps++] = nbrp;
+                shash_add(&ports, nbrp->name,
+                          port_router_dgps_alloc(nbrp, rdgps));
+            }
+        }
+    }
+
+    struct hmapx all_localnets = HMAPX_INITIALIZER(&all_localnets);
+    const struct ovn_synced_logical_switch *ls;
+    HMAP_FOR_EACH (ls, hmap_node, &ls_map->synced_switches) {
+        struct switch_localnets *localnets = xzalloc(sizeof *localnets);
+        localnets->ls = ls;
+        hmapx_add(&all_localnets, localnets);
+        for (size_t i = 0; i < ls->nb->n_ports; i++) {
+            const struct nbrec_logical_switch_port *nbsp = ls->nb->ports[i];
+            if (!strcmp(nbsp->type, "localnet")) {
+                localnets->n_localnet_ports++;
+            }
+        }
+    }
+
+    /* All logical router DGPs need corresponding chassisredirect ports
+     * made
+     */
+    struct hmapx_node *hmapx_node;
+    HMAPX_FOR_EACH (hmapx_node, &all_rdgps) {
+        struct router_dgps *rdgps = hmapx_node->data;
+        struct ovn_unpaired_port_binding *upb;
+        for (size_t i = 0; i < rdgps->n_dgps; i++) {
+            const struct nbrec_logical_router_port *nbrp = rdgps->dgps[i];
+            struct chassisredirect_router_port *crp =
+                chassisredirect_router_port_alloc(nbrp);
+            upb = ovn_unpaired_port_binding_alloc(0, crp->name,
+                                                  CR_ROUTER_PORT_TYPE,
+                                                  crp, rdgps->lr->sb);
+            shash_add(&map->ports, crp->name, upb);
+        }
+    }
+
+    /* Logical switch ports that are peered with DGPs need chassisredirect
+     * ports created if
+     * 1. The DGP it is paired with is the only one on its router, and
+     * 2. There are no localnet ports on the switch
+     */
+    HMAPX_FOR_EACH (hmapx_node, &all_localnets) {
+        struct switch_localnets *localnets = hmapx_node->data;
+        if (localnets->n_localnet_ports > 0) {
+            continue;
+        }
+        for (size_t i = 0; i < localnets->ls->nb->n_ports; i++) {
+            const struct nbrec_logical_switch_port *nbsp =
+                localnets->ls->nb->ports[i];
+            if (strcmp(nbsp->type, "router")) {
+                continue;
+            }
+            const char *peer_name = smap_get(&nbsp->options, "router-port");
+            if (!peer_name) {
+                continue;
+            }
+            struct port_router_dgps *prdgps = shash_find_data(&ports,
+                                                              peer_name);
+            if (!prdgps) {
+                continue;
+            }
+            if (prdgps->r->n_dgps > 1) {
+                continue;
+            }
+            struct ovn_unpaired_port_binding *upb;
+            struct chassisredirect_switch_port *crp =
+                chassisredirect_switch_port_alloc(nbsp);
+            upb = ovn_unpaired_port_binding_alloc(0, crp->name,
+                                                  CR_SWITCH_PORT_TYPE,
+                                                  crp, localnets->ls->sb);
+            shash_add(&map->ports, crp->name, upb);
+        }
+    }
+
+    return EN_UPDATED;
+}
+
+void
+en_port_binding_chassisredirect_port_cleanup(void *data)
+{
+    struct ovn_unpaired_port_binding_map *map = data;
+    chassisredirect_port_binding_map_destroy(map);
+}
+
+
+static void
+ovn_paired_chassisredirect_port_map_init(
+    struct ovn_paired_chassisredirect_port_map *map)
+{
+    shash_init(&map->paired_chassisredirect_router_ports);
+    shash_init(&map->paired_chassisredirect_switch_ports);
+}
+
+static void
+ovn_paired_chassisredirect_port_map_destroy(
+    struct ovn_paired_chassisredirect_port_map *map)
+{
+    shash_destroy_free_data(&map->paired_chassisredirect_switch_ports);
+    shash_destroy_free_data(&map->paired_chassisredirect_router_ports);
+}
+
+void *
+en_port_binding_paired_chassisredirect_port_init(
+    struct engine_node *node OVS_UNUSED, struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_paired_chassisredirect_port_map *map = xzalloc(sizeof *map);
+    ovn_paired_chassisredirect_port_map_init(map);
+    return map;
+}
+
+enum engine_node_state
+en_port_binding_paired_chassisredirect_port_run(struct engine_node *node,
+                                                void *data)
+{
+    const struct ovn_paired_port_bindings *pbs =
+        engine_get_input_data("port_binding_pair", node);
+    struct ovn_paired_chassisredirect_port_map *map = data;
+
+    ovn_paired_chassisredirect_port_map_destroy(map);
+    ovn_paired_chassisredirect_port_map_init(map);
+
+    struct ovn_paired_port_binding *port;
+    LIST_FOR_EACH (port, list_node, &pbs->paired_pbs) {
+        if (!strcmp(port->type, CR_SWITCH_PORT_TYPE)) {
+            const struct chassisredirect_switch_port *cr_port = port->cookie;
+            struct ovn_paired_chassisredirect_switch_port *paired_cr_port;
+            paired_cr_port = xmalloc(sizeof *cr_port);
+            paired_cr_port->name = cr_port->name;
+            paired_cr_port->sb = port->sb_pb;
+            paired_cr_port->primary_port = cr_port->nbsp;
+            shash_add(&map->paired_chassisredirect_switch_ports, cr_port->name,
+                      paired_cr_port);
+        } else if (!strcmp(port->type, CR_ROUTER_PORT_TYPE)) {
+            const struct chassisredirect_router_port *cr_port = port->cookie;
+            struct ovn_paired_chassisredirect_router_port *paired_cr_port;
+            paired_cr_port = xmalloc(sizeof *cr_port);
+            paired_cr_port->name = cr_port->name;
+            paired_cr_port->sb = port->sb_pb;
+            paired_cr_port->primary_port = cr_port->nbrp;
+            shash_add(&map->paired_chassisredirect_router_ports, cr_port->name,
+                      paired_cr_port);
+        }
+    }
+
+    return EN_UPDATED;
+}
+
+void
+en_port_binding_paired_chassisredirect_port_cleanup(void *data)
+{
+    struct ovn_paired_chassisredirect_port_map *map = data;
+
+    ovn_paired_chassisredirect_port_map_destroy(map);
+}
diff --git a/northd/en-port-binding-chassisredirect.h b/northd/en-port-binding-chassisredirect.h
new file mode 100644
index 000000000..bbea31993
--- /dev/null
+++ b/northd/en-port-binding-chassisredirect.h
@@ -0,0 +1,53 @@ 
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EN_PORT_BINDING_CHASSISREDIRECT_PORT_H
+#define EN_PORT_BINDING_CHASSISREDIRECT_PORT_H
+
+#include "lib/inc-proc-eng.h"
+#include "openvswitch/shash.h"
+
+
+void *en_port_binding_chassisredirect_port_init(struct engine_node *,
+                                           struct engine_arg *);
+
+enum engine_node_state en_port_binding_chassisredirect_port_run(
+    struct engine_node *, void *data);
+void en_port_binding_chassisredirect_port_cleanup(void *data);
+
+struct ovn_paired_chassisredirect_switch_port {
+    const char *name;
+    const struct nbrec_logical_switch_port *primary_port;
+    const struct sbrec_port_binding *sb;
+};
+
+struct ovn_paired_chassisredirect_router_port {
+    const char *name;
+    const struct nbrec_logical_router_port *primary_port;
+    const struct sbrec_port_binding *sb;
+};
+
+struct ovn_paired_chassisredirect_port_map {
+    struct shash paired_chassisredirect_router_ports;
+    struct shash paired_chassisredirect_switch_ports;
+};
+
+void *en_port_binding_paired_chassisredirect_port_init(struct engine_node *,
+                                                       struct engine_arg *);
+enum engine_node_state en_port_binding_paired_chassisredirect_port_run(
+    struct engine_node *, void *data);
+void en_port_binding_paired_chassisredirect_port_cleanup(void *data);
+#endif /* EN_PORT_BINDING_CHASSISREDIRECT_PORT_H */
diff --git a/northd/en-port-binding-logical-router-port.c b/northd/en-port-binding-logical-router-port.c
new file mode 100644
index 000000000..07cd9ac26
--- /dev/null
+++ b/northd/en-port-binding-logical-router-port.c
@@ -0,0 +1,178 @@ 
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "openvswitch/hmap.h"
+#include "openvswitch/vlog.h"
+#include "util.h"
+
+#include "inc-proc-eng.h"
+#include "ovn-nb-idl.h"
+#include "port_binding_pair.h"
+#include "en-datapath-logical-router.h"
+#include "en-port-binding-logical-router-port.h"
+
+#define LRP_TYPE_NAME "logical-router-port"
+
+VLOG_DEFINE_THIS_MODULE(en_port_binding_logical_router_port);
+
+struct logical_router_port_cookie {
+    const struct nbrec_logical_router_port *nbrp;
+    const struct ovn_synced_logical_router *router;
+};
+
+static struct logical_router_port_cookie *
+logical_router_port_cookie_alloc(const struct nbrec_logical_router_port *nbrp,
+                                 const struct ovn_synced_logical_router *lr)
+{
+    struct logical_router_port_cookie *cookie = xmalloc(sizeof *cookie);
+    cookie->nbrp = nbrp;
+    cookie->router = lr;
+
+    return cookie;
+}
+
+static void
+logical_router_port_cookie_free(struct logical_router_port_cookie *cookie)
+{
+    free(cookie);
+}
+
+static void
+unpaired_logical_router_port_map_destroy(
+    struct ovn_unpaired_port_binding_map *map)
+{
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &map->ports) {
+        struct ovn_unpaired_port_binding *upb = node->data;
+        logical_router_port_cookie_free(upb->cookie);
+    }
+    ovn_unpaired_port_binding_map_destroy(map);
+}
+
+void *
+en_port_binding_logical_router_port_init(struct engine_node *node OVS_UNUSED,
+                                         struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+    return map;
+}
+
+enum engine_node_state
+en_port_binding_logical_router_port_run(struct engine_node *node, void *data)
+{
+    const struct ovn_synced_logical_router_map *lr_map =
+        engine_get_input_data("datapath_synced_logical_router", node);
+
+    struct ovn_unpaired_port_binding_map *map = data;
+
+    unpaired_logical_router_port_map_destroy(map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+
+    const struct ovn_synced_logical_router *paired_lr;
+    HMAP_FOR_EACH (paired_lr, hmap_node, &lr_map->synced_routers) {
+        const struct nbrec_logical_router_port *nbrp;
+        for (size_t i = 0; i < paired_lr->nb->n_ports; i++) {
+            nbrp = paired_lr->nb->ports[i];
+            uint32_t requested_tunnel_key = smap_get_int(&nbrp->options,
+                                                         "requested-tnl-key",
+                                                         0);
+            struct logical_router_port_cookie *cookie =
+                logical_router_port_cookie_alloc(nbrp, paired_lr);
+            struct ovn_unpaired_port_binding *upb;
+            upb = ovn_unpaired_port_binding_alloc(requested_tunnel_key,
+                                                  nbrp->name,
+                                                  LRP_TYPE_NAME,
+                                                  cookie,
+                                                  paired_lr->sb);
+            smap_clone(&upb->external_ids, &nbrp->external_ids);
+            if (!shash_add_once(&map->ports, nbrp->name, upb)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "duplicate logical router port %s",
+                             nbrp->name);
+            }
+        }
+    }
+
+    return EN_UPDATED;
+}
+
+void
+en_port_binding_logical_router_port_cleanup(void *data)
+{
+    struct ovn_unpaired_port_binding_map *map = data;
+    unpaired_logical_router_port_map_destroy(map);
+}
+
+static void
+paired_logical_router_port_map_init(
+    struct ovn_paired_logical_router_port_map *router_port_map)
+{
+    shash_init(&router_port_map->paired_router_ports);
+}
+
+static void
+paired_logical_router_port_map_destroy(
+    struct ovn_paired_logical_router_port_map *router_port_map)
+{
+    shash_destroy_free_data(&router_port_map->paired_router_ports);
+}
+
+void *
+en_port_binding_paired_logical_router_port_init(
+    struct engine_node *node OVS_UNUSED, struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_paired_logical_router_port_map *router_port_map;
+    router_port_map = xzalloc(sizeof *router_port_map);
+    paired_logical_router_port_map_init(router_port_map);
+
+    return router_port_map;
+}
+
+enum engine_node_state
+en_port_binding_paired_logical_router_port_run(struct engine_node *node,
+                                               void *data)
+{
+    const struct ovn_paired_port_bindings *pbs =
+        engine_get_input_data("port_binding_pair", node);
+    struct ovn_paired_logical_router_port_map *router_port_map = data;
+
+    paired_logical_router_port_map_destroy(router_port_map);
+    paired_logical_router_port_map_init(router_port_map);
+
+    struct ovn_paired_port_binding *spb;
+    LIST_FOR_EACH (spb, list_node, &pbs->paired_pbs) {
+        if (strcmp(spb->type, LRP_TYPE_NAME)) {
+            continue;
+        }
+        const struct logical_router_port_cookie *cookie = spb->cookie;
+        struct ovn_paired_logical_router_port *lrp = xzalloc(sizeof *lrp);
+        lrp->nb = cookie->nbrp;
+        lrp->router = cookie->router;
+        lrp->sb = spb->sb_pb;
+        shash_add(&router_port_map->paired_router_ports, lrp->nb->name, lrp);
+    }
+
+    return EN_UPDATED;
+}
+
+void en_port_binding_paired_logical_router_port_cleanup(void *data)
+{
+    struct ovn_paired_logical_router_port_map *map = data;
+    paired_logical_router_port_map_destroy(map);
+}
diff --git a/northd/en-port-binding-logical-router-port.h b/northd/en-port-binding-logical-router-port.h
new file mode 100644
index 000000000..156a25da6
--- /dev/null
+++ b/northd/en-port-binding-logical-router-port.h
@@ -0,0 +1,47 @@ 
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EN_PORT_BINDING_LOGICAL_ROUTER_PORT_H
+#define EN_PORT_BINDING_LOGICAL_ROUTER_PORT_H
+
+#include "lib/inc-proc-eng.h"
+#include "openvswitch/shash.h"
+
+void *en_port_binding_logical_router_port_init(struct engine_node *,
+                                           struct engine_arg *);
+
+enum engine_node_state en_port_binding_logical_router_port_run(
+    struct engine_node *, void *data);
+void en_port_binding_logical_router_port_cleanup(void *data);
+
+struct ovn_paired_logical_router_port {
+    const struct nbrec_logical_router_port *nb;
+    const struct sbrec_port_binding *sb;
+    const struct ovn_synced_logical_router *router;
+};
+
+struct ovn_paired_logical_router_port_map {
+    struct shash paired_router_ports;
+};
+
+void *en_port_binding_paired_logical_router_port_init(struct engine_node *,
+                                                      struct engine_arg *);
+
+enum engine_node_state en_port_binding_paired_logical_router_port_run(
+    struct engine_node *, void *data);
+void en_port_binding_paired_logical_router_port_cleanup(void *data);
+
+#endif /* EN_PORT_BINDING_LOGICAL_ROUTER_PORT_H */
diff --git a/northd/en-port-binding-logical-switch-port.c b/northd/en-port-binding-logical-switch-port.c
new file mode 100644
index 000000000..d4fcece5f
--- /dev/null
+++ b/northd/en-port-binding-logical-switch-port.c
@@ -0,0 +1,231 @@ 
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "openvswitch/hmap.h"
+#include "openvswitch/vlog.h"
+#include "util.h"
+
+#include "inc-proc-eng.h"
+#include "ovn-nb-idl.h"
+#include "ovn-sb-idl.h"
+#include "port_binding_pair.h"
+#include "en-datapath-logical-switch.h"
+#include "en-port-binding-logical-switch-port.h"
+#include "northd.h"
+
+#define LSP_TYPE_NAME "logical-switch-port"
+
+VLOG_DEFINE_THIS_MODULE(en_port_binding_logical_switch_port);
+
+struct logical_switch_port_cookie {
+    const struct nbrec_logical_switch_port *nbsp;
+    const struct ovn_synced_logical_switch *sw;
+};
+
+static struct logical_switch_port_cookie *
+logical_switch_port_cookie_alloc(const struct nbrec_logical_switch_port *nbsp,
+                                 const struct ovn_synced_logical_switch *sw)
+{
+    struct logical_switch_port_cookie *cookie = xmalloc(sizeof *cookie);
+    cookie->nbsp = nbsp;
+    cookie->sw = sw;
+    return cookie;
+}
+
+static void
+logical_switch_port_cookie_free(struct logical_switch_port_cookie *cookie)
+{
+    free(cookie);
+}
+
+static bool
+switch_port_sb_is_valid(const struct sbrec_port_binding *sb_pb,
+                        const struct ovn_unpaired_port_binding *upb)
+{
+    const struct logical_switch_port_cookie *cookie = upb->cookie;
+
+    bool update_sbrec = false;
+    if (lsp_is_type_changed(sb_pb, cookie->nbsp, &update_sbrec)
+        && update_sbrec) {
+        return false;
+    }
+
+    return true;
+}
+
+struct ovn_unpaired_port_binding_map_callbacks switch_port_callbacks = {
+    .sb_is_valid = switch_port_sb_is_valid,
+};
+
+static void
+unpaired_logical_switch_port_map_destroy(
+    struct ovn_unpaired_port_binding_map *map)
+{
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &map->ports) {
+        struct ovn_unpaired_port_binding *upb = node->data;
+        logical_switch_port_cookie_free(upb->cookie);
+    }
+    ovn_unpaired_port_binding_map_destroy(map);
+}
+
+void *
+en_port_binding_logical_switch_port_init(struct engine_node *node OVS_UNUSED,
+                                         struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
+    ovn_unpaired_port_binding_map_init(map, &switch_port_callbacks);
+    return map;
+}
+
+enum engine_node_state
+en_port_binding_logical_switch_port_run(struct engine_node *node, void *data)
+{
+    const struct ovn_synced_logical_switch_map *ls_map =
+        engine_get_input_data("datapath_synced_logical_switch", node);
+
+    struct ovn_unpaired_port_binding_map *map = data;
+
+    unpaired_logical_switch_port_map_destroy(map);
+    ovn_unpaired_port_binding_map_init(map, &switch_port_callbacks);
+
+    const struct ovn_synced_logical_switch *paired_ls;
+    HMAP_FOR_EACH (paired_ls, hmap_node, &ls_map->synced_switches) {
+        const struct nbrec_logical_switch_port *nbsp;
+        for (size_t i = 0; i < paired_ls->nb->n_ports; i++) {
+            nbsp = paired_ls->nb->ports[i];
+            uint32_t requested_tunnel_key = smap_get_int(&nbsp->options,
+                                                         "requested-tnl-key",
+                                                         0);
+            struct logical_switch_port_cookie *cookie =
+                logical_switch_port_cookie_alloc(nbsp, paired_ls);
+            struct ovn_unpaired_port_binding *upb;
+            upb = ovn_unpaired_port_binding_alloc(requested_tunnel_key,
+                                                  nbsp->name,
+                                                  LSP_TYPE_NAME,
+                                                  cookie,
+                                                  paired_ls->sb);
+            smap_clone(&upb->external_ids, &nbsp->external_ids);
+            const char *name = smap_get(&nbsp->external_ids,
+                                        "neutron:port_name");
+            if (name && name[0]) {
+                smap_add(&upb->external_ids, "name", name);
+            }
+            if (!shash_add_once(&map->ports, nbsp->name, upb)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "duplicate logical port %s", nbsp->name);
+            }
+        }
+    }
+    return EN_UPDATED;
+}
+
+void
+en_port_binding_logical_switch_port_cleanup(void *data)
+{
+    struct ovn_unpaired_port_binding_map *map = data;
+    unpaired_logical_switch_port_map_destroy(map);
+}
+
+static void
+paired_logical_switch_port_map_init(
+    struct ovn_paired_logical_switch_port_map *switch_port_map)
+{
+    shash_init(&switch_port_map->paired_switch_ports);
+}
+
+static void
+paired_logical_switch_port_map_destroy(
+    struct ovn_paired_logical_switch_port_map *switch_port_map)
+{
+    shash_destroy_free_data(&switch_port_map->paired_switch_ports);
+}
+
+void *
+en_port_binding_paired_logical_switch_port_init(
+    struct engine_node *node OVS_UNUSED, struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_paired_logical_switch_port_map *switch_port_map;
+    switch_port_map = xzalloc(sizeof *switch_port_map);
+    paired_logical_switch_port_map_init(switch_port_map);
+
+    return switch_port_map;
+}
+
+enum engine_node_state
+en_port_binding_paired_logical_switch_port_run(struct engine_node *node,
+                                               void *data)
+{
+    const struct ovn_paired_port_bindings *pbs =
+        engine_get_input_data("port_binding_pair", node);
+    struct ovn_paired_logical_switch_port_map *switch_port_map = data;
+
+    paired_logical_switch_port_map_destroy(switch_port_map);
+    paired_logical_switch_port_map_init(switch_port_map);
+
+    struct ovn_paired_port_binding *spb;
+    LIST_FOR_EACH (spb, list_node, &pbs->paired_pbs) {
+        if (strcmp(spb->type, LSP_TYPE_NAME)) {
+            continue;
+        }
+        const struct logical_switch_port_cookie *cookie = spb->cookie;
+        struct ovn_paired_logical_switch_port *lsw = xzalloc(sizeof *lsw);
+        lsw->nb = cookie->nbsp;
+        lsw->sw = cookie->sw;
+        lsw->sb = spb->sb_pb;
+        shash_add(&switch_port_map->paired_switch_ports, lsw->nb->name, lsw);
+
+        /* This deals with a special case where a logical switch port is
+         * removed and added back very quickly. The sequence of events is as
+         * follows:
+         * 1) NB Logical_Switch_Port "lsp" is added to the NB DB.
+         * 2) en-port-binding-pair creates a corresponding SB Port_Binding.
+         * 3) The user binds the port to a hypervisor.
+         * 4) ovn-controller on the hypervisor sets the SB Port_Binding "up"
+         *    column to "true".
+         * 5) ovn-northd sets the Logical_Switch_Port "up" column to "true".
+         * 6) A user deletes and then re-adds "lsp" back to the NB
+         *    Logical_Switch_Port column very quickly, so quickly that we
+         *    do not detect the deletion at all.
+         * 7) The new northbound Logical_Switch_Port has its "up" column
+         *    empty (i.e. not "true") since it is new.
+         * 8) The pairing code matches the new Logical_Switch_Port with the
+         *    existing Port_Binding for "lsp" since the pairing code matches
+         *    using the name of the Logical_Switch_Port.
+         *
+         * At this point, the SB Port_Binding's "up" column is set "true",
+         * but the NB Logical_Switch_Port's "up" column is not. We need to
+         * ensure the NB Logical_Switch_Port's "up" column is set to "true"
+         * as well.
+         *
+         * In most cases, setting the NB Logical_Switch_Port "up" column to
+         * true is accomplished when changes on the SB Port_Binding are
+         * detected. But in this rare case, there is no SB Port_Binding
+         * change, so the "up" column is unserviced.
+         */
+        lsp_set_up(lsw->sb, lsw->nb);
+    }
+
+    return EN_UPDATED;
+}
+
+void en_port_binding_paired_logical_switch_port_cleanup(void *data)
+{
+    struct ovn_paired_logical_switch_port_map *map = data;
+    paired_logical_switch_port_map_destroy(map);
+}
diff --git a/northd/en-port-binding-logical-switch-port.h b/northd/en-port-binding-logical-switch-port.h
new file mode 100644
index 000000000..9ef32ce88
--- /dev/null
+++ b/northd/en-port-binding-logical-switch-port.h
@@ -0,0 +1,48 @@ 
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EN_PORT_BINDING_LOGICAL_SWITCH_PORT_H
+#define EN_PORT_BINDING_LOGICAL_SWITCH_PORT_H
+
+#include "lib/inc-proc-eng.h"
+#include "openvswitch/shash.h"
+
+
+void *en_port_binding_logical_switch_port_init(struct engine_node *,
+                                           struct engine_arg *);
+
+enum engine_node_state en_port_binding_logical_switch_port_run(
+    struct engine_node *, void *data);
+void en_port_binding_logical_switch_port_cleanup(void *data);
+
+struct ovn_paired_logical_switch_port {
+    const struct nbrec_logical_switch_port *nb;
+    const struct sbrec_port_binding *sb;
+    const struct ovn_synced_logical_switch *sw;
+};
+
+struct ovn_paired_logical_switch_port_map {
+    struct shash paired_switch_ports;
+};
+
+void *en_port_binding_paired_logical_switch_port_init(struct engine_node *,
+                                                      struct engine_arg *);
+
+enum engine_node_state en_port_binding_paired_logical_switch_port_run(
+    struct engine_node *, void *data);
+void en_port_binding_paired_logical_switch_port_cleanup(void *data);
+
+#endif /* EN_PORT_BINDING_LOGICAL_SWITCH_PORT_H */
diff --git a/northd/en-port-binding-mirror.c b/northd/en-port-binding-mirror.c
new file mode 100644
index 000000000..6e8647dba
--- /dev/null
+++ b/northd/en-port-binding-mirror.c
@@ -0,0 +1,191 @@ 
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <config.h>
+#include "ovn-util.h"
+#include "lib/inc-proc-eng.h"
+#include "ovn-nb-idl.h"
+#include "en-datapath-logical-switch.h"
+#include "en-port-binding-mirror.h"
+#include "port_binding_pair.h"
+#include "northd.h"
+#include "openvswitch/vlog.h"
+
+#define MIRROR_PORT_TYPE "mirror"
+
+VLOG_DEFINE_THIS_MODULE(en_port_binding_mirror);
+
+struct mirror_port {
+    char *name;
+    const char *sink;
+    const struct nbrec_logical_switch_port *nbsp;
+};
+
+static struct mirror_port *
+mirror_port_alloc(const struct sbrec_datapath_binding *sb, const char *sink,
+                  const struct nbrec_logical_switch_port *nbsp)
+{
+    struct mirror_port *mp = xzalloc(sizeof *mp);
+    mp->name = ovn_mirror_port_name(ovn_datapath_name(sb), sink);
+    mp->sink = sink;
+    mp->nbsp = nbsp;
+
+    return mp;
+}
+
+static void
+mirror_port_free(struct mirror_port *mp)
+{
+    free(mp->name);
+    free(mp);
+}
+
+static void
+unpaired_mirror_map_destroy(struct ovn_unpaired_port_binding_map *map)
+{
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &map->ports) {
+        struct ovn_unpaired_port_binding *upb = node->data;
+        mirror_port_free(upb->cookie);
+    }
+    ovn_unpaired_port_binding_map_destroy(map);
+}
+
+void *
+en_port_binding_mirror_init(struct engine_node *node OVS_UNUSED,
+                            struct engine_arg *arg OVS_UNUSED)
+{
+    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+    return map;
+}
+
+enum engine_node_state
+en_port_binding_mirror_run(struct engine_node *node, void *data)
+{
+    const struct ovn_synced_logical_switch_map *ls_map =
+        engine_get_input_data("datapath_synced_logical_switch", node);
+    struct ovn_unpaired_port_binding_map *map = data;
+
+    unpaired_mirror_map_destroy(map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+
+    /* Typically, we'd use an ovsdb_idl_index to search for a specific record
+     * based on a column value. However, we currently are not monitoring
+     * the Logical_Switch_Port table at all in ovn-northd. Introducing
+     * this monitoring is likely more computationally intensive than
+     * making an on-the-fly sset of logical switch port names.
+     */
+    struct sset all_switch_ports = SSET_INITIALIZER(&all_switch_ports);
+    const struct ovn_synced_logical_switch *ls;
+    HMAP_FOR_EACH (ls, hmap_node, &ls_map->synced_switches) {
+        for (size_t i = 0; i < ls->nb->n_ports; i++) {
+            sset_add(&all_switch_ports, ls->nb->ports[i]->name);
+        }
+    }
+
+    HMAP_FOR_EACH (ls, hmap_node, &ls_map->synced_switches) {
+        for (size_t i = 0; i < ls->nb->n_ports; i++) {
+            const struct nbrec_logical_switch_port *nbsp = ls->nb->ports[i];
+            for (size_t j = 0; j < nbsp->n_mirror_rules; j++) {
+                struct nbrec_mirror *nb_mirror = nbsp->mirror_rules[j];
+                if (strcmp(nb_mirror->type, "lport")) {
+                    continue;
+                }
+                if (!sset_find(&all_switch_ports, nb_mirror->sink)) {
+                    continue;
+                }
+                struct mirror_port *mp = mirror_port_alloc(ls->sb,
+                                                           nb_mirror->sink,
+                                                           nbsp);
+                struct ovn_unpaired_port_binding *upb;
+                upb = ovn_unpaired_port_binding_alloc(0, mp->name,
+                                                      MIRROR_PORT_TYPE, mp,
+                                                      ls->sb);
+                shash_add(&map->ports, mp->name, upb);
+            }
+        }
+    }
+    sset_destroy(&all_switch_ports);
+
+    return EN_UPDATED;
+}
+
+void
+en_port_binding_mirror_cleanup(void *data)
+{
+    struct ovn_unpaired_port_binding_map *map = data;
+    unpaired_mirror_map_destroy(map);
+}
+
+static void
+ovn_paired_mirror_map_init(
+    struct ovn_paired_mirror_map *map)
+{
+    shash_init(&map->paired_mirror_ports);
+}
+
+static void
+ovn_paired_mirror_map_destroy(
+    struct ovn_paired_mirror_map *map)
+{
+    shash_destroy_free_data(&map->paired_mirror_ports);
+}
+
+void *
+en_port_binding_paired_mirror_init(struct engine_node *node OVS_UNUSED,
+                                   struct engine_arg *arg OVS_UNUSED)
+{
+    struct ovn_paired_mirror_map *map = xzalloc(sizeof *map);
+    ovn_paired_mirror_map_init(map);
+    return map;
+}
+
+enum engine_node_state
+en_port_binding_paired_mirror_run(struct engine_node *node,
+                                  void *data)
+{
+    const struct ovn_paired_port_bindings *pbs =
+        engine_get_input_data("port_binding_pair", node);
+    struct ovn_paired_mirror_map *map = data;
+
+    ovn_paired_mirror_map_destroy(map);
+    ovn_paired_mirror_map_init(map);
+
+    struct ovn_paired_port_binding *port;
+    LIST_FOR_EACH (port, list_node, &pbs->paired_pbs) {
+        if (strcmp(port->type, MIRROR_PORT_TYPE)) {
+            continue;
+        }
+        const struct mirror_port *mp = port->cookie;
+        struct ovn_paired_mirror *opm = xmalloc(sizeof *opm);
+        opm->name = mp->name;
+        opm->sink = mp->sink;
+        opm->sb = port->sb_pb;
+        opm->nbsp = mp->nbsp;
+        shash_add(&map->paired_mirror_ports, opm->name, opm);
+    }
+
+    return EN_UPDATED;
+}
+
+void
+en_port_binding_paired_mirror_cleanup(void *data)
+{
+    struct ovn_paired_mirror_map *map = data;
+
+    ovn_paired_mirror_map_destroy(map);
+}
+
diff --git a/northd/en-port-binding-mirror.h b/northd/en-port-binding-mirror.h
new file mode 100644
index 000000000..a4bf2645a
--- /dev/null
+++ b/northd/en-port-binding-mirror.h
@@ -0,0 +1,48 @@ 
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EN_PORT_BINDING_MIRROR_H
+#define EN_PORT_BINDING_MIRROR_H
+
+#include "lib/inc-proc-eng.h"
+#include "openvswitch/shash.h"
+
+void *en_port_binding_mirror_init(struct engine_node *,
+                                  struct engine_arg *);
+
+enum engine_node_state en_port_binding_mirror_run(struct engine_node *,
+                                                  void *data);
+void en_port_binding_mirror_cleanup(void *data);
+
+struct ovn_paired_mirror {
+    const char *name;
+    const char *sink;
+    const struct nbrec_logical_switch_port *nbsp;
+    const struct sbrec_port_binding *sb;
+};
+
+struct ovn_paired_mirror_map {
+    struct shash paired_mirror_ports;
+};
+
+void *en_port_binding_paired_mirror_init(struct engine_node *,
+                                         struct engine_arg *);
+
+enum engine_node_state en_port_binding_paired_mirror_run(struct engine_node *,
+                                                         void *data);
+void en_port_binding_paired_mirror_cleanup(void *data);
+
+#endif /* EN_PORT_BINDING_MIRROR_H */
diff --git a/northd/en-port-binding-pair.c b/northd/en-port-binding-pair.c
new file mode 100644
index 000000000..21f7fefc8
--- /dev/null
+++ b/northd/en-port-binding-pair.c
@@ -0,0 +1,467 @@ 
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "en-port-binding-pair.h"
+#include "en-global-config.h"
+#include "port_binding_pair.h"
+#include "ovn-sb-idl.h"
+#include "mcast-group-index.h"
+
+#include "openvswitch/vlog.h"
+
+VLOG_DEFINE_THIS_MODULE(port_binding_pair);
+
+void *
+en_port_binding_pair_init(struct engine_node *node OVS_UNUSED,
+                      struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_paired_port_bindings *paired_port_bindings
+        = xzalloc(sizeof *paired_port_bindings);
+    ovs_list_init(&paired_port_bindings->paired_pbs);
+    hmap_init(&paired_port_bindings->tunnel_key_maps);
+
+    return paired_port_bindings;
+}
+
+static struct ovn_unpaired_port_binding *
+find_unpaired_port_binding(const struct ovn_unpaired_port_binding_map **maps,
+                           size_t n_maps,
+                           const struct sbrec_port_binding *sb_pb)
+{
+    const struct ovn_unpaired_port_binding_map *map;
+
+    for (size_t i = 0; i < n_maps; i++) {
+        map = maps[i];
+        struct ovn_unpaired_port_binding *upb;
+        upb = shash_find_data(&map->ports, sb_pb->logical_port);
+        if (upb && map->cb->sb_is_valid(sb_pb, upb)) {
+            return upb;
+        }
+    }
+
+    return NULL;
+}
+
+struct tunnel_key_map {
+    struct hmap_node hmap_node;
+    uint32_t datapath_tunnel_key;
+    struct hmap port_tunnel_keys;
+};
+
+static struct tunnel_key_map *
+find_tunnel_key_map(uint32_t datapath_tunnel_key,
+                    const struct hmap *tunnel_key_maps)
+{
+    uint32_t hash = hash_int(datapath_tunnel_key, 0);
+    struct tunnel_key_map *key_map;
+    HMAP_FOR_EACH_WITH_HASH (key_map, hmap_node, hash, tunnel_key_maps) {
+        if (key_map->datapath_tunnel_key == datapath_tunnel_key) {
+            return key_map;
+        }
+    }
+    return NULL;
+}
+
+static struct tunnel_key_map *
+alloc_tunnel_key_map(uint32_t datapath_tunnel_key,
+                     struct hmap *tunnel_key_maps)
+{
+    uint32_t hash = hash_int(datapath_tunnel_key, 0);
+    struct tunnel_key_map *key_map;
+
+    key_map = xzalloc(sizeof *key_map);
+    key_map->datapath_tunnel_key = datapath_tunnel_key;
+    hmap_init(&key_map->port_tunnel_keys);
+    hmap_insert(tunnel_key_maps, &key_map->hmap_node, hash);
+
+    return key_map;
+
+}
+
+static struct tunnel_key_map *
+find_or_alloc_tunnel_key_map(const struct sbrec_datapath_binding *sb_dp,
+                             struct hmap *tunnel_key_maps)
+{
+    struct tunnel_key_map *key_map = find_tunnel_key_map(sb_dp->tunnel_key,
+                                                         tunnel_key_maps);
+    if (!key_map) {
+        key_map = alloc_tunnel_key_map(sb_dp->tunnel_key, tunnel_key_maps);
+    }
+    return key_map;
+}
+
+static void
+tunnel_key_maps_destroy(struct hmap *tunnel_key_maps)
+{
+    struct tunnel_key_map *key_map;
+    HMAP_FOR_EACH_POP (key_map, hmap_node, tunnel_key_maps) {
+        hmap_destroy(&key_map->port_tunnel_keys);
+        free(key_map);
+    }
+    hmap_destroy(tunnel_key_maps);
+}
+
+struct candidate_spb {
+    struct ovs_list list_node;
+    struct ovn_paired_port_binding *spb;
+    uint32_t requested_tunnel_key;
+    uint32_t existing_tunnel_key;
+    struct tunnel_key_map *tunnel_key_map;
+};
+
+static void
+reset_port_binding_pair_data(
+    struct ovn_paired_port_bindings *paired_port_bindings)
+{
+    /* Free the old paired port_bindings */
+    struct ovn_paired_port_binding *spb;
+    LIST_FOR_EACH_POP (spb, list_node, &paired_port_bindings->paired_pbs) {
+        free(spb);
+    }
+    tunnel_key_maps_destroy(&paired_port_bindings->tunnel_key_maps);
+
+    hmap_init(&paired_port_bindings->tunnel_key_maps);
+    ovs_list_init(&paired_port_bindings->paired_pbs);
+}
+
+static struct candidate_spb *
+candidate_spb_alloc(const struct ovn_unpaired_port_binding *upb,
+                    const struct sbrec_port_binding *sb_pb,
+                    struct hmap *tunnel_key_maps)
+{
+    struct ovn_paired_port_binding *spb;
+    spb = xzalloc(sizeof *spb);
+    spb->sb_pb = sb_pb;
+    spb->cookie = upb->cookie;
+    spb->type = upb->type;
+    sbrec_port_binding_set_external_ids(sb_pb, &upb->external_ids);
+    sbrec_port_binding_set_logical_port(sb_pb, upb->name);
+
+    struct candidate_spb *candidate;
+    candidate = xzalloc(sizeof *candidate);
+    candidate->spb = spb;
+    candidate->requested_tunnel_key = upb->requested_tunnel_key;
+    candidate->existing_tunnel_key = spb->sb_pb->tunnel_key;
+    candidate->tunnel_key_map = find_or_alloc_tunnel_key_map(upb->sb_dp,
+                                                             tunnel_key_maps);
+
+    return candidate;
+}
+
+static void
+get_candidate_pbs_from_sb(
+    const struct sbrec_port_binding_table *sb_pb_table,
+    const struct ovn_unpaired_port_binding_map **input_maps,
+    size_t n_input_maps, struct hmap *tunnel_key_maps,
+    struct ovs_list *candidate_spbs, struct smap *visited)
+{
+    const struct sbrec_port_binding *sb_pb;
+    const struct ovn_unpaired_port_binding *upb;
+    SBREC_PORT_BINDING_TABLE_FOR_EACH_SAFE (sb_pb, sb_pb_table) {
+        upb = find_unpaired_port_binding(input_maps, n_input_maps, sb_pb);
+        if (!upb) {
+            sbrec_port_binding_delete(sb_pb);
+            continue;
+        }
+
+        if (!uuid_equals(&upb->sb_dp->header_.uuid,
+            &sb_pb->datapath->header_.uuid)) {
+            /* A matching unpaired port was found for this port binding, but it
+             * has moved to a different datapath. Delete the old SB port
+             * binding so that a new one will be created later when we traverse
+             * unpaired port bindings later.
+             */
+            sbrec_port_binding_delete(sb_pb);
+            continue;
+        }
+
+        if (!smap_add_once(visited, sb_pb->logical_port, upb->type)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_INFO_RL(
+                &rl, "deleting port_binding "UUID_FMT" with "
+                "duplicate name %s",
+                UUID_ARGS(&sb_pb->header_.uuid), sb_pb->logical_port);
+            sbrec_port_binding_delete(sb_pb);
+            continue;
+        }
+        struct candidate_spb *candidate;
+        candidate = candidate_spb_alloc(upb, sb_pb, tunnel_key_maps);
+        ovs_list_push_back(candidate_spbs, &candidate->list_node);
+    }
+}
+
+static void
+get_candidate_pbs_from_nb(
+    struct ovsdb_idl_txn *ovnsb_idl_txn,
+    const struct ovn_unpaired_port_binding_map **input_maps,
+    uint32_t n_input_maps,
+    struct hmap *tunnel_key_maps,
+    struct ovs_list *candidate_spbs,
+    struct smap *visited)
+{
+    for (size_t i = 0; i < n_input_maps; i++) {
+        const struct ovn_unpaired_port_binding_map *map = input_maps[i];
+        struct shash_node *shash_node;
+        SHASH_FOR_EACH (shash_node, &map->ports) {
+            const struct ovn_unpaired_port_binding *upb = shash_node->data;
+            const char *visited_type = smap_get(visited, upb->name);
+            if (visited_type) {
+                if (strcmp(upb->type, visited_type)) {
+                    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
+                                                                            1);
+                    VLOG_WARN_RL(&rl, "duplicate logical port %s", upb->name);
+                }
+                continue;
+            } else {
+                /* Add the port to "visited" to help with detection of
+                 * duplicated port names across different types of ports.
+                 */
+                smap_add_once(visited, upb->name, upb->type);
+            }
+            const struct sbrec_port_binding *sb_pb;
+            sb_pb = sbrec_port_binding_insert(ovnsb_idl_txn);
+
+            struct candidate_spb *candidate;
+            candidate = candidate_spb_alloc(upb, sb_pb, tunnel_key_maps);
+            ovs_list_push_back(candidate_spbs, &candidate->list_node);
+        }
+    }
+}
+
+static void
+pair_requested_tunnel_keys(struct ovs_list *candidate_spbs,
+                           struct ovs_list *paired_pbs)
+{
+    struct candidate_spb *candidate;
+    LIST_FOR_EACH_SAFE (candidate, list_node, candidate_spbs) {
+        if (!candidate->requested_tunnel_key) {
+            continue;
+        }
+        if (candidate->requested_tunnel_key >= OVN_VXLAN_MIN_MULTICAST) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+            VLOG_WARN_RL(&rl, "Tunnel key %"PRIu32" for port %s"
+                         " is incompatible with VXLAN",
+                         candidate->requested_tunnel_key,
+                         candidate->spb->sb_pb->logical_port);
+            continue;
+        }
+
+        if (ovn_add_tnlid(&candidate->tunnel_key_map->port_tunnel_keys,
+                          candidate->requested_tunnel_key)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+            VLOG_WARN_RL(&rl, "Logical port_binding %s requests same "
+                         "tunnel key %"PRIu32" as another logical "
+                         "port_binding on the same datapath",
+                         candidate->spb->sb_pb->logical_port,
+                         candidate->requested_tunnel_key);
+        }
+        sbrec_port_binding_set_tunnel_key(candidate->spb->sb_pb,
+                                          candidate->requested_tunnel_key);
+        ovs_list_remove(&candidate->list_node);
+        ovs_list_push_back(paired_pbs, &candidate->spb->list_node);
+        free(candidate);
+    }
+}
+
+static void
+pair_existing_tunnel_keys(struct ovs_list *candidate_spbs,
+                          struct ovs_list *paired_pbs)
+{
+    struct candidate_spb *candidate;
+    LIST_FOR_EACH_SAFE (candidate, list_node, candidate_spbs) {
+        if (!candidate->existing_tunnel_key) {
+            continue;
+        }
+        /* Existing southbound pb. If this key is available,
+         * reuse it.
+         */
+        if (ovn_add_tnlid(&candidate->tunnel_key_map->port_tunnel_keys,
+                          candidate->existing_tunnel_key)) {
+            ovs_list_remove(&candidate->list_node);
+            ovs_list_push_back(paired_pbs, &candidate->spb->list_node);
+            free(candidate);
+        }
+    }
+}
+
+static void
+pair_new_tunnel_keys(struct ovs_list *candidate_spbs,
+                     struct ovs_list *paired_pbs,
+                     uint32_t max_pb_tunnel_id)
+{
+    uint32_t hint = 0;
+    struct candidate_spb *candidate;
+    LIST_FOR_EACH_SAFE (candidate, list_node, candidate_spbs) {
+        uint32_t tunnel_key =
+            ovn_allocate_tnlid(&candidate->tunnel_key_map->port_tunnel_keys,
+                               "port", 1, max_pb_tunnel_id,
+                               &hint);
+        if (!tunnel_key) {
+            continue;
+        }
+        sbrec_port_binding_set_tunnel_key(candidate->spb->sb_pb,
+                                              tunnel_key);
+        ovs_list_remove(&candidate->list_node);
+        ovs_list_push_back(paired_pbs, &candidate->spb->list_node);
+        free(candidate);
+    }
+
+}
+
+static void
+free_unpaired_candidates(struct ovs_list *candidate_spbs)
+{
+    struct candidate_spb *candidate;
+    /* Anything from this list represents a port_binding where a tunnel ID
+     * could not be allocated. Delete the SB port_binding binding for these.
+     */
+    LIST_FOR_EACH_POP (candidate, list_node, candidate_spbs) {
+        sbrec_port_binding_delete(candidate->spb->sb_pb);
+        free(candidate->spb);
+        free(candidate);
+    }
+}
+
+static void
+cleanup_stale_fdb_entries(const struct sbrec_fdb_table *sbrec_fdb_table,
+                          struct hmap *tunnel_key_maps)
+{
+    const struct sbrec_fdb *fdb_e;
+    SBREC_FDB_TABLE_FOR_EACH_SAFE (fdb_e, sbrec_fdb_table) {
+        bool delete = true;
+        struct tunnel_key_map *map = find_tunnel_key_map(fdb_e->dp_key,
+                                                         tunnel_key_maps);
+        if (map) {
+            if (ovn_tnlid_present(&map->port_tunnel_keys, fdb_e->port_key)) {
+                delete = false;
+            }
+        }
+
+        if (delete) {
+            sbrec_fdb_delete(fdb_e);
+        }
+    }
+}
+
+enum engine_node_state
+en_port_binding_pair_run(struct engine_node *node , void *data)
+{
+    const struct sbrec_port_binding_table *sb_pb_table =
+        EN_OVSDB_GET(engine_get_input("SB_port_binding", node));
+    const struct sbrec_fdb_table *sb_fdb_table =
+        EN_OVSDB_GET(engine_get_input("SB_fdb", node));
+    const struct ed_type_global_config *global_config =
+        engine_get_input_data("global_config", node);
+    /* The inputs are:
+     * * Some number of input maps.
+     * * Southbound Port Binding table.
+     * * Global config data.
+     * * FDB Table.
+     *
+     * Therefore, the number of inputs - 3 is the number of input
+     * maps from the port_binding-specific nodes.
+     */
+    size_t n_input_maps = node->n_inputs - 3;
+    const struct ovn_unpaired_port_binding_map **input_maps =
+        xmalloc(n_input_maps *sizeof *input_maps);
+    struct ovn_paired_port_bindings *paired_port_bindings = data;
+
+    for (size_t i = 0; i < n_input_maps; i++) {
+        input_maps[i] = engine_get_data(node->inputs[i].node);
+    }
+
+    reset_port_binding_pair_data(paired_port_bindings);
+
+    struct smap visited = SMAP_INITIALIZER(&visited);
+    struct ovs_list candidate_spbs = OVS_LIST_INITIALIZER(&candidate_spbs);
+    get_candidate_pbs_from_sb(sb_pb_table, input_maps, n_input_maps,
+                              &paired_port_bindings->tunnel_key_maps,
+                              &candidate_spbs, &visited);
+
+    const struct engine_context *eng_ctx = engine_get_context();
+    get_candidate_pbs_from_nb(eng_ctx->ovnsb_idl_txn, input_maps,
+                              n_input_maps,
+                              &paired_port_bindings->tunnel_key_maps,
+                              &candidate_spbs, &visited);
+
+    smap_destroy(&visited);
+
+    pair_requested_tunnel_keys(&candidate_spbs,
+                               &paired_port_bindings->paired_pbs);
+    pair_existing_tunnel_keys(&candidate_spbs,
+                              &paired_port_bindings->paired_pbs);
+    pair_new_tunnel_keys(&candidate_spbs, &paired_port_bindings->paired_pbs,
+                         global_config->max_pb_tunnel_id);
+
+    cleanup_stale_fdb_entries(sb_fdb_table,
+                              &paired_port_bindings->tunnel_key_maps);
+
+    free_unpaired_candidates(&candidate_spbs);
+    free(input_maps);
+
+    return EN_UPDATED;
+}
+
+void
+en_port_binding_pair_cleanup(void *data)
+{
+    struct ovn_paired_port_bindings *paired_port_bindings = data;
+    struct ovn_paired_port_binding *spb;
+
+    LIST_FOR_EACH_POP (spb, list_node, &paired_port_bindings->paired_pbs) {
+        free(spb);
+    }
+    tunnel_key_maps_destroy(&paired_port_bindings->tunnel_key_maps);
+}
+
+enum engine_input_handler_result
+port_binding_fdb_change_handler(struct engine_node *node, void *data)
+{
+    struct ovn_paired_port_bindings *paired_port_bindings = data;
+    const struct sbrec_fdb_table *sbrec_fdb_table =
+        EN_OVSDB_GET(engine_get_input("SB_fdb", node));
+
+    /* check if changed rows are stale and delete them */
+    const struct sbrec_fdb *fdb_e, *fdb_prev_del = NULL;
+    SBREC_FDB_TABLE_FOR_EACH_TRACKED (fdb_e, sbrec_fdb_table) {
+        if (sbrec_fdb_is_deleted(fdb_e)) {
+            continue;
+        }
+
+        if (fdb_prev_del) {
+            sbrec_fdb_delete(fdb_prev_del);
+        }
+
+        fdb_prev_del = fdb_e;
+        struct tunnel_key_map *tunnel_key_map =
+            find_tunnel_key_map(fdb_e->dp_key,
+                                &paired_port_bindings->tunnel_key_maps);
+        if (tunnel_key_map) {
+            if (ovn_tnlid_present(&tunnel_key_map->port_tunnel_keys,
+                                  fdb_e->port_key)) {
+                fdb_prev_del = NULL;
+            }
+        }
+    }
+
+    if (fdb_prev_del) {
+        sbrec_fdb_delete(fdb_prev_del);
+    }
+
+    return EN_HANDLED_UNCHANGED;
+}
diff --git a/northd/en-port-binding-pair.h b/northd/en-port-binding-pair.h
new file mode 100644
index 000000000..9b9417487
--- /dev/null
+++ b/northd/en-port-binding-pair.h
@@ -0,0 +1,34 @@ 
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EN_PORT_BINDING_PAIR_H
+#define EN_PORT_BINDING_PAIR_H
+
+#include "inc-proc-eng.h"
+
+void *en_port_binding_pair_init(struct engine_node *node,
+                            struct engine_arg *args);
+
+
+enum engine_node_state en_port_binding_pair_run(struct engine_node *node,
+                                                void *data);
+
+void en_port_binding_pair_cleanup(void *data);
+
+enum engine_input_handler_result
+port_binding_fdb_change_handler(struct engine_node *, void *data);
+
+#endif /* EN_PORT_BINDING_PAIR_H */
diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
index bdc6c48df..752f1e9dc 100644
--- a/northd/inc-proc-northd.c
+++ b/northd/inc-proc-northd.c
@@ -50,6 +50,11 @@ 
 #include "en-datapath-logical-router.h"
 #include "en-datapath-logical-switch.h"
 #include "en-datapath-sync.h"
+#include "en-port-binding-logical-router-port.h"
+#include "en-port-binding-logical-switch-port.h"
+#include "en-port-binding-chassisredirect.h"
+#include "en-port-binding-mirror.h"
+#include "en-port-binding-pair.h"
 #include "unixctl.h"
 #include "util.h"
 
@@ -187,6 +192,15 @@  static ENGINE_NODE(datapath_logical_switch);
 static ENGINE_NODE(datapath_synced_logical_router);
 static ENGINE_NODE(datapath_synced_logical_switch);
 static ENGINE_NODE(datapath_sync);
+static ENGINE_NODE(port_binding_logical_router_port);
+static ENGINE_NODE(port_binding_logical_switch_port);
+static ENGINE_NODE(port_binding_chassisredirect_port);
+static ENGINE_NODE(port_binding_mirror);
+static ENGINE_NODE(port_binding_paired_logical_router_port);
+static ENGINE_NODE(port_binding_paired_logical_switch_port);
+static ENGINE_NODE(port_binding_paired_chassisredirect_port);
+static ENGINE_NODE(port_binding_paired_mirror);
+static ENGINE_NODE(port_binding_pair);
 
 void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
                           struct ovsdb_idl_loop *sb)
@@ -232,6 +246,36 @@  void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_datapath_synced_logical_switch, &en_datapath_sync,
                      NULL);
 
+    engine_add_input(&en_port_binding_logical_switch_port,
+                     &en_datapath_synced_logical_switch, NULL);
+    engine_add_input(&en_port_binding_logical_router_port,
+                     &en_datapath_synced_logical_router, NULL);
+    engine_add_input(&en_port_binding_chassisredirect_port,
+                     &en_datapath_synced_logical_switch, NULL);
+    engine_add_input(&en_port_binding_chassisredirect_port,
+                     &en_datapath_synced_logical_router, NULL);
+    engine_add_input(&en_port_binding_mirror,
+                     &en_datapath_synced_logical_switch, NULL);
+    engine_add_input(&en_port_binding_pair,
+                     &en_port_binding_logical_switch_port, NULL);
+    engine_add_input(&en_port_binding_pair,
+                     &en_port_binding_logical_router_port, NULL);
+    engine_add_input(&en_port_binding_pair,
+                     &en_port_binding_chassisredirect_port, NULL);
+    engine_add_input(&en_port_binding_pair, &en_port_binding_mirror, NULL);
+    engine_add_input(&en_port_binding_pair, &en_sb_port_binding, NULL);
+    engine_add_input(&en_port_binding_pair, &en_global_config, NULL);
+    engine_add_input(&en_port_binding_pair, &en_sb_fdb,
+                     port_binding_fdb_change_handler);
+    engine_add_input(&en_port_binding_paired_logical_router_port,
+                     &en_port_binding_pair, NULL);
+    engine_add_input(&en_port_binding_paired_logical_switch_port,
+                     &en_port_binding_pair, NULL);
+    engine_add_input(&en_port_binding_paired_chassisredirect_port,
+                     &en_port_binding_pair, NULL);
+    engine_add_input(&en_port_binding_paired_mirror, &en_port_binding_pair,
+                     NULL);
+
     engine_add_input(&en_northd, &en_nb_mirror, NULL);
     engine_add_input(&en_northd, &en_nb_mirror_rule, NULL);
     engine_add_input(&en_northd, &en_nb_static_mac_binding, NULL);
@@ -247,7 +291,7 @@  void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_northd, &en_sb_service_monitor, NULL);
     engine_add_input(&en_northd, &en_sb_static_mac_binding, NULL);
     engine_add_input(&en_northd, &en_sb_chassis_template_var, NULL);
-    engine_add_input(&en_northd, &en_sb_fdb, northd_sb_fdb_change_handler);
+    engine_add_input(&en_northd, &en_sb_fdb, engine_noop_handler);
     engine_add_input(&en_northd, &en_global_config,
                      northd_global_config_handler);
 
@@ -286,6 +330,14 @@  void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
                      engine_noop_handler);
     engine_add_input(&en_northd, &en_datapath_synced_logical_switch,
                      engine_noop_handler);
+    engine_add_input(&en_northd, &en_port_binding_paired_logical_router_port,
+                     engine_noop_handler);
+    engine_add_input(&en_northd, &en_port_binding_paired_logical_switch_port,
+                     engine_noop_handler);
+    engine_add_input(&en_northd, &en_port_binding_paired_chassisredirect_port,
+                     engine_noop_handler);
+    engine_add_input(&en_northd, &en_port_binding_paired_mirror,
+                     engine_noop_handler);
 
     engine_add_input(&en_lr_nat, &en_northd, lr_nat_northd_handler);
 
diff --git a/northd/northd.c b/northd/northd.c
index 47b54dbd9..51f1c3eb6 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -54,6 +54,10 @@ 
 #include "en-sampling-app.h"
 #include "en-datapath-logical-switch.h"
 #include "en-datapath-logical-router.h"
+#include "en-port-binding-logical-switch-port.h"
+#include "en-port-binding-logical-router-port.h"
+#include "en-port-binding-chassisredirect.h"
+#include "en-port-binding-mirror.h"
 #include "lib/ovn-parallel-hmap.h"
 #include "ovn/actions.h"
 #include "ovn/features.h"
@@ -464,7 +468,7 @@  od_has_lb_vip(const struct ovn_datapath *od)
     }
 }
 
-static const char *
+const char *
 ovn_datapath_name(const struct sbrec_datapath_binding *sb)
 {
     return smap_get_def(&sb->external_ids, "name", "");
@@ -495,8 +499,6 @@  ovn_datapath_create(struct hmap *datapaths, const struct uuid *key,
     od->sb = sb;
     od->nbs = nbs;
     od->nbr = nbr;
-    hmap_init(&od->port_tnlids);
-    od->port_key_hint = 0;
     hmap_insert(datapaths, &od->key_node, uuid_hash(&od->key));
     od->lr_group = NULL;
     hmap_init(&od->ports);
@@ -527,7 +529,6 @@  ovn_datapath_destroy(struct hmap *datapaths, struct ovn_datapath *od)
          * private list and once we've exited that function it is not safe to
          * use it. */
         hmap_remove(datapaths, &od->key_node);
-        ovn_destroy_tnlids(&od->port_tnlids);
         destroy_ipam_info(&od->ipam_info);
         vector_destroy(&od->router_ports);
         vector_destroy(&od->ls_peers);
@@ -986,6 +987,7 @@  ovn_port_create(struct hmap *ports, const char *key,
     op->sb = sb;
     ovn_port_set_nb(op, nbsp, nbrp);
     op->primary_port = op->cr_port = NULL;
+    op->tunnel_key = sb->tunnel_key;
     hmap_insert(ports, &op->key_node, hash_string(op->key, 0));
 
     op->lflow_ref = lflow_ref_create();
@@ -997,10 +999,6 @@  ovn_port_create(struct hmap *ports, const char *key,
 static void
 ovn_port_cleanup(struct ovn_port *port)
 {
-    if (port->tunnel_key) {
-        ovs_assert(port->od);
-        ovn_free_tnlid(&port->od->port_tnlids, port->tunnel_key);
-    }
     for (int i = 0; i < port->n_lsp_addrs; i++) {
         destroy_lport_addresses(&port->lsp_addrs[i]);
     }
@@ -1077,12 +1075,6 @@  ovn_port_find(const struct hmap *ports, const char *name)
     return ovn_port_find__(ports, name, false);
 }
 
-static struct ovn_port *
-ovn_port_find_bound(const struct hmap *ports, const char *name)
-{
-    return ovn_port_find__(ports, name, true);
-}
-
 static bool
 lsp_is_clone_to_unknown(const struct nbrec_logical_switch_port *nbsp)
 {
@@ -1147,7 +1139,7 @@  lsp_disable_arp_nd_rsp(const struct nbrec_logical_switch_port *nbsp)
     return smap_get_bool(&nbsp->options, "disable_arp_nd_rsp", false);
 }
 
-static bool
+bool
 lsp_is_type_changed(const struct sbrec_port_binding *sb,
                 const struct nbrec_logical_switch_port *nbsp,
                 bool *update_sbrec)
@@ -1883,104 +1875,35 @@  parse_lsp_addrs(struct ovn_port *op)
     }
 }
 
-static void
-create_mirror_port(struct ovn_port *op, struct hmap *ports,
-                   struct ovs_list *both_dbs, struct ovs_list *nb_only,
-                   const struct nbrec_mirror *nb_mirror)
-{
-    char *mp_name = ovn_mirror_port_name(ovn_datapath_name(op->od->sb),
-                                         nb_mirror->sink);
-    struct ovn_port *mp = ovn_port_find(ports, mp_name);
-    struct ovn_port *target_port = ovn_port_find(ports, nb_mirror->sink);
-
-    if (!target_port) {
-        goto clear;
-    }
-
-    if (!mp) {
-        mp = ovn_port_create(ports, mp_name, op->nbsp, NULL, NULL);
-        ovs_list_push_back(nb_only, &mp->list);
-    } else if (mp->sb) {
-        ovn_port_set_nb(mp, op->nbsp, NULL);
-        ovs_list_remove(&mp->list);
-        ovs_list_push_back(both_dbs, &mp->list);
-    } else {
-        goto clear;
-    }
-
-    mp->mirror_target_port = target_port;
-    mp->od = op->od;
+static struct ovn_port *
+create_mirror_port(const struct ovn_port *source,
+                   struct ovn_port *sink, const char *mirror_port_name,
+                   struct hmap *ports,
+                   const struct sbrec_port_binding *sb_pb)
+{
+    struct ovn_port *mp = ovn_port_create(ports, mirror_port_name,
+                                          source->nbsp, NULL, sb_pb);
+    ovn_port_set_nb(mp, source->nbsp, NULL);
+    mp->mirror_target_port = sink;
+    mp->od = source->od;
 
-clear:
-    free(mp_name);
+    return mp;
 }
 
 static struct ovn_port *
 join_logical_ports_lsp(struct hmap *ports,
-                       struct ovs_list *nb_only, struct ovs_list *both,
                        struct ovn_datapath *od,
                        const struct nbrec_logical_switch_port *nbsp,
+                       const struct sbrec_port_binding *sb_pb,
                        const char *name,
                        unsigned long *queue_id_bitmap,
-                       struct hmap *tag_alloc_table,
-                       struct hmapx *mirror_attached_ports)
-{
-    struct ovn_port *op = ovn_port_find_bound(ports, name);
-    if (op && (op->od || op->nbsp || op->nbrp)) {
-        static struct vlog_rate_limit rl
-            = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "duplicate logical port %s", name);
-        return NULL;
-    } else if (op && (!op->sb || op->sb->datapath == od->sb)) {
-        /*
-         * Handle cases where lport type was explicitly changed
-         * in the NBDB, in such cases:
-         * 1. remove the current sbrec of the affected lport from
-         *    the port_binding table.
-         *
-         * 2. create a new sbrec with the same logical_port as the
-         *    deleted lport and add it to the nb_only list which
-         *    will make the northd handle this lport as a new
-         *    created one and recompute everything that is needed
-         *    for this lport.
-         *
-         * This change will affect container/virtual lport type
-         * changes only for now, this change is needed in
-         * contaier/virtual lport cases to avoid port type
-         * conflicts in the ovn-controller when the user clears
-         * the parent_port field in the container lport or updated
-         * the lport type.
-         *
-         */
-        bool update_sbrec = false;
-        if (op->sb && lsp_is_type_changed(op->sb, nbsp,
-                                          &update_sbrec)
-                       && update_sbrec) {
-            ovs_list_remove(&op->list);
-            sbrec_port_binding_delete(op->sb);
-            ovn_port_destroy(ports, op);
-            op = ovn_port_create(ports, name, nbsp,
-                                 NULL, NULL);
-            ovs_list_push_back(nb_only, &op->list);
-        } else {
-            ovn_port_set_nb(op, nbsp, NULL);
-            ovs_list_remove(&op->list);
-
-            uint32_t queue_id = smap_get_int(&op->sb->options,
-                                             "qdisc_queue_id", 0);
-            if (queue_id) {
-                bitmap_set1(queue_id_bitmap, queue_id);
-            }
-
-            ovs_list_push_back(both, &op->list);
-
-            /* This port exists due to a SB binding, but should
-             * not have been initialized fully. */
-            ovs_assert(!op->n_lsp_addrs && !op->n_ps_addrs);
-        }
-    } else {
-        op = ovn_port_create(ports, name, nbsp, NULL, NULL);
-        ovs_list_push_back(nb_only, &op->list);
+                       struct hmap *tag_alloc_table)
+{
+    struct ovn_port *op = ovn_port_create(ports, name, nbsp, NULL, sb_pb);
+    uint32_t queue_id = smap_get_int(&op->sb->options,
+                                     "qdisc_queue_id", 0);
+    if (queue_id) {
+        bitmap_set1(queue_id_bitmap, queue_id);
     }
 
     if (lsp_is_localnet(nbsp)) {
@@ -2000,47 +1923,23 @@  join_logical_ports_lsp(struct hmap *ports,
     hmap_insert(&od->ports, &op->dp_node,
                 hmap_node_hash(&op->key_node));
 
-    if (nbsp->n_mirror_rules) {
-        hmapx_add(mirror_attached_ports, op);
-    }
-
     tag_alloc_add_existing_tags(tag_alloc_table, nbsp);
     return op;
 }
 
 static struct ovn_port*
 join_logical_ports_lrp(struct hmap *ports,
-                       struct ovs_list *nb_only, struct ovs_list *both,
                        struct hmapx *dgps,
                        struct ovn_datapath *od,
                        const struct nbrec_logical_router_port *nbrp,
+                       const struct sbrec_port_binding *sb_pb,
                        const char *name, struct lport_addresses *lrp_networks)
 {
     if (!lrp_networks->n_ipv4_addrs && !lrp_networks->n_ipv6_addrs) {
       return NULL;
     }
 
-    struct ovn_port *op = ovn_port_find_bound(ports, name);
-    if (op && (op->od || op->nbsp || op->nbrp)) {
-        static struct vlog_rate_limit rl
-            = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "duplicate logical router port %s",
-                     name);
-        destroy_lport_addresses(lrp_networks);
-        return NULL;
-    } else if (op && (!op->sb || op->sb->datapath == od->sb)) {
-        ovn_port_set_nb(op, NULL, nbrp);
-        ovs_list_remove(&op->list);
-        ovs_list_push_back(both, &op->list);
-
-        /* This port exists but should not have been
-         * initialized fully. */
-        ovs_assert(!op->lrp_networks.n_ipv4_addrs
-                   && !op->lrp_networks.n_ipv6_addrs);
-    } else {
-        op = ovn_port_create(ports, name, NULL, nbrp, NULL);
-        ovs_list_push_back(nb_only, &op->list);
-    }
+    struct ovn_port *op = ovn_port_create(ports, name, NULL, nbrp, sb_pb);
 
     op->lrp_networks = *lrp_networks;
     op->od = od;
@@ -2094,128 +1993,126 @@  join_logical_ports_lrp(struct hmap *ports,
 
 
 static struct ovn_port *
-create_cr_port(struct ovn_port *op, struct hmap *ports,
-               struct ovs_list *both_dbs, struct ovs_list *nb_only)
+create_cr_port(struct ovn_port *op, const char *name, struct hmap *ports,
+               const struct sbrec_port_binding *sb_pb)
 {
-    char *redirect_name = ovn_chassis_redirect_name(
-        op->nbsp ? op->nbsp->name : op->nbrp->name);
-
-    struct ovn_port *crp = ovn_port_find(ports, redirect_name);
-    if (crp && crp->sb && crp->sb->datapath == op->od->sb) {
-        ovn_port_set_nb(crp, op->nbsp, op->nbrp);
-        ovs_list_remove(&crp->list);
-        ovs_list_push_back(both_dbs, &crp->list);
-    } else {
-        crp = ovn_port_create(ports, redirect_name,
-                              op->nbsp, op->nbrp, NULL);
-        ovs_list_push_back(nb_only, &crp->list);
-    }
+    struct ovn_port *crp = ovn_port_create(ports, name, op->nbsp, op->nbrp,
+                                           sb_pb);
 
     crp->primary_port = op;
     op->cr_port = crp;
     crp->od = op->od;
-    free(redirect_name);
 
     return crp;
 }
 
-/* Returns true if chassis resident port needs to be created for
- * op's peer logical switch.  False otherwise.
- *
- * Chassis resident port needs to be created if the following
- * conditionsd are met:
- *   - op is a distributed gateway port
- *   - op is the only distributed gateway port attached to its
- *     router
- *   - op's peer logical switch has no localnet ports.
- */
-static bool
-peer_needs_cr_port_creation(struct ovn_port *op)
-{
-    if ((op->nbrp->n_gateway_chassis || op->nbrp->ha_chassis_group)
-        && vector_len(&op->od->l3dgw_ports) == 1 && op->peer && op->peer->nbsp
-        && vector_is_empty(&op->peer->od->localnet_ports)) {
-        return true;
-    }
-
-    return false;
-}
-
 static void
-join_mirror_ports(struct ovn_port *op,
-                  const struct nbrec_logical_switch_port *nbsp,
-                  struct hmap *ports, struct ovs_list *both,
-                  struct ovs_list *nb_only)
+join_logical_ports(
+    struct hmap *ls_datapaths, struct hmap *lr_datapaths,
+    const struct ovn_paired_logical_switch_port_map *paired_lsps,
+    const struct ovn_paired_logical_router_port_map *paired_lrps,
+    const struct ovn_paired_chassisredirect_port_map *paired_crps,
+    const struct ovn_paired_mirror_map *paired_mirrors,
+    struct hmap *ls_ports, struct hmap *lr_ports,
+    unsigned long *queue_id_bitmap,
+    struct hmap *tag_alloc_table)
 {
-    /* Create mirror targets port bindings if there any mirror
-     * with lport type attached to this port. */
-    for (size_t j = 0; j < op->nbsp->n_mirror_rules; j++) {
-        struct nbrec_mirror *mirror = nbsp->mirror_rules[j];
-        if (!strcmp(mirror->type, "lport")) {
-            create_mirror_port(op, ports, both, nb_only, mirror);
+    struct ovn_datapath *od;
+    struct hmapx dgps = HMAPX_INITIALIZER(&dgps);
+
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &paired_lrps->paired_router_ports) {
+        struct ovn_paired_logical_router_port *slrp = node->data;
+        od = ovn_datapath_from_sbrec(ls_datapaths, lr_datapaths,
+                                     slrp->router->sb);
+        if (!od) {
+            /* This can happen if the router is not enabled */
+            continue;
         }
+        struct lport_addresses lrp_networks;
+        if (!extract_lrp_networks(slrp->nb, &lrp_networks)) {
+            static struct vlog_rate_limit rl
+                = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad 'mac' %s", slrp->nb->mac);
+            continue;
+        }
+
+        join_logical_ports_lrp(lr_ports, &dgps, od, slrp->nb, slrp->sb,
+                               slrp->nb->name, &lrp_networks);
     }
-}
 
-static void
-join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table,
-                   struct hmap *ls_datapaths, struct hmap *lr_datapaths,
-                   struct hmap *ports, unsigned long *queue_id_bitmap,
-                   struct hmap *tag_alloc_table, struct ovs_list *sb_only,
-                   struct ovs_list *nb_only, struct ovs_list *both)
-{
-    ovs_list_init(sb_only);
-    ovs_list_init(nb_only);
-    ovs_list_init(both);
+    SHASH_FOR_EACH (node, &paired_lsps->paired_switch_ports) {
+        struct ovn_paired_logical_switch_port *slsp = node->data;
+        od = ovn_datapath_from_sbrec(ls_datapaths, lr_datapaths,
+                                     slsp->sw->sb);
+        if (!od) {
+            /* This should not happen, but we'll be defensive just in case */
+            continue;
+        }
+        join_logical_ports_lsp(ls_ports, od, slsp->nb, slsp->sb,
+                               slsp->nb->name, queue_id_bitmap,
+                               tag_alloc_table);
+    }
 
-    const struct sbrec_port_binding *sb;
-    SBREC_PORT_BINDING_TABLE_FOR_EACH (sb, sbrec_pb_table) {
-        struct ovn_port *op = ovn_port_create(ports, sb->logical_port,
-                                              NULL, NULL, sb);
-        ovs_list_push_back(sb_only, &op->list);
+    SHASH_FOR_EACH (node, &paired_crps->paired_chassisredirect_router_ports) {
+        struct ovn_paired_chassisredirect_router_port *crp = node->data;
+        struct ovn_port *primary_port =
+            ovn_port_find(lr_ports, crp->primary_port->name);
+        create_cr_port(primary_port, crp->name, lr_ports, crp->sb);
     }
 
-    struct ovn_datapath *od;
-    struct hmapx dgps = HMAPX_INITIALIZER(&dgps);
-    struct hmapx mirror_attached_ports =
-                    HMAPX_INITIALIZER(&mirror_attached_ports);
-    HMAP_FOR_EACH (od, key_node, lr_datapaths) {
-        ovs_assert(od->nbr);
-        for (size_t i = 0; i < od->nbr->n_ports; i++) {
-            const struct nbrec_logical_router_port *nbrp
-                = od->nbr->ports[i];
-
-            struct lport_addresses lrp_networks;
-            if (!extract_lrp_networks(nbrp, &lrp_networks)) {
-                static struct vlog_rate_limit rl
-                    = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl, "bad 'mac' %s", nbrp->mac);
-                continue;
-            }
-            join_logical_ports_lrp(ports, nb_only, both, &dgps,
-                                   od, nbrp,
-                                   nbrp->name, &lrp_networks);
-        }
+    SHASH_FOR_EACH (node, &paired_crps->paired_chassisredirect_switch_ports) {
+        struct ovn_paired_chassisredirect_switch_port *crp = node->data;
+        struct ovn_port *primary_port =
+            ovn_port_find(ls_ports, crp->primary_port->name);
+        create_cr_port(primary_port, crp->name, ls_ports, crp->sb);
     }
 
-    HMAP_FOR_EACH (od, key_node, ls_datapaths) {
-        ovs_assert(od->nbs);
-        for (size_t i = 0; i < od->nbs->n_ports; i++) {
-            const struct nbrec_logical_switch_port *nbsp
-                = od->nbs->ports[i];
-            join_logical_ports_lsp(ports, nb_only, both, od, nbsp,
-                                   nbsp->name, queue_id_bitmap,
-                                   tag_alloc_table, &mirror_attached_ports);
+    SHASH_FOR_EACH (node, &paired_mirrors->paired_mirror_ports) {
+        struct ovn_paired_mirror *mirror = node->data;
+        struct ovn_port *source_port =
+            ovn_port_find(ls_ports, mirror->nbsp->name);
+        struct ovn_port *sink_port =
+            ovn_port_find(ls_ports, mirror->sink);
+        if (!sink_port) {
+            continue;
         }
+        create_mirror_port(source_port, sink_port, mirror->name, ls_ports,
+                           mirror->sb);
     }
 
     /* Connect logical router ports, and logical switch ports of type "router",
      * to their peers.  As well as logical switch ports of type "switch" to
      * theirs. */
+
     struct ovn_port *op;
-    HMAP_FOR_EACH (op, key_node, ports) {
-        if (op->nbsp && lsp_is_router(op->nbsp) && !op->primary_port) {
-            struct ovn_port *peer = ovn_port_get_peer(ports, op);
+    HMAP_FOR_EACH (op, key_node, lr_ports) {
+        if (op->nbrp->peer && !is_cr_port(op)) {
+            struct ovn_port *peer = ovn_port_find(lr_ports, op->nbrp->peer);
+            if (peer) {
+                if (peer->nbrp && peer->nbrp->peer &&
+                        !strcmp(op->nbrp->name, peer->nbrp->peer)) {
+                    /* We only configure LRP peers if each LRP has the other as
+                     * its peer. */
+                    op->peer = peer;
+                } else if (peer->nbsp) {
+                    /* An ovn_port for a switch port of type "router" does have
+                     * a router port as its peer (see the case above for
+                     * "router" ports), but this is set via options:router-port
+                     * in Logical_Switch_Port and does not involve the
+                     * Logical_Router_Port's 'peer' column. */
+                    static struct vlog_rate_limit rl =
+                            VLOG_RATE_LIMIT_INIT(5, 1);
+                    VLOG_WARN_RL(&rl, "Bad configuration: The peer of router "
+                                 "port %s is a switch port", op->key);
+                }
+            }
+        }
+    }
+
+    HMAP_FOR_EACH (op, key_node, ls_ports) {
+        if (lsp_is_router(op->nbsp) && !op->primary_port) {
+            struct ovn_port *peer = ovn_port_get_peer(lr_ports, op);
             if (!peer || !peer->nbrp) {
                 continue;
             }
@@ -2271,21 +2168,21 @@  join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table,
                         arp_proxy, op->nbsp->name);
                 }
             }
-        } else if (op->nbsp && op->nbsp->peer && lsp_is_switch(op->nbsp)) {
+        } else if (op->nbsp->peer && lsp_is_switch(op->nbsp)) {
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-            struct ovn_port *peer = ovn_port_find(ports, op->nbsp->peer);
+            struct ovn_port *peer = ovn_port_find(ls_ports, op->nbsp->peer);
 
             if (!peer) {
                 continue;
             }
 
-            if (peer->nbrp || (peer->nbsp && lsp_is_router(peer->nbsp))) {
+            if (lsp_is_router(peer->nbsp)) {
                 VLOG_WARN_RL(&rl, "Bad configuration: The peer of switch "
                                   "port %s is a router port", op->key);
                 continue;
             }
 
-            if (!peer->nbsp || !lsp_is_switch(peer->nbsp)) {
+            if (!lsp_is_switch(peer->nbsp)) {
                 /* Common case.  Likely the manual configuration is not
                  * finished yet. */
                 continue;
@@ -2300,26 +2197,6 @@  join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table,
             }
 
             op->peer = peer;
-        } else if (op->nbrp && op->nbrp->peer && !is_cr_port(op)) {
-            struct ovn_port *peer = ovn_port_find(ports, op->nbrp->peer);
-            if (peer) {
-                if (peer->nbrp && peer->nbrp->peer &&
-                        !strcmp(op->nbrp->name, peer->nbrp->peer)) {
-                    /* We only configure LRP peers if each LRP has the other as
-                     * its peer. */
-                    op->peer = peer;
-                } else if (peer->nbsp) {
-                    /* An ovn_port for a switch port of type "router" does have
-                     * a router port as its peer (see the case above for
-                     * "router" ports), but this is set via options:router-port
-                     * in Logical_Switch_Port and does not involve the
-                     * Logical_Router_Port's 'peer' column. */
-                    static struct vlog_rate_limit rl =
-                            VLOG_RATE_LIMIT_INIT(5, 1);
-                    VLOG_WARN_RL(&rl, "Bad configuration: The peer of router "
-                                 "port %s is a switch port", op->key);
-                }
-            }
         }
     }
 
@@ -2330,11 +2207,6 @@  join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table,
         ovs_assert(op->nbrp);
         ovs_assert(op->nbrp->ha_chassis_group || op->nbrp->n_gateway_chassis);
 
-        /* Additional "derived" ovn_port crp represents the instance of op on
-         * the gateway chassis. */
-        struct ovn_port *crp = create_cr_port(op, ports, both, nb_only);
-        ovs_assert(crp);
-
         /* Add to l3dgw_ports in od, for later use during flow creation. */
         vector_push(&od->l3dgw_ports, &op);
 
@@ -2345,41 +2217,16 @@  join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table,
         }
     }
 
-
-    /* Create chassisredirect port for the distributed gateway port's (DGP)
-     * peer if
-     *  - DGP's router has only one DGP and
-     *  - Its peer is a logical switch port and
-     *  - Its peer's logical switch has no localnet ports
-     *
-     * This is required to support
-     *   - NAT via geneve (for the overlay provider networks) and
-     *   - to centralize routing on the gateway chassis for the traffic
-     *     destined to the DGP's networks.
-     *
-     * Future enhancement: Support 'centralizerouting' for all the DGP's
-     * of a logical router.
-     * */
-    HMAPX_FOR_EACH (hmapx_node, &dgps) {
-        op = hmapx_node->data;
-        if (peer_needs_cr_port_creation(op)) {
-            create_cr_port(op->peer, ports, both, nb_only);
-        }
-    }
     hmapx_destroy(&dgps);
 
-    HMAPX_FOR_EACH (hmapx_node, &mirror_attached_ports) {
-        op = hmapx_node->data;
-        if (op && op->nbsp) {
-            join_mirror_ports(op, op->nbsp, ports, both, nb_only);
-        }
-    }
-    hmapx_destroy(&mirror_attached_ports);
-
     /* Wait until all ports have been connected to add to IPAM since
      * it relies on proper peers to be set
      */
-    HMAP_FOR_EACH (op, key_node, ports) {
+    HMAP_FOR_EACH (op, key_node, ls_ports) {
+        ipam_add_port_addresses(op->od, op);
+    }
+
+    HMAP_FOR_EACH (op, key_node, lr_ports) {
         ipam_add_port_addresses(op->od, op);
     }
 }
@@ -2783,15 +2630,6 @@  copy_gw_chassis_from_nbrp_to_sbpb(
     free(sb_ha_chassis);
 }
 
-static const char*
-op_get_name(const struct ovn_port *op)
-{
-    ovs_assert(op->nbsp || op->nbrp);
-    const char *name = op->nbsp ? op->nbsp->name
-                                : op->nbrp->name;
-    return name;
-}
-
 static void
 ovn_update_ipv6_prefix(struct hmap *lr_ports)
 {
@@ -3052,8 +2890,6 @@  ovn_port_update_sbrec(struct ovsdb_idl_txn *ovnsb_txn,
         const char *addresses = ds_cstr(&s);
         sbrec_port_binding_set_mac(op->sb, &addresses, 1);
         ds_destroy(&s);
-
-        sbrec_port_binding_set_external_ids(op->sb, &op->nbrp->external_ids);
     } else {
         if (op->mirror_target_port) {
             /* In case of using a lport mirror, we establish a port binding
@@ -3262,15 +3098,6 @@  ovn_port_update_sbrec(struct ovsdb_idl_txn *ovnsb_txn,
             op->sb, (const char **) op->nbsp->port_security,
             op->nbsp->n_port_security);
 
-        struct smap ids = SMAP_INITIALIZER(&ids);
-        smap_clone(&ids, &op->nbsp->external_ids);
-        const char *name = smap_get(&ids, "neutron:port_name");
-        if (name && name[0]) {
-            smap_add(&ids, "name", name);
-        }
-        sbrec_port_binding_set_external_ids(op->sb, &ids);
-        smap_destroy(&ids);
-
         if (!op->nbsp->n_mirror_rules) {
             /* Nothing is set. Clear mirror_rules from pb. */
             sbrec_port_binding_set_mirror_rules(op->sb, NULL, 0);
@@ -3328,27 +3155,6 @@  cleanup_sb_ha_chassis_groups(
     }
 }
 
-static void
-cleanup_stale_fdb_entries(const struct sbrec_fdb_table *sbrec_fdb_table,
-                          struct hmap *ls_datapaths)
-{
-    const struct sbrec_fdb *fdb_e;
-    SBREC_FDB_TABLE_FOR_EACH_SAFE (fdb_e, sbrec_fdb_table) {
-        bool delete = true;
-        struct ovn_datapath *od
-            = ovn_datapath_find_by_key(ls_datapaths, fdb_e->dp_key);
-        if (od) {
-            if (ovn_tnlid_present(&od->port_tnlids, fdb_e->port_key)) {
-                delete = false;
-            }
-        }
-
-        if (delete) {
-            sbrec_fdb_delete(fdb_e);
-        }
-    }
-}
-
 static void
 delete_fdb_entries(struct ovsdb_idl_index *sbrec_fdb_by_dp_and_port,
                  uint32_t dp_key, uint32_t port_key)
@@ -4122,64 +3928,6 @@  sync_pbs_for_northd_changed_ovn_ports(
     return true;
 }
 
-static bool
-ovn_port_add_tnlid(struct ovn_port *op, uint32_t tunnel_key)
-{
-    bool added = ovn_add_tnlid(&op->od->port_tnlids, tunnel_key);
-    if (added) {
-        op->tunnel_key = tunnel_key;
-        if (tunnel_key > op->od->port_key_hint) {
-            op->od->port_key_hint = tunnel_key;
-        }
-    }
-    return added;
-}
-
-/* Returns false if the requested key is confict with another allocated key, so
- * that the I-P engine can fallback to recompute if needed; otherwise return
- * true (even if the key is not allocated). */
-static bool
-ovn_port_assign_requested_tnl_id(struct ovn_port *op)
-{
-    const struct smap *options = (op->nbsp
-                                  ? &op->nbsp->options
-                                  : &op->nbrp->options);
-    uint32_t tunnel_key = smap_get_int(options, "requested-tnl-key", 0);
-    if (tunnel_key) {
-        if (vxlan_mode && tunnel_key >= OVN_VXLAN_MIN_MULTICAST) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-            VLOG_WARN_RL(&rl, "Tunnel key %"PRIu32" for port %s "
-                         "is incompatible with VXLAN",
-                         tunnel_key, op_get_name(op));
-            return true;
-        }
-        if (!ovn_port_add_tnlid(op, tunnel_key)) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-            VLOG_WARN_RL(&rl, "Logical %s port %s requests same tunnel key "
-                         "%"PRIu32" as another LSP or LRP",
-                         op->nbsp ? "switch" : "router",
-                         op_get_name(op), tunnel_key);
-            return false;
-        }
-    }
-    return true;
-}
-
-static bool
-ovn_port_allocate_key(struct ovn_port *op)
-{
-    if (!op->tunnel_key) {
-        uint8_t key_bits = vxlan_mode ? 12 : 16;
-        op->tunnel_key = ovn_allocate_tnlid(&op->od->port_tnlids, "port",
-                                            1, (1u << (key_bits - 1)) - 1,
-                                            &op->od->port_key_hint);
-        if (!op->tunnel_key) {
-            return false;
-        }
-    }
-    return true;
-}
-
 /* Updates the southbound Port_Binding table so that it contains the logical
  * switch ports specified by the northbound database.
  *
@@ -4188,17 +3936,19 @@  ovn_port_allocate_key(struct ovn_port *op)
  * datapaths. */
 static void
 build_ports(struct ovsdb_idl_txn *ovnsb_txn,
-    const struct sbrec_port_binding_table *sbrec_port_binding_table,
     const struct sbrec_mirror_table *sbrec_mirror_table,
     const struct sbrec_mac_binding_table *sbrec_mac_binding_table,
     const struct sbrec_ha_chassis_group_table *sbrec_ha_chassis_group_table,
     struct ovsdb_idl_index *sbrec_chassis_by_name,
     struct ovsdb_idl_index *sbrec_chassis_by_hostname,
     struct ovsdb_idl_index *sbrec_ha_chassis_grp_by_name,
+    const struct ovn_paired_logical_switch_port_map *paired_lsps,
+    const struct ovn_paired_logical_router_port_map *paired_lrps,
+    const struct ovn_paired_chassisredirect_port_map *paired_crps,
+    const struct ovn_paired_mirror_map *paired_mirrors,
     struct hmap *ls_datapaths, struct hmap *lr_datapaths,
     struct hmap *ls_ports, struct hmap *lr_ports)
 {
-    struct ovs_list sb_only, nb_only, both;
     /* XXX: Add tag_alloc_table and queue_id_bitmap as part of northd_data
      * to improve I-P. */
     struct hmap tag_alloc_table = HMAP_INITIALIZER(&tag_alloc_table);
@@ -4209,107 +3959,37 @@  build_ports(struct ovsdb_idl_txn *ovnsb_txn,
     struct sset active_ha_chassis_grps =
         SSET_INITIALIZER(&active_ha_chassis_grps);
 
-    /* Borrow ls_ports for joining NB and SB for both LSPs and LRPs.
-     * We will split them later. */
-    struct hmap *ports = ls_ports;
-    join_logical_ports(sbrec_port_binding_table, ls_datapaths, lr_datapaths,
-                       ports, queue_id_bitmap,
-                       &tag_alloc_table, &sb_only, &nb_only, &both);
+    join_logical_ports(ls_datapaths, lr_datapaths,
+                       paired_lsps, paired_lrps, paired_crps,
+                       paired_mirrors, ls_ports, lr_ports, queue_id_bitmap,
+                       &tag_alloc_table);
 
-    /* Purge stale Mac_Bindings if ports are deleted. */
-    bool remove_mac_bindings = !ovs_list_is_empty(&sb_only);
-
-    /* Assign explicitly requested tunnel ids first. */
     struct ovn_port *op;
-    LIST_FOR_EACH (op, list, &both) {
-        ovn_port_assign_requested_tnl_id(op);
-    }
-    LIST_FOR_EACH (op, list, &nb_only) {
-        ovn_port_assign_requested_tnl_id(op);
-    }
-
-    /* Keep nonconflicting tunnel IDs that are already assigned. */
-    LIST_FOR_EACH (op, list, &both) {
-        if (!op->tunnel_key) {
-            ovn_port_add_tnlid(op, op->sb->tunnel_key);
-        }
-    }
-
-    /* Assign new tunnel ids where needed. */
-    LIST_FOR_EACH_SAFE (op, list, &both) {
-        if (!ovn_port_allocate_key(op)) {
-            sbrec_port_binding_delete(op->sb);
-            ovs_list_remove(&op->list);
-            ovn_port_destroy(ports, op);
-        }
-    }
-    LIST_FOR_EACH_SAFE (op, list, &nb_only) {
-        if (!ovn_port_allocate_key(op)) {
-            ovs_list_remove(&op->list);
-            ovn_port_destroy(ports, op);
-        }
-    }
-
-    /* For logical ports that are in both databases, update the southbound
-     * record based on northbound data.
-     * For logical ports that are in NB database, do any tag allocation
-     * needed. */
-    LIST_FOR_EACH_SAFE (op, list, &both) {
-        /* When reusing stale Port_Bindings, make sure that stale
-         * Mac_Bindings are purged.
-         */
-        if (op->od->sb != op->sb->datapath) {
-            remove_mac_bindings = true;
-        }
-        if (op->nbsp) {
-            tag_alloc_create_new_tag(&tag_alloc_table, op->nbsp);
-        }
+    /* For logical ports, update the southbound record based on northbound
+     * data.
+     * For logical switch ports, do any tag allocation needed.
+     */
+    HMAP_FOR_EACH (op, key_node, ls_ports) {
+        tag_alloc_create_new_tag(&tag_alloc_table, op->nbsp);
         ovn_port_update_sbrec(ovnsb_txn, sbrec_chassis_by_name,
                               sbrec_chassis_by_hostname,
                               sbrec_ha_chassis_grp_by_name,
                               sbrec_mirror_table,
                               op, queue_id_bitmap,
                               &active_ha_chassis_grps);
-        op->od->is_transit_router |= is_transit_router_port(op);
-        ovs_list_remove(&op->list);
     }
 
-    /* Add southbound record for each unmatched northbound record. */
-    LIST_FOR_EACH_SAFE (op, list, &nb_only) {
-        op->sb = sbrec_port_binding_insert(ovnsb_txn);
+    HMAP_FOR_EACH (op, key_node, lr_ports) {
         ovn_port_update_sbrec(ovnsb_txn, sbrec_chassis_by_name,
                               sbrec_chassis_by_hostname,
                               sbrec_ha_chassis_grp_by_name,
                               sbrec_mirror_table,
                               op, queue_id_bitmap,
                               &active_ha_chassis_grps);
-        sbrec_port_binding_set_logical_port(op->sb, op->key);
         op->od->is_transit_router |= is_transit_router_port(op);
-        ovs_list_remove(&op->list);
-    }
-
-    /* Delete southbound records without northbound matches. */
-    if (!ovs_list_is_empty(&sb_only)) {
-        LIST_FOR_EACH_SAFE (op, list, &sb_only) {
-            ovs_list_remove(&op->list);
-            sbrec_port_binding_delete(op->sb);
-            ovn_port_destroy(ports, op);
-        }
     }
 
-    /* Move logical router ports to lr_ports, and logical switch ports will
-     * remain in ports/ls_ports. */
-    HMAP_FOR_EACH_SAFE (op, key_node, ports) {
-        if (!op->nbrp) {
-            continue;
-        }
-        hmap_remove(ports, &op->key_node);
-        hmap_insert(lr_ports, &op->key_node, op->key_node.hash);
-    }
-
-    if (remove_mac_bindings) {
-        cleanup_mac_bindings(sbrec_mac_binding_table, lr_datapaths, lr_ports);
-    }
+    cleanup_mac_bindings(sbrec_mac_binding_table, lr_datapaths, lr_ports);
 
     tag_alloc_destroy(&tag_alloc_table);
     bitmap_free(queue_id_bitmap);
@@ -4453,67 +4133,39 @@  ovn_port_find_in_datapath(struct ovn_datapath *od,
     return NULL;
 }
 
-static bool
+static void
 ls_port_init(struct ovn_port *op, struct ovsdb_idl_txn *ovnsb_txn,
              struct ovn_datapath *od,
-             const struct sbrec_port_binding *sb,
              const struct sbrec_mirror_table *sbrec_mirror_table,
              struct ovsdb_idl_index *sbrec_chassis_by_name,
              struct ovsdb_idl_index *sbrec_chassis_by_hostname)
 {
     op->od = od;
     parse_lsp_addrs(op);
-    /* Assign explicitly requested tunnel ids first. */
-    if (!ovn_port_assign_requested_tnl_id(op)) {
-        return false;
-    }
-    /* Keep nonconflicting tunnel IDs that are already assigned. */
-    if (sb) {
-        if (!op->tunnel_key) {
-            ovn_port_add_tnlid(op, sb->tunnel_key);
-        }
-    }
-    /* Assign new tunnel ids where needed. */
-    if (!ovn_port_allocate_key(op)) {
-        return false;
-    }
-    /* Create new binding, if needed. */
-    if (sb) {
-        op->sb = sb;
-    } else {
-        /* XXX: the new SB port_binding will change in IDL, so need to handle
-         * SB port_binding updates incrementally to achieve end-to-end
-         * incremental processing. */
-        op->sb = sbrec_port_binding_insert(ovnsb_txn);
-        sbrec_port_binding_set_logical_port(op->sb, op->key);
-    }
     ovn_port_update_sbrec(ovnsb_txn, sbrec_chassis_by_name,
                           sbrec_chassis_by_hostname, NULL, sbrec_mirror_table,
                           op, NULL, NULL);
-    return true;
 }
 
 static struct ovn_port *
 ls_port_create(struct ovsdb_idl_txn *ovnsb_txn, struct hmap *ls_ports,
                const char *key, const struct nbrec_logical_switch_port *nbsp,
                struct ovn_datapath *od,
+               const struct sbrec_port_binding *sb,
                const struct sbrec_mirror_table *sbrec_mirror_table,
                struct ovsdb_idl_index *sbrec_chassis_by_name,
                struct ovsdb_idl_index *sbrec_chassis_by_hostname)
 {
     struct ovn_port *op = ovn_port_create(ls_ports, key, nbsp, NULL,
-                                          NULL);
+                                          sb);
     hmap_insert(&od->ports, &op->dp_node, hmap_node_hash(&op->key_node));
-    if (!ls_port_init(op, ovnsb_txn, od, NULL, sbrec_mirror_table,
-                      sbrec_chassis_by_name, sbrec_chassis_by_hostname)) {
-        ovn_port_destroy(ls_ports, op);
-        return NULL;
-    }
+    ls_port_init(op, ovnsb_txn, od, sbrec_mirror_table,
+                 sbrec_chassis_by_name, sbrec_chassis_by_hostname);
 
     return op;
 }
 
-static bool
+static void
 ls_port_reinit(struct ovn_port *op, struct ovsdb_idl_txn *ovnsb_txn,
                 const struct nbrec_logical_switch_port *nbsp,
                 struct ovn_datapath *od,
@@ -4524,10 +4176,11 @@  ls_port_reinit(struct ovn_port *op, struct ovsdb_idl_txn *ovnsb_txn,
 {
     ovn_port_cleanup(op);
     op->sb = sb;
+    op->tunnel_key = sb->tunnel_key;
     ovn_port_set_nb(op, nbsp, NULL);
     op->primary_port = op->cr_port = NULL;
-    return ls_port_init(op, ovnsb_txn, od, sb, sbrec_mirror_table,
-                        sbrec_chassis_by_name, sbrec_chassis_by_hostname);
+    ls_port_init(op, ovnsb_txn, od, sbrec_mirror_table,
+                 sbrec_chassis_by_name, sbrec_chassis_by_hostname);
 }
 
 /* Returns true if the logical switch has changes which can be
@@ -4544,6 +4197,7 @@  ls_changes_can_be_handled(
 {
     /* Check if the columns are changed in this row. */
     enum nbrec_logical_switch_column_id col;
+
     for (col = 0; col < NBREC_LOGICAL_SWITCH_N_COLUMNS; col++) {
         if (nbrec_logical_switch_is_updated(ls, col)) {
             if (col == NBREC_LOGICAL_SWITCH_COL_ACLS ||
@@ -4693,15 +4347,18 @@  ls_handle_lsp_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
 
     /* Compare the individual ports in the old and new Logical Switches */
     for (size_t j = 0; j < changed_ls->n_ports; ++j) {
-        struct nbrec_logical_switch_port *new_nbsp = changed_ls->ports[j];
-        op = ovn_port_find_in_datapath(od, new_nbsp);
+        const struct ovn_paired_logical_switch_port *paired_lsp =
+            shash_find_data(&ni->paired_lsps->paired_switch_ports,
+                            changed_ls->ports[j]->name);
+        op = ovn_port_find_in_datapath(od, paired_lsp->nb);
 
         if (!op) {
-            if (!lsp_can_be_inc_processed(new_nbsp)) {
+            if (!lsp_can_be_inc_processed(paired_lsp->nb)) {
                 goto fail;
             }
             op = ls_port_create(ovnsb_idl_txn, &nd->ls_ports,
-                                new_nbsp->name, new_nbsp, od,
+                                paired_lsp->nb->name, paired_lsp->nb, od,
+                                paired_lsp->sb,
                                 ni->sbrec_mirror_table,
                                 ni->sbrec_chassis_by_name,
                                 ni->sbrec_chassis_by_hostname);
@@ -4709,28 +4366,27 @@  ls_handle_lsp_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
                 goto fail;
             }
             add_op_to_northd_tracked_ports(&trk_lsps->created, op);
-        } else if (ls_port_has_changed(new_nbsp)) {
+        } else if (ls_port_has_changed(paired_lsp->nb)) {
             /* Existing port updated */
             bool temp = false;
-            if (lsp_is_type_changed(op->sb, new_nbsp, &temp) ||
+            if (lsp_is_type_changed(op->sb, paired_lsp->nb, &temp) ||
                 !op->lsp_can_be_inc_processed ||
-                !lsp_can_be_inc_processed(new_nbsp)) {
+                !lsp_can_be_inc_processed(paired_lsp->nb)) {
                 goto fail;
             }
-            const struct sbrec_port_binding *sb = op->sb;
-            if (sset_contains(&nd->svc_monitor_lsps, new_nbsp->name)) {
+            if (sset_contains(&nd->svc_monitor_lsps, paired_lsp->nb->name)) {
                 /* This port is used for svc monitor, which may be impacted
                  * by this change. Fallback to recompute. */
                 goto fail;
             }
-            if (!lsp_handle_mirror_rules_changes(new_nbsp) ||
+            if (!lsp_handle_mirror_rules_changes(paired_lsp->nb) ||
                  is_lsp_mirror_target_port(ni->nbrec_mirror_by_type_and_sink,
                                            op)) {
                 /* Fallback to recompute. */
                 goto fail;
             }
             if (!check_lsp_is_up &&
-                !check_lsp_changes_other_than_up(new_nbsp)) {
+                !check_lsp_changes_other_than_up(paired_lsp->nb)) {
                 /* If the only change is the "up" column while the
                  * "ignore_lsp_down" is set to true, just ignore this
                  * change. */
@@ -4739,17 +4395,11 @@  ls_handle_lsp_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
             }
 
             uint32_t old_tunnel_key = op->tunnel_key;
-            if (!ls_port_reinit(op, ovnsb_idl_txn,
-                                new_nbsp,
-                                od, sb, ni->sbrec_mirror_table,
-                                ni->sbrec_chassis_by_name,
-                                ni->sbrec_chassis_by_hostname)) {
-                if (sb) {
-                    sbrec_port_binding_delete(sb);
-                }
-                ovn_port_destroy(&nd->ls_ports, op);
-                goto fail;
-            }
+            ls_port_reinit(op, ovnsb_idl_txn,
+                           paired_lsp->nb,
+                           od, paired_lsp->sb, ni->sbrec_mirror_table,
+                           ni->sbrec_chassis_by_name,
+                           ni->sbrec_chassis_by_hostname);
             add_op_to_northd_tracked_ports(&trk_lsps->updated, op);
 
             if (old_tunnel_key != op->tunnel_key) {
@@ -4774,7 +4424,6 @@  ls_handle_lsp_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
             add_op_to_northd_tracked_ports(&trk_lsps->deleted, op);
             hmap_remove(&nd->ls_ports, &op->key_node);
             hmap_remove(&od->ports, &op->dp_node);
-            sbrec_port_binding_delete(op->sb);
             delete_fdb_entries(ni->sbrec_fdb_by_dp_and_port, od->tunnel_key,
                                 op->tunnel_key);
             if (is_lsp_mirror_target_port(ni->nbrec_mirror_by_type_and_sink,
@@ -19032,13 +18681,16 @@  ovnnb_db_run(struct northd_input *input_data,
                        &data->ls_datapaths, &data->lr_datapaths,
                        &data->lb_datapaths_map, &data->lb_group_datapaths_map);
     build_ports(ovnsb_txn,
-                input_data->sbrec_port_binding_table,
                 input_data->sbrec_mirror_table,
                 input_data->sbrec_mac_binding_table,
                 input_data->sbrec_ha_chassis_group_table,
                 input_data->sbrec_chassis_by_name,
                 input_data->sbrec_chassis_by_hostname,
                 input_data->sbrec_ha_chassis_grp_by_name,
+                input_data->paired_lsps,
+                input_data->paired_lrps,
+                input_data->paired_crps,
+                input_data->paired_mirrors,
                 &data->ls_datapaths.datapaths, &data->lr_datapaths.datapaths,
                 &data->ls_ports, &data->lr_ports);
     build_lb_port_related_data(ovnsb_txn,
@@ -19072,10 +18724,7 @@  ovnnb_db_run(struct northd_input *input_data,
     sync_template_vars(ovnsb_txn, input_data->nbrec_chassis_template_var_table,
                        input_data->sbrec_chassis_template_var_table);
 
-    cleanup_stale_fdb_entries(input_data->sbrec_fdb_table,
-                              &data->ls_datapaths.datapaths);
     stopwatch_stop(CLEAR_LFLOWS_CTX_STOPWATCH_NAME, time_msec());
-
 }
 
 /* Stores the set of chassis which references an ha_chassis_group.
@@ -19267,6 +18916,25 @@  handle_cr_port_binding_changes(const struct sbrec_port_binding *sb,
     }
 }
 
+void
+lsp_set_up(const struct sbrec_port_binding *pb,
+           const struct nbrec_logical_switch_port *lsp)
+{
+    bool up = false;
+
+    if (lsp_is_router(lsp) || lsp_is_switch(lsp)) {
+        up = true;
+    } else if (pb->chassis) {
+        up = !smap_get_bool(&pb->chassis->other_config, "is-remote", false)
+             ? pb->n_up && pb->up[0]
+             : true;
+    }
+
+    if (!lsp->up || *lsp->up != up) {
+        nbrec_logical_switch_port_set_up(lsp, &up, 1);
+    }
+}
+
 /* Handle changes to the 'chassis' column of the 'Port_Binding' table.  When
  * this column is not empty, it means we need to set the corresponding logical
  * port as 'up' in the northbound DB. */
@@ -19315,25 +18983,13 @@  handle_port_binding_changes(struct ovsdb_idl_txn *ovnsb_txn,
             continue;
         }
 
-        bool up = false;
-
-        if (lsp_is_router(op->nbsp) || lsp_is_switch(op->nbsp)) {
-            up = true;
-        } else if (sb->chassis) {
-            up = !smap_get_bool(&sb->chassis->other_config, "is-remote", false)
-                 ? sb->n_up && sb->up[0]
-                 : true;
-        }
-
-        if (!op->nbsp->up || *op->nbsp->up != up) {
-            nbrec_logical_switch_port_set_up(op->nbsp, &up, 1);
-        }
+        lsp_set_up(sb, op->nbsp);
 
         /* ovn-controller will update 'Port_Binding.up' only if it was
          * explicitly set to 'false'.
          */
         if (!op->sb->n_up) {
-            up = false;
+            bool up = false;
             sbrec_port_binding_set_up(op->sb, &up, 1);
         }
 
diff --git a/northd/northd.h b/northd/northd.h
index e5a9cc775..ee34c28c0 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -74,6 +74,12 @@  struct northd_input {
     const struct ovn_synced_logical_switch_map *synced_lses;
     const struct ovn_synced_logical_router_map *synced_lrs;
 
+    /* Paired port binding inputs. */
+    const struct ovn_paired_logical_switch_port_map *paired_lsps;
+    const struct ovn_paired_logical_router_port_map *paired_lrps;
+    const struct ovn_paired_chassisredirect_port_map *paired_crps;
+    const struct ovn_paired_mirror_map *paired_mirrors;
+
     /* Indexes */
     struct ovsdb_idl_index *sbrec_chassis_by_name;
     struct ovsdb_idl_index *sbrec_chassis_by_hostname;
@@ -376,9 +382,6 @@  struct ovn_datapath {
     /* Logical switch data. */
     struct vector router_ports; /* Vector of struct ovn_port *. */
 
-    struct hmap port_tnlids;
-    uint32_t port_key_hint;
-
     bool has_unknown;
     bool has_vtep_lports;
     bool has_arp_proxy_port;
@@ -827,6 +830,8 @@  void ovnsb_db_run(struct ovsdb_idl_txn *ovnnb_txn,
                   const struct sbrec_ha_chassis_group_table *,
                   struct hmap *ls_ports,
                   struct hmap *lr_ports);
+void lsp_set_up(const struct sbrec_port_binding *pb,
+                const struct nbrec_logical_switch_port *lsp);
 bool northd_handle_ls_changes(struct ovsdb_idl_txn *,
                               const struct northd_input *,
                               struct northd_data *);
@@ -1037,4 +1042,10 @@  struct ovn_port_routable_addresses get_op_addresses(
 
 void destroy_routable_addresses(struct ovn_port_routable_addresses *ra);
 
+bool lsp_is_type_changed(const struct sbrec_port_binding *sb,
+                         const struct nbrec_logical_switch_port *nbsp,
+                         bool *update_sbrec);
+
+const char *
+ovn_datapath_name(const struct sbrec_datapath_binding *sb);
 #endif /* NORTHD_H */
diff --git a/northd/port_binding_pair.c b/northd/port_binding_pair.c
new file mode 100644
index 000000000..bfd3d0b42
--- /dev/null
+++ b/northd/port_binding_pair.c
@@ -0,0 +1,81 @@ 
+/* Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "port_binding_pair.h"
+
+struct ovn_unpaired_port_binding *
+ovn_unpaired_port_binding_alloc(uint32_t requested_tunnel_key,
+                                const char *name,
+                                const char *type,
+                                void *cookie,
+                                const struct sbrec_datapath_binding *sb_dp)
+{
+    struct ovn_unpaired_port_binding *pb = xzalloc(sizeof *pb);
+    pb->requested_tunnel_key = requested_tunnel_key;
+    pb->name = name;
+    pb->type = type;
+    pb->cookie = cookie;
+    pb->sb_dp = sb_dp;
+    smap_init(&pb->external_ids);
+
+    return pb;
+}
+
+void
+ovn_unpaired_port_binding_destroy(struct ovn_unpaired_port_binding *pb)
+{
+    smap_destroy(&pb->external_ids);
+}
+
+static bool
+default_sb_is_valid(const struct sbrec_port_binding *sb_pb OVS_UNUSED,
+                    const struct ovn_unpaired_port_binding *upb OVS_UNUSED)
+{
+    return true;
+}
+
+static struct ovn_unpaired_port_binding_map_callbacks default_callbacks = {
+    .sb_is_valid = default_sb_is_valid,
+};
+
+void
+ovn_unpaired_port_binding_map_init(
+    struct ovn_unpaired_port_binding_map *map,
+    const struct ovn_unpaired_port_binding_map_callbacks *cb)
+{
+    shash_init(&map->ports);
+    if (cb) {
+        map->cb = cb;
+    } else {
+        map->cb = &default_callbacks;
+    }
+}
+
+void
+ovn_unpaired_port_binding_map_destroy(
+    struct ovn_unpaired_port_binding_map *map)
+{
+    struct ovn_unpaired_port_binding *pb;
+    struct shash_node *node;
+    SHASH_FOR_EACH_SAFE (node, &map->ports) {
+        pb = node->data;
+        shash_delete(&map->ports, node);
+        ovn_unpaired_port_binding_destroy(pb);
+        free(pb);
+    }
+    shash_destroy(&map->ports);
+}
diff --git a/northd/port_binding_pair.h b/northd/port_binding_pair.h
new file mode 100644
index 000000000..c76d30ca1
--- /dev/null
+++ b/northd/port_binding_pair.h
@@ -0,0 +1,117 @@ 
+/* Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef PORT_BINDING_PAIR_H
+#define PORT_BINDING_PAIR_H 1
+
+#include "openvswitch/hmap.h"
+#include "openvswitch/list.h"
+#include "openvswitch/shash.h"
+#include "smap.h"
+
+/* Port Binding pairing API. This file consists of utility functions
+ * that can be used when pairing northbound port types (e.g.
+ * Logical_Router_Port and Logical_Switch_Port) to southbound Port_Bindings.
+ *
+ * The basic flow of data is as such.
+ * 1. A northbound type is converted into an ovn_unpaired_port_binding.
+ * All ovn_unpaired_port_bindings are placed into an ovn_unpaired_datapath_map.
+ * 2. The en_port_binding_pair node takes all of the maps in as input and
+ * pairs them with southbound port bindings. This includes allocating
+ * tunnel keys across all ports. The output of this node is
+ * ovn_paired_port_bindings, which contains a list of all paired port bindings.
+ * 3. A northbound type-aware node then takes the ovn_paired_port_bindings,
+ * and decodes the generic paired port bindings back into a type-specific
+ * version (e.g. ovn_paired_logical_router_port). Later nodes can then consume
+ * these type-specific paired port binding types in order to perform
+ * further processing.
+ *
+ * It is important to note that this code pairs northbound ports to southbound
+ * port bindings, but it does not 100% sync them. The following fields are
+ * synced between the northbound port and the southbound Port_Binding:
+ * - logical_port
+ * - tunnel_key
+ * - external_ids
+ *
+ * Two later incremental engine nodes sync the rest of the fields on the Port
+ * Binding. en_northd syncs the vast majority of the data. Then finally,
+ * en_sync_to_sb syncs the nat_addresses of the Port_Binding.
+ */
+
+struct ovn_unpaired_port_binding {
+    uint32_t requested_tunnel_key;
+    struct smap external_ids;
+    void *cookie;
+    const char *name;
+    const char *type;
+    const struct sbrec_datapath_binding *sb_dp;
+};
+
+struct sbrec_port_binding;
+struct ovn_unpaired_port_binding_map_callbacks {
+    bool (*sb_is_valid)(const struct sbrec_port_binding *sp_pb,
+                        const struct ovn_unpaired_port_binding *upb);
+};
+
+struct ovn_unpaired_port_binding_map {
+    struct shash ports;
+    const struct ovn_unpaired_port_binding_map_callbacks *cb;
+};
+
+struct sbrec_port_binding;
+struct unpaired_port_data;
+
+struct unpaired_port_data_callbacks {
+    bool (*is_valid)(const struct unpaired_port_data *unpaired,
+                     const struct sbrec_port_binding *sp_pb);
+    struct ovn_unpaired_port_binding *
+        (*find)(const struct unpaired_port_data *unpaired,
+                const struct sbrec_port_binding *sb_pb);
+    void (*get_ports)(const struct unpaired_port_data *unpaired,
+                      struct shash *returned_ports);
+};
+
+struct unpaired_port_data {
+    void *private_data;
+    struct unpaired_port_data_callbacks *cb;
+};
+
+struct ovn_paired_port_binding {
+    struct ovs_list list_node;
+    const void *cookie;
+    const char *type;
+    const struct sbrec_port_binding *sb_pb;
+};
+
+struct ovn_paired_port_bindings {
+    struct ovs_list paired_pbs;
+    struct hmap tunnel_key_maps;
+};
+
+struct ovn_unpaired_port_binding *ovn_unpaired_port_binding_alloc(
+        uint32_t requested_tunnel_key, const char *name,
+        const char *type,
+        void *cookie,
+        const struct sbrec_datapath_binding *sb_dp);
+
+void ovn_unpaired_port_binding_destroy(struct ovn_unpaired_port_binding *pb);
+
+void ovn_unpaired_port_binding_map_init(
+    struct ovn_unpaired_port_binding_map *map,
+    const struct ovn_unpaired_port_binding_map_callbacks *cb);
+void ovn_unpaired_port_binding_map_destroy(
+    struct ovn_unpaired_port_binding_map *map);
+
+#endif /* PORT_BINDING_PAIR_H */