On Mon, Jun 15, 2026 at 5:46 PM Mairtin O'Loingsigh <[email protected]>
wrote:

> Enable the creation and management of transit switch ports, when
> created, these ports are available across multiple AZs and may share
> port binding depending on configuration. This patch also includes tests
> and a multinode test.
>
> Commands to add, set address and delete port
>     ovn-ic-nbctl tsp-add ts0 ts0-p0 chassis=chassis
>     ovn-ic-nbctl tsp-set-addr ts0-p0 "00:11:22:11:22:34
>         192.168.10.11/24"
>     ovn-ic-nbctl tsp-del ts0-p0
>
> Reported-at: https://issues.redhat.com/browse/FDP-2878
> Signed-off-by: Mairtin O'Loingsigh <[email protected]>
> ---
>

Hi Mairtin,

thank you for the v5. There are still a few things that need to be
addressed. See down below.


>
> Changes since v4:
>   - Fix router port type address sync.
>   - Sync requested chassis, type, peer, tunnel.
>   - Fix ICSB race by adding a leader check.
>
> Changes since v2:
>   - Remove function which was not needed.
>   - Dont overwrite router-id.
>   - Unify support functions.
>   - Remove left over debug code from tests.
>   - Add ovn-ic-nbctl.8.xml updates.
>   - Add tsp-del test.
>
> Changes since v1:
>   - Merge patches.
>   - Bump OVNIC NB version number.
>   - Add NEWS entry.
>   - Remove option column from TSP.
>   - Remove nb_uuid from TSP.
>   - Clean up coding standard violations.
>   - Fix missing port issue.
>
>  NEWS                         |   2 +
>  ic/ovn-ic.c                  | 389 +++++++++++++++++++++++++++--------
>  ic/ovn-ic.h                  |   1 +
>  lib/ovn-util.c               |  44 ++++
>  lib/ovn-util.h               |   4 +
>  ovn-ic-nb.ovsschema          |  26 ++-
>  ovn-ic-nb.xml                |  42 ++++
>  tests/multinode.at           |  44 +---
>  tests/ovn-ic-nbctl.at        |  47 +++++
>  tests/ovn-ic.at              | 315 ++++++++++++++++++++++++++++
>  utilities/ovn-ic-nbctl.8.xml |  56 +++++
>  utilities/ovn-ic-nbctl.c     | 248 +++++++++++++++++++++-
>  utilities/ovn-nbctl.c        |  52 +----
>  13 files changed, 1098 insertions(+), 172 deletions(-)
>
> diff --git a/NEWS b/NEWS
> index 748ae30eb..82d2999f9 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -1,5 +1,7 @@
>  Post v26.03.0
>  -------------
> +   - Added Transit Switch port support with new ovn-ic-nbctl 'tsp-add',
> +     'tsp-del' and 'tsp-set-addr' commands.
>     - Added ability to set any "ipsec_*" NB_Global option to configure the
>       IPsec backend.
>     - Documented missing ovn-nbctl commands: "mirror-rule-add",
> diff --git a/ic/ovn-ic.c b/ic/ovn-ic.c
> index 0a3f2336b..8efb083db 100644
> --- a/ic/ovn-ic.c
> +++ b/ic/ovn-ic.c
> @@ -733,6 +733,17 @@ get_lp_address_for_sb_pb(struct ic_context *ctx,
>      return peer->n_mac ? *peer->mac : NULL;
>  }
>
> +static const char *
> +get_lp_address_for_ts_pb(struct ic_context *ctx, const char *peer_name)
> +{
> +    const struct sbrec_port_binding *peer =
> +        find_sb_pb_by_name(ctx->sbrec_port_binding_by_name, peer_name);
> +    if (peer && peer->n_mac) {
> +        return *peer->mac;
> +    }
> +    return NULL;
> +}
> +
>  static const struct sbrec_chassis *
>  find_sb_chassis(struct ic_context *ctx, const char *name)
>  {
> @@ -817,52 +828,157 @@ update_isb_pb_external_ids(struct ic_context *ctx,
>      free(uuid_s);
>  }
>
> -/* For each local port:
> - *   - Sync from NB to ISB.
> - *   - Sync gateway from SB to ISB.
> - *   - Sync tunnel key from ISB to NB.
> - */
> +static const char *
> +get_ts_port_address(struct ic_context *ctx,
> +                    const struct icnbrec_transit_switch_port *tsp,
> +                    const struct sbrec_port_binding *sb_pb)
> +{
> +    if (tsp) {
> +        if (!strcmp(tsp->type, "router") && tsp->peer[0]) {
> +            return get_lp_address_for_ts_pb(ctx, tsp->peer);
> +        }
> +        return tsp->n_addresses ? tsp->addresses[0] : NULL;
> +    }
> +
> +    return sb_pb ? get_lp_address_for_sb_pb(ctx, sb_pb) : NULL;
> +}
> +
> +/* Sync a local port's fields from SB/TSP towards ISB and NB.
> + * sb_pb may be NULL when the NB LSP was just created and northd
> + * hasn't processed it yet; in that case gateway is set from
> + * tsp->chassis and external_ids:router-id is skipped. */
>  static void
>  sync_local_port(struct ic_context *ctx,
> +                const struct icnbrec_transit_switch_port *tsp,
>                  const struct icsbrec_port_binding *isb_pb,
> -                const struct sbrec_port_binding *sb_pb,
> -                const struct nbrec_logical_switch_port *lsp)
> +                const struct nbrec_logical_switch_port *lsp,
> +                const struct sbrec_port_binding *sb_pb)
>  {
> -    /* Sync address from NB to ISB */
> -    const char *address = get_lp_address_for_sb_pb(ctx, sb_pb);
> +    const char *address = get_ts_port_address(ctx, tsp, sb_pb);
> +    /* Sync address to ISB. */
>      if (!address) {
> -        VLOG_DBG("Can't get router/switch port address for logical"
> -                 " switch port %s", sb_pb->logical_port);
>          if (isb_pb->address[0]) {
>              icsbrec_port_binding_set_address(isb_pb, "");
>          }
> -    } else {
> -        if (strcmp(address, isb_pb->address)) {
> -            icsbrec_port_binding_set_address(isb_pb, address);
> +    } else if (strcmp(address, isb_pb->address)) {
> +        icsbrec_port_binding_set_address(isb_pb, address);
> +    }
> +
> +    /* Sync gateway to ISB. */
> +    if (sb_pb) {
> +        const struct sbrec_port_binding *crp =
> +            find_crp_for_sb_pb(ctx, sb_pb);
> +        if (crp && crp->chassis) {
> +            if (strcmp(crp->chassis->name, isb_pb->gateway)) {
> +                icsbrec_port_binding_set_gateway(isb_pb,
> +                                                 crp->chassis->name);
> +            }
> +        } else if (!strcmp(lsp->type, "switch") && sb_pb->chassis) {
> +            if (strcmp(sb_pb->chassis->name, isb_pb->gateway)) {
> +                icsbrec_port_binding_set_gateway(isb_pb,
> +                                                 sb_pb->chassis->name);
> +            }
> +        } else if (isb_pb->gateway[0] && !tsp) {
> +            icsbrec_port_binding_set_gateway(isb_pb, "");
> +        }
> +    } else if (tsp && tsp->chassis[0]) {
> +        if (strcmp(tsp->chassis, isb_pb->gateway)) {
> +            icsbrec_port_binding_set_gateway(isb_pb, tsp->chassis);
>          }
>      }
>
> -    /* Sync gateway from SB to ISB */
> -    const struct sbrec_port_binding *crp = find_crp_for_sb_pb(ctx, sb_pb);
> -    if (crp && crp->chassis) {
> -        if (strcmp(crp->chassis->name, isb_pb->gateway)) {
> -            icsbrec_port_binding_set_gateway(isb_pb, crp->chassis->name);
> +    /* Sync external_ids:router-id to ISB (requires sb_pb). */
> +    if (sb_pb) {
> +        update_isb_pb_external_ids(ctx, sb_pb, isb_pb);
> +    }
> +
> +    sync_lsp_tnl_key(lsp, isb_pb->tunnel_key);
> +}
> +
> +static bool
> +trp_is_remote(struct ic_context *ctx, const char *chassis_name)
> +{
> +    if (chassis_name) {
> +        const struct sbrec_chassis *chassis =
> +            find_sb_chassis(ctx, chassis_name);
> +        if (chassis) {
> +            return smap_get_bool(&chassis->other_config, "is-remote",
> false);
>          }
> -    } else if (!strcmp(lsp->type, "switch") && sb_pb->chassis) {
> -        if (strcmp(sb_pb->chassis->name, isb_pb->gateway)) {
> -            icsbrec_port_binding_set_gateway(isb_pb,
> sb_pb->chassis->name);
> +        return true;
> +    }
> +
> +    return false;
> +}
> +
> +static void
> +sync_tsp(struct ic_context *ctx, const struct icnbrec_transit_switch_port
> *tsp,
> +         const struct icsbrec_port_binding *isb_pb,
> +         const struct nbrec_logical_switch_port *lsp)
> +{
> +    /* Sync type and peer from TSP to NB. */
> +    if (!tsp->chassis[0] || !trp_is_remote(ctx, tsp->chassis)) {
> +        if (strcmp(lsp->type, tsp->type)) {
> +            nbrec_logical_switch_port_set_type(lsp, tsp->type);
> +            if (!strcmp(tsp->type, "router")) {
> +                if (tsp->peer[0]) {
> +                    nbrec_logical_switch_port_update_options_setkey(
> +                        lsp, "router-port", tsp->peer);
> +                }
> +                nbrec_logical_switch_port_set_addresses(
> +                    lsp, (const char *[]) {"router"}, 1);
>

This is a bit confusing setting the address even though this block is
taking care of the type and peer. It would make more sense to move
it down below.

+            } else {
> +                nbrec_logical_switch_port_update_options_delkey(
> +                    lsp, "router-port");
> +            }
>          }
>      } else {
> -        if (isb_pb->gateway[0]) {
> -            icsbrec_port_binding_set_gateway(isb_pb, "");
> +        if (strcmp(lsp->type, "remote")) {
> +            nbrec_logical_switch_port_set_type(lsp, "remote");
> +        }
> +    }
> +
> +    /* Sync address to NB (skip for router type, which
> +     * uses "router" as its NB address). */
> +    const char *address = get_ts_port_address(ctx, tsp, NULL);
>

We don't need the helper right? The address is set only if the type is not
router, and in that case we can copy directly what is in the IC-NB.
Which we would end up doing anyway. Arguably the TSP of type router
should actually have address "router". We could just copy it regardless
of the type WDYT?


> +    if (strcmp(tsp->type, "router")) {
> +        if (!address) {
> +            if (lsp->n_addresses) {
> +                nbrec_logical_switch_port_set_addresses(lsp, NULL, 0);
> +            }
> +        } else if (!lsp->n_addresses || strcmp(address,
> lsp->addresses[0])) {
> +            nbrec_logical_switch_port_set_addresses(
> +                lsp, (const char **) &address, 1);
>          }
>      }
>
> -    /* Sync external_ids:router-id to ISB */
> -    update_isb_pb_external_ids(ctx, sb_pb, isb_pb);
> +    if (tsp->chassis[0]) {
> +        const char *current = smap_get(&lsp->options,
> "requested-chassis");
> +        if (!current || strcmp(current, tsp->chassis)) {
> +            nbrec_logical_switch_port_update_options_setkey(
> +                lsp, "requested-chassis", tsp->chassis);
> +        }
> +    } else if (smap_get(&lsp->options, "requested-chassis")) {
> +        nbrec_logical_switch_port_update_options_delkey(
> +            lsp, "requested-chassis");
> +    }
>
> -    /* Sync back tunnel key from ISB to NB */
> +    /* Sync tunnel key from ISB to NB. */
>      sync_lsp_tnl_key(lsp, isb_pb->tunnel_key);
> +
> +    if (tsp->peer[0]) {
> +        if (!lsp->peer || strcmp(lsp->peer, tsp->peer)) {
> +            nbrec_logical_switch_port_set_peer(lsp, tsp->peer);
> +        }
> +        if (!strcmp(lsp->type, "router")) {
> +            const char *current = smap_get(&lsp->options, "router-port");
> +            if (!current || strcmp(current, tsp->peer)) {
> +                nbrec_logical_switch_port_update_options_setkey(
> +                    lsp, "router-port", tsp->peer);
> +            }
> +        }
> +    } else if (lsp->peer && lsp->peer[0]) {
> +        nbrec_logical_switch_port_set_peer(lsp, "");
> +    }
>  }
>
>  /* For each remote port:
> @@ -992,7 +1108,7 @@ allocate_port_key(struct hmap *pb_tnlids)
>  }
>
>  static const struct icsbrec_port_binding *
> -create_isb_pb(struct ic_context *ctx, const char *logical_port,
> +create_isb_pb(struct ovsdb_idl_txn *isb_txn, const char *logical_port,
>                const struct icsbrec_availability_zone *az, const char
> *ts_name,
>                const struct uuid *nb_ic_uuid, const char *type,
>                struct hmap *pb_tnlids)
> @@ -1003,7 +1119,7 @@ create_isb_pb(struct ic_context *ctx, const char
> *logical_port,
>      }
>
>      const struct icsbrec_port_binding *isb_pb =
> -        icsbrec_port_binding_insert(ctx->ovnisb_unlocked_txn);
> +        icsbrec_port_binding_insert(isb_txn);
>      icsbrec_port_binding_set_availability_zone(isb_pb, az);
>      icsbrec_port_binding_set_transit_switch(isb_pb, ts_name);
>      icsbrec_port_binding_set_logical_port(isb_pb, logical_port);
> @@ -1027,22 +1143,6 @@ get_lrp_by_lrp_name(struct ic_context *ctx, const
> char *lrp_name)
>      return lrp;
>  }
>
> -static bool
> -trp_is_remote(struct ic_context *ctx, const char *chassis_name)
> -{
> -    if (chassis_name) {
> -        const struct sbrec_chassis *chassis =
> -            find_sb_chassis(ctx, chassis_name);
> -        if (chassis) {
> -            return smap_get_bool(&chassis->other_config, "is-remote",
> false);
> -        } else {
> -            return true;
> -        }
> -    }
> -
> -    return false;
> -}
> -
>  static struct nbrec_logical_router_port *
>  lrp_create(struct ic_context *ctx, const struct nbrec_logical_router *lr,
>             const struct icnbrec_transit_router_port *trp)
> @@ -1057,28 +1157,38 @@ lrp_create(struct ic_context *ctx, const struct
> nbrec_logical_router *lr,
>      return lrp;
>  }
>
> -static void
> -sync_ts_isb_pb(struct ic_context *ctx, const struct sbrec_port_binding
> *sb_pb,
> -               const struct icsbrec_port_binding *isb_pb)
> +static struct nbrec_logical_switch_port *
> +lsp_create(struct ic_context *ctx, const struct nbrec_logical_switch *ls,
> +           const struct icnbrec_transit_switch_port *tsp)
>  {
> -    const char *address = get_lp_address_for_sb_pb(ctx, sb_pb);
> -    if (address) {
> -        icsbrec_port_binding_set_address(isb_pb, address);
> -    }
> +    bool router_port = !strcmp(tsp->type, "router");
> +
> +    struct nbrec_logical_switch_port *lsp =
> +        nbrec_logical_switch_port_insert(ctx->ovnnb_txn);
> +    nbrec_logical_switch_port_set_name(lsp, tsp->name);
>
> -    const struct sbrec_port_binding *crp = find_crp_for_sb_pb(ctx, sb_pb);
> -    if (crp && crp->chassis) {
> -        icsbrec_port_binding_set_gateway(isb_pb, crp->chassis->name);
> +    nbrec_logical_switch_port_update_options_setkey(lsp, "interconn-ts",
> +                                                    tsp->name);
> +    if (tsp->peer[0]) {
> +        nbrec_logical_switch_port_set_peer(lsp, tsp->peer);
>      }
>
> -    update_isb_pb_external_ids(ctx, sb_pb, isb_pb);
> +    if (router_port) {
> +        if (tsp->peer[0]) {
> +            nbrec_logical_switch_port_update_options_setkey(
> +                lsp, "router-port", tsp->peer);
> +        }
> +
> +        nbrec_logical_switch_port_set_addresses(
> +            lsp, (const char *[]) {"router"}, 1);
> +    } else {
> +        nbrec_logical_switch_port_set_addresses(lsp,
> +                                                (const char **)
> tsp->addresses,
> +                                                tsp->n_addresses);
> +    }
>
> -    /* XXX: Sync encap so that multiple encaps can be used for the same
> -     * gateway.  However, it is not needed for now, since we don't yet
> -     * support specifying encap type/ip for gateway chassis or ha-chassis
> -     * for logical router port in NB DB, and now encap should always be
> -     * empty.  The sync can be added if we add such support for gateway
> -     * chassis/ha-chassis in NB DB. */
> +    nbrec_logical_switch_update_ports_addvalue(ls, lsp);
> +    return lsp;
>  }
>
>  static const struct sbrec_port_binding *
> @@ -1118,7 +1228,6 @@ port_binding_run(struct ic_context *ctx)
>      }
>      icsbrec_port_binding_index_destroy_row(isb_pb_key);
>
> -    const struct sbrec_port_binding *sb_pb;
>      const struct icnbrec_transit_switch *ts;
>      ICNBREC_TRANSIT_SWITCH_FOR_EACH (ts, ctx->ovninb_idl) {
>          const struct nbrec_logical_switch *ls = find_ts_in_nb(ctx,
> ts->name);
> @@ -1126,9 +1235,20 @@ port_binding_run(struct ic_context *ctx)
>              VLOG_DBG("Transit switch %s not found in NB.", ts->name);
>              continue;
>          }
> +        struct shash nb_ports = SHASH_INITIALIZER(&nb_ports);
> +        struct shash old_nb_ports = SHASH_INITIALIZER(&old_nb_ports);
>          struct shash local_pbs = SHASH_INITIALIZER(&local_pbs);
>          struct shash remote_pbs = SHASH_INITIALIZER(&remote_pbs);
>
> +        for (size_t i = 0; i < ls->n_ports; i++) {
> +            const struct nbrec_logical_switch_port *lsp = ls->ports[i];
> +            if (smap_get(&lsp->options, "interconn-ts")) {
> +                shash_add(&nb_ports, lsp->name, lsp);
> +            } else {
> +                shash_add(&old_nb_ports, lsp->name, lsp);
> +            }
> +        }
> +
>          isb_pb_key = icsbrec_port_binding_index_init_row(
>              ctx->icsbrec_port_binding_by_ts);
>          icsbrec_port_binding_index_set_transit_switch(isb_pb_key,
> ts->name);
> @@ -1145,9 +1265,70 @@ port_binding_run(struct ic_context *ctx)
>          }
>          icsbrec_port_binding_index_destroy_row(isb_pb_key);
>
> +        for (size_t i = 0; i < ts->n_ports; i++) {
> +            struct icnbrec_transit_switch_port *tsp = ts->ports[i];
> +            bool is_owner = false;
> +
> +            if (!tsp->chassis[0]) {
> +                const struct icsbrec_port_binding *isb_pb_key_name =
> +                    icsbrec_port_binding_index_init_row(
> +                        ctx->icsbrec_port_binding_by_name);
> +
> icsbrec_port_binding_index_set_logical_port(isb_pb_key_name,
> +                                                            tsp->name);
> +                isb_pb = icsbrec_port_binding_index_find(
> +                    ctx->icsbrec_port_binding_by_name, isb_pb_key_name);
> +                icsbrec_port_binding_index_destroy_row(isb_pb_key_name);
> +                if (isb_pb) {
> +                    shash_find_and_delete(&local_pbs, tsp->name);
> +                    shash_find_and_delete(&remote_pbs, tsp->name);
> +                }
>

I'm still slightly confused why do we need the lookup? The isb_pb
should end up in one of those shashes. So maybe doing a lookup in
both? If it's not in either we need to create a new one anyway.

+
> +                if (ctx->ovnisb_txn && is_az_leader(ctx->ovnisb_txn)) {
> +                    if (!isb_pb) {
> +                        isb_pb = create_isb_pb(
> +                            ctx->ovnisb_txn, tsp->name, ctx->runned_az,
> +                            ts->name, &ts->header_.uuid,
> "transit-switch-port",
> +                            &pb_tnlids);
> +                    }
> +                    is_owner = true;
> +                }
> +            } else if (!trp_is_remote(ctx, tsp->chassis)) {
> +                /* Create ISB port_binding as its chassis is local. */
> +                isb_pb = shash_find_and_delete(&local_pbs, tsp->name);
> +                if (!isb_pb) {
> +                    isb_pb = create_isb_pb(ctx->ovnisb_unlocked_txn,
> tsp->name,
> +                                           ctx->runned_az,
> +                                           ts->name, &ts->header_.uuid,
> +                                           "transit-switch-port",
> &pb_tnlids);
> +                }
> +                is_owner = true;
> +            } else {
> +                isb_pb = shash_find_and_delete(&remote_pbs, tsp->name);
> +            }
> +
> +            if (!isb_pb) {
> +                continue;
> +            }
> +
> +            const struct nbrec_logical_switch_port *lsp =
> +                shash_find_and_delete(&nb_ports, tsp->name);
> +            if (!lsp) {
> +                lsp = lsp_create(ctx, ls, tsp);
> +            }
> +
> +            const struct sbrec_port_binding *sb_pb = find_lsp_in_sb(ctx,
> lsp);
> +            if (is_owner) {
> +                sync_local_port(ctx, tsp, isb_pb, lsp, sb_pb);
> +            }
> +
> +            sync_tsp(ctx, tsp, isb_pb, lsp);
> +        }
> +
> +        /* Support legacy way of adding transit switch ports. */
> +        const struct sbrec_port_binding *sb_pb;
>          const struct nbrec_logical_switch_port *lsp;
> -        for (int i = 0; i < ls->n_ports; i++) {
> -            lsp = ls->ports[i];
> +        SHASH_FOR_EACH (node, &old_nb_ports) {
>

I think that we should leave the legacy way completely intact.
There are some similarities which could be handled by common
helpers, but I'm afraid this will create confusion. I would suggest
to focus only on the tsp sync, that is IC-NB -> IC-SB sync.

The part for syncing into IC-SB -> NB seems to fine with some minor
comments.

+            lsp = node->data;
>
>              if (!strcmp(lsp->type, "router")
>                  || !strcmp(lsp->type, "switch")) {
> @@ -1158,23 +1339,18 @@ port_binding_run(struct ic_context *ctx)
>                  }
>                  isb_pb = shash_find_and_delete(&local_pbs, lsp->name);
>                  if (!isb_pb) {
> -                    isb_pb = create_isb_pb(
> -                        ctx, sb_pb->logical_port, ctx->runned_az,
> ts->name,
> -                        &ts->header_.uuid, "transit-switch-port",
> &pb_tnlids);
> -                    sync_ts_isb_pb(ctx, sb_pb, isb_pb);
> -                } else {
> -                    sync_local_port(ctx, isb_pb, sb_pb, lsp);
> -                }
> -
> -                if (isb_pb->type) {
> -                    icsbrec_port_binding_set_type(isb_pb,
> -                                                  "transit-switch-port");
> -                }
> -
> -                if (isb_pb->nb_ic_uuid) {
> -                    icsbrec_port_binding_set_nb_ic_uuid(isb_pb,
> -
> &ts->header_.uuid, 1);
> +                    isb_pb = create_isb_pb(ctx->ovnisb_unlocked_txn,
> +                                           sb_pb->logical_port,
> +                                           ctx->runned_az, ts->name,
> +                                           &ts->header_.uuid,
> +                                           "transit-switch-port",
> &pb_tnlids);
> +                    if (!isb_pb) {
> +                        VLOG_DBG("Tunnel ID allocation failed for %s",
> +                                 sb_pb->logical_port);
> +                        continue;
> +                    }
>                  }
> +                sync_local_port(ctx, NULL, isb_pb, lsp, sb_pb);
>              } else if (!strcmp(lsp->type, "remote")) {
>                  /* The port is remote. */
>                  isb_pb = shash_find_and_delete(&remote_pbs, lsp->name);
> @@ -1186,13 +1362,28 @@ port_binding_run(struct ic_context *ctx)
>                          continue;
>                      }
>                      sync_remote_port(ctx, isb_pb, lsp, sb_pb);
> +                    if (!isb_pb->type[0]) {
> +                        icsbrec_port_binding_set_type(isb_pb,
> +
> "transit-switch-port");
> +                    }
> +
> +                    if (!isb_pb->n_nb_ic_uuid) {
> +                        icsbrec_port_binding_set_nb_ic_uuid(
> +                            isb_pb, &ts->header_.uuid, 1);
> +                    }
>                  }
> +
>              } else {
>                  VLOG_DBG("Ignore lsp %s on ts %s with type %s.",
>                           lsp->name, ts->name, lsp->type);
>              }
>          }
>
> +        SHASH_FOR_EACH (node, &nb_ports) {
> +            nbrec_logical_switch_port_delete(node->data);
> +            nbrec_logical_switch_update_ports_delvalue(ls, node->data);
> +        }
> +
>          /* Delete extra port-binding from ISB */
>          SHASH_FOR_EACH (node, &local_pbs) {
>              icsbrec_port_binding_delete(node->data);
> @@ -1203,8 +1394,10 @@ port_binding_run(struct ic_context *ctx)
>              create_nb_lsp(ctx, node->data, ls);
>          }
>
> +        shash_destroy(&nb_ports);
>          shash_destroy(&local_pbs);
>          shash_destroy(&remote_pbs);
> +        shash_destroy(&old_nb_ports);
>      }
>
>      SHASH_FOR_EACH (node, &switch_all_local_pbs) {
> @@ -1255,7 +1448,8 @@ port_binding_run(struct ic_context *ctx)
>              } else {
>                  isb_pb = shash_find_and_delete(&local_pbs, trp->name);
>                  if (!isb_pb) {
> -                    isb_pb = create_isb_pb(ctx, trp->name, ctx->runned_az,
> +                    isb_pb = create_isb_pb(ctx->ovnisb_unlocked_txn,
> trp->name,
> +                                           ctx->runned_az,
>                                             tr->name, &tr->header_.uuid,
>                                             "transit-router-port",
> &pb_tnlids);
>                      icsbrec_port_binding_set_address(isb_pb, trp->mac);
> @@ -1275,7 +1469,7 @@ port_binding_run(struct ic_context *ctx)
>              }
>          }
>
> -        SHASH_FOR_EACH(node, &nb_ports) {
> +        SHASH_FOR_EACH (node, &nb_ports) {
>              nbrec_logical_router_port_delete(node->data);
>              nbrec_logical_router_update_ports_delvalue(lr, node->data);
>          }
> @@ -2629,16 +2823,30 @@ route_run(struct ic_context *ctx)
>          const struct nbrec_logical_switch_port *nb_lsp;
>
>          nb_lsp = get_lsp_by_ts_port_name(ctx, isb_pb->logical_port);
> -        if (!strcmp(nb_lsp->type, "switch")) {
> -            VLOG_DBG("IC-SB Port_Binding '%s' on ts '%s' corresponds to a
> "
> -                     "switch port, not considering for route collection.",
> -                     isb_pb->logical_port, isb_pb->transit_switch);
> +        if (!nb_lsp) {
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
> +            VLOG_DBG_RL(&rl,
> +                        "IC-SB Port_Binding '%s' on ts '%s': "
> +                        "NB LSP not found, skipping route collection.",
> +                        isb_pb->logical_port, isb_pb->transit_switch);
> +            continue;
> +        }
> +
> +        if (!strcmp(nb_lsp->type, "switch") || !strcmp(nb_lsp->type, ""))
> {
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
> +            VLOG_DBG_RL(&rl,
> +                        "IC-SB Port_Binding '%s' on ts '%s' corresponds
> to a "
> +                        "switch port, not considering for route
> collection.",
> +                        isb_pb->logical_port, isb_pb->transit_switch);
>              continue;
>          }
>
>          const char *ts_lrp_name =
>              get_lrp_name_by_ts_port_name(ctx, isb_pb->logical_port);
>          if (!ts_lrp_name) {
> +            if (!strcmp(isb_pb->type, "transit-switch-port")) {
> +                continue;
> +            }
>              static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
>              VLOG_WARN_RL(&rl, "Route sync ignores port %s on ts %s
> because "
>                           "logical router port is not found in NB.
> Deleting it",
> @@ -3575,6 +3783,8 @@ main(int argc, char *argv[])
>                                 &nbrec_logical_switch_port_col_enabled);
>      ovsdb_idl_track_add_column(ovnnb_idl_loop.idl,
>
> &nbrec_logical_switch_port_col_external_ids);
> +    ovsdb_idl_track_add_column(ovnnb_idl_loop.idl,
> +                               &nbrec_logical_switch_port_col_peer);
>
>      ovsdb_idl_add_table(ovnnb_idl_loop.idl, &nbrec_table_load_balancer);
>      ovsdb_idl_track_add_column(ovnnb_idl_loop.idl,
> @@ -3720,6 +3930,10 @@ main(int argc, char *argv[])
>          = ovsdb_idl_index_create1(ovnisb_unlocked_idl_loop.idl,
>
>  &icsbrec_port_binding_col_transit_switch);
>
> +    struct ovsdb_idl_index *icsbrec_port_binding_by_name
> +        = ovsdb_idl_index_create1(ovnisb_unlocked_idl_loop.idl,
> +                                  &icsbrec_port_binding_col_logical_port);
> +
>      struct ovsdb_idl_index *icsbrec_port_binding_by_ts_az
>          = ovsdb_idl_index_create2(ovnisb_unlocked_idl_loop.idl,
>
>  &icsbrec_port_binding_col_transit_switch,
> @@ -3912,6 +4126,7 @@ main(int argc, char *argv[])
>                      icnbrec_transit_switch_by_name,
>                  .icsbrec_port_binding_by_az = icsbrec_port_binding_by_az,
>                  .icsbrec_port_binding_by_ts = icsbrec_port_binding_by_ts,
> +                .icsbrec_port_binding_by_name =
> icsbrec_port_binding_by_name,
>                  .icsbrec_port_binding_by_ts_az =
> icsbrec_port_binding_by_ts_az,
>                  .icsbrec_route_by_az = icsbrec_route_by_az,
>                  .icsbrec_route_by_ts = icsbrec_route_by_ts,
> diff --git a/ic/ovn-ic.h b/ic/ovn-ic.h
> index 9f52bb0f9..b41c214e0 100644
> --- a/ic/ovn-ic.h
> +++ b/ic/ovn-ic.h
> @@ -43,6 +43,7 @@ struct ic_context {
>      struct ovsdb_idl_index *icnbrec_transit_switch_by_name;
>      struct ovsdb_idl_index *icsbrec_port_binding_by_az;
>      struct ovsdb_idl_index *icsbrec_port_binding_by_ts;
> +    struct ovsdb_idl_index *icsbrec_port_binding_by_name;
>      struct ovsdb_idl_index *icsbrec_port_binding_by_ts_az;
>      struct ovsdb_idl_index *icsbrec_route_by_az;
>      struct ovsdb_idl_index *icsbrec_route_by_ts;
> diff --git a/lib/ovn-util.c b/lib/ovn-util.c
> index e6143d7a9..8ec22f81a 100644
> --- a/lib/ovn-util.c
> +++ b/lib/ovn-util.c
> @@ -1850,3 +1850,47 @@ eth_addr_parse_masked(const char *s, struct
> eth_addr *ea, unsigned int *plen)
>      *ea = eth_addr_zero;
>      return false;
>  }
> +
> +bool
> +port_contains_duplicate_ip(struct lport_addresses *laddrs1,
> +                          struct lport_addresses *laddrs2,
> +                          const char *port_name,
> +                          char **error_str)
> +{
> +    for (size_t i = 0; i < laddrs1->n_ipv4_addrs; i++) {
> +        for (size_t j = 0; j < laddrs2->n_ipv4_addrs; j++) {
> +            if (laddrs1->ipv4_addrs[i].addr ==
> laddrs2->ipv4_addrs[j].addr) {
> +                if (error_str) {
> +                    *error_str = xasprintf("duplicate IPv4 address '%s' "
> +                                           "found on logical switch "
> +                                           "port '%s'",
> +                                           laddrs1->ipv4_addrs[i].addr_s,
> +                                           port_name);
> +                }
> +                return true;
> +            }
> +        }
> +    }
> +
> +    for (size_t i = 0; i < laddrs1->n_ipv6_addrs; i++) {
> +        for (size_t j = 0; j < laddrs2->n_ipv6_addrs; j++) {
> +            if (IN6_ARE_ADDR_EQUAL(&laddrs1->ipv6_addrs[i].addr,
> +                                   &laddrs2->ipv6_addrs[j].addr)) {
> +                if (error_str) {
> +                    *error_str = xasprintf("duplicate IPv6 address "
> +                                           "'%s' found on logical "
> +                                           "switch port '%s'",
> +                                           laddrs1->ipv6_addrs[i].addr_s,
> +                                           port_name);
> +                }
> +                return true;
> +            }
> +        }
> +    }
> +
> +    if (error_str) {
> +        *error_str = NULL;
> +    }
> +
> +    return false;
> +}
> diff --git a/lib/ovn-util.h b/lib/ovn-util.h
> index bfca178e4..eb88f8549 100644
> --- a/lib/ovn-util.h
> +++ b/lib/ovn-util.h
> @@ -794,6 +794,10 @@ char *normalize_ipv6_addr_str(const char *orig_addr);
>
>  char *normalize_addr_str(const char *orig_addr);
>
> +bool port_contains_duplicate_ip(struct lport_addresses *laddrs1,
> +                                struct lport_addresses *laddrs2,
> +                                const char *name, char **error_str);
> +
>  #define NEIGH_REDISTRIBUTE_MODES    \
>      NEIGH_REDISTRIBUTE_MODE(FDB, 0) \
>      NEIGH_REDISTRIBUTE_MODE(IP, 1)
> diff --git a/ovn-ic-nb.ovsschema b/ovn-ic-nb.ovsschema
> index ca67a2fa9..9628e5824 100644
> --- a/ovn-ic-nb.ovsschema
> +++ b/ovn-ic-nb.ovsschema
> @@ -1,7 +1,7 @@
>  {
>      "name": "OVN_IC_Northbound",
> -    "version": "1.3.0",
> -    "cksum": "1918565391 5082",
> +    "version": "1.4.0",
> +    "cksum": "518084103 6175",
>      "tables": {
>          "IC_NB_Global": {
>              "columns": {
> @@ -24,9 +24,31 @@
>                               "min": 0, "max": "unlimited"}}},
>              "maxRows": 1,
>              "isRoot": true},
> +        "Transit_Switch_Port": {
> +            "columns": {
> +                "name": {"type": "string"},
> +                "other_config": {
> +                    "type": {"key": "string", "value": "string",
> +                             "min": 0, "max": "unlimited"}},
> +                "type": {"type": "string"},
> +                "chassis": {"type": "string"},
> +                "peer": {"type": "string"},
> +                "addresses": {"type": {"key": "string",
> +                                       "min": 0,
> +                                       "max": "unlimited"}},
> +                "external_ids": {
> +                    "type": {"key": "string", "value": "string",
> +                             "min": 0, "max": "unlimited"}}},
> +            "isRoot": false,
> +            "indexes": [["name"]]},
>          "Transit_Switch": {
>              "columns": {
>                  "name": {"type": "string"},
> +                "ports": {"type": {"key": {"type": "uuid",
> +                                           "refTable":
> "Transit_Switch_Port",
> +                                           "refType": "strong"},
> +                                   "min": 0,
> +                                   "max": "unlimited"}},
>                  "other_config": {
>                      "type": {"key": "string", "value": "string",
>                               "min": 0, "max": "unlimited"}},
> diff --git a/ovn-ic-nb.xml b/ovn-ic-nb.xml
> index a3a35baf2..669f9f7aa 100644
> --- a/ovn-ic-nb.xml
> +++ b/ovn-ic-nb.xml
> @@ -112,6 +112,10 @@
>        </column>
>      </group>
>
> +    <column name="ports">
> +      The transit switch's <ref table="Transit_Switch_Port"/> ports.
> +    </column>
> +
>      <group title="Common Columns">
>        <column name="other_config"/>
>        <column name="external_ids">
> @@ -190,6 +194,44 @@
>      </group>
>    </table>
>
> +  <table name="Transit_Switch_Port" title="Transit logical switch port">
> +    <p>
> +      Each row represents one transit logical switch port for
> interconnection
> +      between different OVN deployments (availability zones).
> +    </p>
> +
> +    <group title="Naming">
> +      <column name="name">
> +        A name that uniquely identifies the transit logical switch port.
> +      </column>
> +    </group>
> +
> +    <column name="type">
> +      The type of the port.  Set to <code>router</code> for a port
> +      connected to a logical router, or leave empty for a VIF port.
> +    </column>
> +
> +    <column name="chassis">
> +      The chassis this switch port should be bound to.  If empty,
> +      the port is local to all availability zones.
> +    </column>
> +
> +    <column name="peer">
> +      The name of the peer logical router port, used when
> +      <ref column="type"/> is <code>router</code>.
> +    </column>
> +
> +    <column name="addresses">
> +      The addresses associated with this port.  Used when
> +      <ref column="type"/> is not <code>router</code>.
> +    </column>
> +
> +    <group title="Common Columns">
> +      <column name="other_config"/>
> +      <column name="external_ids"/>
> +    </group>
> +  </table>
> +
>    <table name="SSL">
>      SSL/TLS configuration for ovn-nb database access.
>
> diff --git a/tests/multinode.at b/tests/multinode.at
> index 069f2a677..1ce75f4d8 100644
> --- a/tests/multinode.at
> +++ b/tests/multinode.at
> @@ -2354,7 +2354,7 @@ fi
>
>  AT_CLEANUP
>
> -AT_SETUP([ovn multinode - Transit Router])
> +AT_SETUP([ovn multinode - Transit Router + Transit Switch])
>
>  # Check that ovn-fake-multinode setup is up and running
>  check_fake_multinode_setup
> @@ -2424,7 +2424,7 @@ for i in 1 2; do
>      check m_as $chassis ovs-vsctl set open .
> external_ids:ovn-is-interconn=true
>  done
>
> -# Do the ovn-ic setup.
> +# Add TR and TS
>  check m_central_as ovn-ic-nbctl tr-add tr
>  check m_central_as ovn-ic-nbctl trp-add tr tr-gw1 \
>      00:00:00:00:30:02 100.65.0.2/30 100:65::2/126 \
> @@ -2432,6 +2432,14 @@ check m_central_as ovn-ic-nbctl trp-add tr tr-gw1 \
>  check m_central_as ovn-ic-nbctl trp-add tr tr-gw2 \
>      00:00:00:00:30:06 100.65.0.6/30 100:65::6/126 \
>      chassis=ovn-chassis-2
> +check m_central_as ovn-ic-nbctl ts-add ts
> +check m_central_as ovn-ic-nbctl tsp-add ts ts-tr type=router peer=tr-ts
> +check m_central_as ovn-ic-nbctl tsp-add ts pod10 chassis=ovn-chassis-1
> +check m_central_as ovn-ic-nbctl tsp-set-addr pod10 "00:00:00:00:10:10
> 10.100.200.10 10:200::10"
> +check m_central_as ovn-ic-nbctl tsp-add ts pod20 chassis=ovn-chassis-2
> +check m_central_as ovn-ic-nbctl tsp-set-addr pod20 "00:00:00:00:10:20
> 10.100.200.20 10:200::20"
> +check m_central_as ovn-ic-nbctl tsp-add ts mgmt
> +check m_central_as ovn-ic-nbctl tsp-set-addr mgmt "00:00:00:00:10:11
> 10.100.200.11 10:200::11"
>  check m_central_as ovn-ic-nbctl --wait=sb sync
>
>  for i in 1 2; do
> @@ -2446,29 +2454,9 @@ for i in 1 2; do
>
>      check m_as $chassis ovn-nbctl set logical_router gw
> options:chassis=$chassis
>
> -    # Add TR and set the same tunnel key for both chassis
> -    check m_as $chassis ovn-nbctl ls-add ts
> -    check m_as $chassis ovn-nbctl set logical_switch ts
> other_config:requested-tnl-key=10
> -
> -    check m_as $chassis ovn-nbctl lsp-add-router-port ts ts-tr tr-ts
> -
>      check m_as $chassis ovn-nbctl lrp-add tr tr-ts 00:00:00:00:10:00
> 10.100.200.1/24 10:200::1/64
> -    check m_as $chassis ovn-nbctl set logical_router tr
> options:requested-tnl-key=20
>      check m_as $chassis ovn-nbctl lrp-set-gateway-chassis tr-ts $chassis
>
> -    # Add TS pods, with the same tunnel keys on both sides
> -    check m_as $chassis ovn-nbctl lsp-add ts pod10
> -    check m_as $chassis ovn-nbctl lsp-set-addresses pod10
> "00:00:00:00:10:10 10.100.200.10 10:200::10"
> -    check m_as $chassis ovn-nbctl set logical_switch_port pod10
> options:requested-tnl-key=10
> -
> -    check m_as $chassis ovn-nbctl lsp-add ts pod20
> -    check m_as $chassis ovn-nbctl lsp-set-addresses pod20
> "00:00:00:00:10:20 10.100.200.20 10:200::20"
> -    check m_as $chassis ovn-nbctl set logical_switch_port pod20
> options:requested-tnl-key=20
> -
> -    # Add mgmt pod
> -    check m_as $chassis ovn-nbctl lsp-add ts mgmt
> -    check m_as $chassis ovn-nbctl lsp-set-addresses mgmt
> "00:00:00:00:10:11 10.100.200.11 10:200::11"
> -    check m_as $chassis ovn-nbctl set logical_switch_port mgmt
> options:requested-tnl-key=11
>  done
>
>  # Add SNAT for the GW router that corresponds to "gw-tr" LRP IP
> @@ -2499,18 +2487,6 @@ check m_as ovn-chassis-1 ovn-nbctl set
> logical_router tr options:ct-commit-all="
>  check m_as ovn-chassis-1 ovn-nbctl lsp-add public external
>  check m_as ovn-chassis-1 ovn-nbctl lsp-set-addresses external
> "00:00:00:00:20:10 192.168.100.10 1000::10"
>
> -# Configure ports on the transit switch as remotes
> -check m_as ovn-chassis-1 ovn-nbctl lsp-set-type pod20 remote
> -check m_as ovn-chassis-1 ovn-nbctl lsp-set-options pod10
> requested-chassis=ovn-chassis-1
> -check m_as ovn-chassis-1 ovn-nbctl lsp-set-options mgmt
> requested-chassis=ovn-chassis-1
> -check m_as ovn-chassis-1 ovn-nbctl lsp-set-options pod20
> requested-chassis=ovn-chassis-2
> -
> -check m_as ovn-chassis-2 ovn-nbctl lsp-set-type pod10 remote
> -check m_as ovn-chassis-2 ovn-nbctl lsp-set-type mgmt remote
> -check m_as ovn-chassis-2 ovn-nbctl lsp-set-options pod10
> requested-chassis=ovn-chassis-1
> -check m_as ovn-chassis-2 ovn-nbctl lsp-set-options mgmt
> requested-chassis=ovn-chassis-1
> -check m_as ovn-chassis-2 ovn-nbctl lsp-set-options pod20
> requested-chassis=ovn-chassis-2
> -
>  m_as ovn-chassis-1 /data/create_fake_vm.sh external external
> 00:00:00:00:20:10 1500 192.168.100.10 24 192.168.100.1 1000::10/64 1000::1
>  m_as ovn-chassis-1 /data/create_fake_vm.sh pod10 pod10 00:00:00:00:10:10
> 1500 10.100.200.10 24 10.100.200.1 10:200::10/64 10:200::1
>  m_as ovn-chassis-1 /data/create_fake_vm.sh mgmt mgmt 00:00:00:00:10:11
> 1500 10.100.200.11 24 10.100.200.1 10:200::11/64 10:200::1
> diff --git a/tests/ovn-ic-nbctl.at b/tests/ovn-ic-nbctl.at
> index 4c5269784..eb333edec 100644
> --- a/tests/ovn-ic-nbctl.at
> +++ b/tests/ovn-ic-nbctl.at
> @@ -61,6 +61,53 @@ AT_CHECK([ovn-ic-nbctl ts-del ts2], [1], [],
>
>  AT_CHECK([ovn-ic-nbctl --if-exists ts-del ts2])
>
> +AT_CHECK([ovn-ic-nbctl --may-exist ts-add ts0])
> +AT_CHECK([ovn-ic-nbctl ts-list | uuidfilt], [0], [dnl
> +<0> (ts0)
> +])
> +
> +AT_CHECK([ovn-ic-nbctl tsp-add], [1], [],
> +  [ovn-ic-nbctl: 'tsp-add' command requires at least 2 arguments
> +])
> +
> +AT_CHECK([ovn-ic-nbctl tsp-add ts0], [1], [],
> +  [ovn-ic-nbctl: 'tsp-add' command requires at least 2 arguments
> +])
> +
> +AT_CHECK([ovn-ic-nbctl tsp-add ts0 ts0-p0 chassis=chassis])
> +AT_CHECK([ovn-ic-nbctl tsp-add ts0 ts0-p0], [1], [],
> +    [ovn-ic-nbctl: ts0-p0: a port with this name already exists
> +])
> +
> +AT_CHECK([ovn-ic-nbctl --may-exist tsp-add ts0 ts0-p0 chassis=chassis])
> +AT_CHECK([ovn-ic-nbctl tsp-set-addr ts0-p0 "00:11:22:11:22:33
> 192.168.10.10"])
> +AT_CHECK([ovn-ic-nbctl tsp-set-addr ts0-p1 "00:11:22:11:22:34
> 192.168.10.11"], [1], [],
> +    [ovn-ic-nbctl: ts0-p1: switch port name not found
> +])
> +
> +check_column "00:11:22:11:22:33 192.168.10.10" ic-nb:transit_switch_port
> addresses name=ts0-p0
> +check_column "" ic-nb:transit_switch_port peer name=ts0-p0
> +check_column "" ic-nb:transit_switch_port type name=ts0-p0
> +check_column "chassis" ic-nb:transit_switch_port chassis name=ts0-p0
> +
> +AT_CHECK([ovn-ic-nbctl tsp-set-addr ts0-p0 "aa:bb:cc:aa:bb:cc
> 192.168.11.11"])
> +check_column "aa:bb:cc:aa:bb:cc 192.168.11.11" ic-nb:transit_switch_port
> addresses name=ts0-p0
> +check_column "" ic-nb:transit_switch_port peer name=ts0-p0
> +check_column "" ic-nb:transit_switch_port type name=ts0-p0
> +check_column "chassis" ic-nb:transit_switch_port chassis name=ts0-p0
> +
> +AT_CHECK([ovn-ic-nbctl tsp-set-addr ts0-p0])
> +check_column "" ic-nb:transit_switch_port addresses name=ts0-p0
> +
> +AT_CHECK([ovn-ic-nbctl tsp-del], [1], [],
> +  [ovn-ic-nbctl: 'tsp-del' command requires at least 1 arguments
> +])
> +AT_CHECK([ovn-ic-nbctl tsp-del ts0-p0])
> +AT_CHECK([ovn-ic-nbctl tsp-del ts0-p0], [1], [],
> +  [ovn-ic-nbctl: ts0-p0: switch port name not found
> +])
> +AT_CHECK([ovn-ic-nbctl --if-exists trp-del tr0-p0])
> +
>  OVN_IC_NBCTL_TEST_STOP
>  AT_CLEANUP
>
> diff --git a/tests/ovn-ic.at b/tests/ovn-ic.at
> index 68d78d9e4..a9a1e83b7 100644
> --- a/tests/ovn-ic.at
> +++ b/tests/ovn-ic.at
> @@ -177,6 +177,157 @@ OVN_CLEANUP_IC
>  AT_CLEANUP
>  ])
>
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([ovn-ic -- transit port-bindings deletion upon TS deletion])
> +
> +ovn_init_ic_db
> +net_add n1
> +
> +# 1 GW per AZ
> +for i in 1 2; do
> +    az=az$i
> +    ovn_start $az
> +    sim_add gw-$az
> +    as gw-$az
> +    check ovs-vsctl add-br br-phys
> +    ovn_az_attach $az n1 br-phys 192.168.1.$i
> +    check ovs-vsctl set open . external-ids:ovn-is-interconn=true
> +done
> +
> +ovn_as az1
> +
> +# create transit switch and connect to LR
> +check ovn-ic-nbctl --wait=sb ts-add ts1
> +check ovn-nbctl lr-add lr1
> +check ovn-nbctl lrp-add lr1 lrp1 00:00:00:00:00:01 10.0.0.1/24
> +check ovn-nbctl lrp-set-gateway-chassis lrp1 gw-az1
> +check ovn-ic-nbctl tsp-add ts1 tsp1 type=router chassis=gw-az1 peer=lrp1
> +
> +wait_row_count Datapath_Binding 1 external_ids:interconn-ts=ts1
> +
> +# Sync ic-sb DB to see the TS changes.
> +check ovn-ic-nbctl --wait=sb sync
> +check_column "router" nb:Logical_Switch_Port type name=tsp1
> +check_column "lrp1" nb:Logical_Switch_Port peer name=tsp1
> +check_column "interconn-ts=tsp1 router-port=lrp1 requested-chassis=gw-az1
> requested-tnl-key=1" nb:Logical_Switch_Port options name=tsp1
> +
> +AT_CHECK([ovn-ic-sbctl show | grep -A2 tsp1], [0], [dnl
> +        port tsp1
> +            transit switch: ts1
> +            address: [["00:00:00:00:00:01 10.0.0.1/24"]]
> +])
> +
> +# remove transit switch and check if port_binding is deleted
> +check ovn-ic-nbctl --wait=sb ts-del ts1
> +check_row_count ic-sb:Port_Binding 0 logical_port=tsp1
> +for i in 1 2; do
> +    az=az$i
> +    ovn_as $az
> +    OVN_CLEANUP_SBOX(gw-$az)
> +    OVN_CLEANUP_AZ([$az])
> +done
> +OVN_CLEANUP_IC
> +AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([ovn-ic -- transit switch port type and address updates])
> +
> +ovn_init_ic_db
> +net_add n1
> +
> +# 1 GW per AZ
> +for i in 1 2; do
> +    az=az$i
> +    ovn_start $az
> +    sim_add gw-$az
> +    as gw-$az
> +    check ovs-vsctl add-br br-phys
> +    ovn_az_attach $az n1 br-phys 192.168.1.$i
> +    check ovs-vsctl set open . external-ids:ovn-is-interconn=true
> +done
> +
> +ovn_as az1
> +
> +# create transit switch and connect to LR
> +check ovn-ic-nbctl --wait=sb ts-add ts1
> +check ovn-nbctl lr-add lr1
> +check ovn-nbctl lrp-add lr1 lrp1 00:00:00:00:00:01 10.0.0.1/24
> +check ovn-nbctl lrp-set-gateway-chassis lrp1 gw-az1
> +check ovn-ic-nbctl tsp-add ts1 tsp1 type=router chassis=gw-az1 peer=lrp1
> +
> +wait_row_count Datapath_Binding 1 external_ids:interconn-ts=ts1
> +
> +# Sync ic-sb DB to see the TS changes.
> +check ovn-ic-nbctl --wait=sb sync
> +
> +AT_CHECK([ovn-ic-sbctl show | grep -A2 tsp1], [0], [dnl
> +        port tsp1
> +            transit switch: ts1
> +            address: [["00:00:00:00:00:01 10.0.0.1/24"]]
> +])
> +
> +# ICSB will ignore address updates as long as type=router.
> +check ovn-ic-nbctl tsp-set-addr tsp1 "00:11:22:11:22:33 192.168.10.10"
> +check ovn-ic-nbctl --wait=sb sync
> +AT_CHECK([ovn-ic-sbctl show | grep -A2 tsp1], [0], [dnl
> +        port tsp1
> +            transit switch: ts1
> +            address: [["00:00:00:00:00:01 10.0.0.1/24"]]
> +])
> +check_column "router" nb:Logical_Switch_Port type name=tsp1
> +check_column "lrp1" nb:Logical_Switch_Port peer name=tsp1
> +check_column "interconn-ts=tsp1 router-port=lrp1 requested-chassis=gw-az1
> requested-tnl-key=1" nb:Logical_Switch_Port options name=tsp1
> +
> +# Verify remote AZ sees the port as type "remote"
> +ovn_as az2
> +wait_row_count nb:Logical_Switch_Port 1 name=tsp1
> +check_column "remote" nb:Logical_Switch_Port type name=tsp1
> +ovn_as az1
> +
> +# Clear type and validate that address has been updated.
> +PORT_UUID=$(ovn-ic-nbctl --bare --columns=_uuid find Transit_Switch_Port
> name=tsp1)
> +check ovn-ic-nbctl set Transit_Switch_Port $PORT_UUID 'type=""'
> +check ovn-ic-nbctl --wait=sb sync
> +check_column "" nb:Logical_Switch_Port type name=tsp1
> +check_column "interconn-ts=tsp1 requested-chassis=gw-az1
> requested-tnl-key=1" nb:Logical_Switch_Port options name=tsp1
> +AT_CHECK([ovn-ic-sbctl show | grep -A2 tsp1], [0], [dnl
> +        port tsp1
> +            transit switch: ts1
> +            address: [["00:11:22:11:22:33 192.168.10.10"]]
> +])
> +
> +# Remote AZ should still show type "remote" regardless of owner type
> change
> +ovn_as az2
> +check_column "remote" nb:Logical_Switch_Port type name=tsp1
> +ovn_as az1
> +
> +# Reset type to router and check address.
> +PORT_UUID=$(ovn-ic-nbctl --bare --columns=_uuid find Transit_Switch_Port
> name=tsp1)
> +check ovn-ic-nbctl set Transit_Switch_Port $PORT_UUID 'type="router"'
> +check ovn-ic-nbctl --wait=sb sync
> +check_column "router" nb:Logical_Switch_Port type name=tsp1
> +check_column "lrp1" nb:Logical_Switch_Port peer name=tsp1
> +check_column "interconn-ts=tsp1 router-port=lrp1 requested-chassis=gw-az1
> requested-tnl-key=1" nb:Logical_Switch_Port options name=tsp1
> +AT_CHECK([ovn-ic-sbctl show | grep -A2 tsp1], [0], [dnl
> +        port tsp1
> +            transit switch: ts1
> +            address: [["00:00:00:00:00:01 10.0.0.1/24"]]
> +])
> +
> +# remove transit switch and check if port_binding is deleted
> +check ovn-ic-nbctl --wait=sb ts-del ts1
> +check_row_count ic-sb:Port_Binding 0 logical_port=tsp1
> +for i in 1 2; do
> +    az=az$i
> +    ovn_as $az
> +    OVN_CLEANUP_SBOX(gw-$az)
> +    OVN_CLEANUP_AZ([$az])
> +done
> +OVN_CLEANUP_IC
> +AT_CLEANUP
> +])
> +
>  OVN_FOR_EACH_NORTHD([
>  AT_SETUP([ovn-ic -- route deletion upon TS deletion])
>
> @@ -248,6 +399,170 @@ OVN_CLEANUP_IC
>  AT_CLEANUP
>  ])
>
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([ovn-ic -- no-chassis transit switch port])
> +
> +ovn_init_ic_db
> +net_add n1
> +
> +for i in 1 2; do
> +    az=az$i
> +    ovn_start $az
> +    sim_add gw-$az
> +    as gw-$az
> +    check ovs-vsctl add-br br-phys
> +    ovn_az_attach $az n1 br-phys 192.168.1.$i
> +    check ovs-vsctl set open . external-ids:ovn-is-interconn=true
> +done
> +
> +ovn_as az1
> +
> +check ovn-ic-nbctl --wait=sb ts-add ts1
> +check ovn-ic-nbctl tsp-add ts1 tsp1
> +check ovn-ic-nbctl tsp-set-addr tsp1 "00:00:00:00:00:02 10.0.0.2"
> +check ovn-ic-nbctl --wait=sb sync
> +
> +# Leader AZ should have the NB LSP
> +wait_row_count nb:Logical_Switch_Port 1 name=tsp1
> +check_column "" nb:Logical_Switch_Port type name=tsp1
> +
> +# Non-leader AZ should also have it (found via index lookup)
> +ovn_as az2
> +check ovn-ic-nbctl --wait=sb sync
> +wait_row_count nb:Logical_Switch_Port 1 name=tsp1
> +
> +# Verify ISB port binding exists
> +check_row_count ic-sb:Port_Binding 1 logical_port=tsp1
> +
> +# Cleanup
> +ovn_as az1
> +check ovn-ic-nbctl --wait=sb ts-del ts1
> +check_row_count ic-sb:Port_Binding 0 logical_port=tsp1
> +for i in 1 2; do
> +    az=az$i
> +    ovn_as $az
> +    OVN_CLEANUP_SBOX(gw-$az)
> +    OVN_CLEANUP_AZ([$az])
> +done
> +OVN_CLEANUP_IC
> +AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([ovn-ic -- transit switch port chassis reassignment])
> +
> +ovn_init_ic_db
> +net_add n1
> +
> +for i in 1 2; do
> +    az=az$i
> +    ovn_start $az
> +    sim_add gw-$az
> +    as gw-$az
> +    check ovs-vsctl add-br br-phys
> +    ovn_az_attach $az n1 br-phys 192.168.1.$i
> +    check ovs-vsctl set open . external-ids:ovn-is-interconn=true
> +done
> +
> +ovn_as az1
> +
> +check ovn-ic-nbctl --wait=sb ts-add ts1
> +
> +# Create a non-router TSP on az1
> +check ovn-ic-nbctl tsp-add ts1 tsp1 chassis=gw-az1
> +check ovn-ic-nbctl tsp-set-addr tsp1 "00:00:00:00:00:01 10.0.0.1"
> +check ovn-ic-nbctl --wait=sb sync
> +
> +# az1 owns it
> +wait_row_count nb:Logical_Switch_Port 1 name=tsp1
> +check_column "" nb:Logical_Switch_Port type name=tsp1
> +
> +# az2 sees it as remote
> +ovn_as az2
> +wait_row_count nb:Logical_Switch_Port 1 name=tsp1
> +check_column "remote" nb:Logical_Switch_Port type name=tsp1
> +
> +# Reassign chassis to gw-az2
> +ovn_as az1
> +PORT_UUID=$(ovn-ic-nbctl --bare --columns=_uuid find Transit_Switch_Port
> name=tsp1)
> +check ovn-ic-nbctl set Transit_Switch_Port $PORT_UUID chassis=gw-az2
> +check ovn-ic-nbctl --wait=sb sync
> +
> +# az1 now sees it as remote
> +wait_column "remote" nb:Logical_Switch_Port type name=tsp1
> +
> +# az2 now owns it
> +ovn_as az2
> +check ovn-ic-nbctl --wait=sb sync
> +wait_column "" nb:Logical_Switch_Port type name=tsp1
> +
> +# Change type to router with peer on az2
> +check ovn-nbctl lr-add lr1
> +check ovn-nbctl lrp-add lr1 lrp1 00:00:00:00:00:02 10.0.0.2/24
> +check ovn-nbctl lrp-set-gateway-chassis lrp1 gw-az2
> +ovn_as az1
> +check ovn-ic-nbctl set Transit_Switch_Port $PORT_UUID 'type=router'
> 'peer=lrp1'
> +check ovn-ic-nbctl --wait=sb sync
> +
> +# az2 owns it with type=router, address derived from LRP
> +ovn_as az2
> +check ovn-ic-nbctl --wait=sb sync
> +wait_column "router" nb:Logical_Switch_Port type name=tsp1
> +check_column "lrp1" nb:Logical_Switch_Port peer name=tsp1
> +AT_CHECK([ovn-ic-sbctl show | grep -A2 tsp1], [0], [dnl
> +        port tsp1
> +            transit switch: ts1
> +            address: [["00:00:00:00:00:02 10.0.0.2/24"]]
> +])
> +# az1 still sees it as remote
> +ovn_as az1
> +check_column "remote" nb:Logical_Switch_Port type name=tsp1
> +
> +# Clear type back to "" while chassis stays gw-az2
> +check ovn-ic-nbctl set Transit_Switch_Port $PORT_UUID 'type=""' 'peer=""'
> +check ovn-ic-nbctl --wait=sb sync
> +
> +# az2 owns it with empty type, address reverts to tsp-set-addr value
> +ovn_as az2
> +check ovn-ic-nbctl --wait=sb sync
> +wait_column "" nb:Logical_Switch_Port type name=tsp1
> +
> +AT_CHECK([ovn-ic-sbctl show | grep -A2 tsp1], [0], [dnl
> +        port tsp1
> +            transit switch: ts1
> +            address: [["00:00:00:00:00:01 10.0.0.1"]]
> +])
> +
> +# az1 still sees it as remote
> +ovn_as az1
> +check_column "remote" nb:Logical_Switch_Port type name=tsp1
> +
> +# Reassign back to gw-az1
> +check ovn-ic-nbctl set Transit_Switch_Port $PORT_UUID chassis=gw-az1
> +check ovn-ic-nbctl --wait=sb sync
> +
> +# az1 now owns it
> +wait_column "" nb:Logical_Switch_Port type name=tsp1
> +
> +# az2 sees it as remote
> +ovn_as az2
> +check ovn-ic-nbctl --wait=sb sync
> +wait_column "remote" nb:Logical_Switch_Port type name=tsp1
> +
> +# Cleanup
> +ovn_as az1
> +check ovn-ic-nbctl --wait=sb ts-del ts1
> +check_row_count ic-sb:Port_Binding 0 logical_port=tsp1
> +for i in 1 2; do
> +    az=az$i
> +    ovn_as $az
> +    OVN_CLEANUP_SBOX(gw-$az)
> +    OVN_CLEANUP_AZ([$az])
> +done
> +OVN_CLEANUP_IC
> +AT_CLEANUP
> +])
> +
>  OVN_FOR_EACH_NORTHD([
>  AT_SETUP([ovn-ic -- duplicate NB route adv/learn])
>
> diff --git a/utilities/ovn-ic-nbctl.8.xml b/utilities/ovn-ic-nbctl.8.xml
> index 633863294..308debd8e 100644
> --- a/utilities/ovn-ic-nbctl.8.xml
> +++ b/utilities/ovn-ic-nbctl.8.xml
> @@ -50,6 +50,62 @@
>        <dd>
>          Lists all existing switches on standard output, one per line.
>        </dd>
> +
> +      <dt>[<code>--may-exist</code>] <code>tsp-add</code>
> <var>switch</var>
> +      <var>port</var> [<var>column</var>[<code>:</code><var>key</var>]
> +      <code>=</code><var>value</var>]...</dt>
> +      <dd>
> +        <p>
> +          Creates a new transit switch port named <var>port</var>
> +          on <var>switch</var>.
> +        </p>
> +
> +        <p>
> +          Transit switch port names must be unique. Adding a duplicated
> name
> +          results in an error. With <code>--may-exist</code>, adding a
> +          duplicate name succeeds but does not create a new transit switch
> +          port.
> +        </p>
> +      </dd>
> +
> +      <dt>[<code>--if-exists</code>] <code>tsp-del</code>
> <var>port</var></dt>
> +      <dd>
> +        Deletes <var>port</var>.  It is an error if <var>port</var> does
> +        not exist, unless <code>--if-exists</code> is specified.
> +      </dd>
> +
> +      <dt><code>tsp-set-addr</code> <var>port</var>
> +        [<var>address</var>]...</dt>
> +      <dd>
> +        <p>
> +          Sets the addresses associated with <var>port</var> to
> +          <var>address</var>.  Each <var>address</var> should be one of
> the
> +          following:
> +        </p>
> +
> +        <dl>
> +          <dt>an Ethernet address, optionally followed by a space and one
> or
> +          more IP addresses</dt>
> +          <dd>
> +            OVN delivers packets for the Ethernet address to this port.
> +          </dd>
> +
> +          <dt><code>router</code></dt>
> +          <dd>
> +            Accepted only when the <code>type</code> of the transit switch
> +            port is <code>router</code>.  This indicates that the
> Ethernet,
> +            IPv4, and IPv6 addresses for this transit switch port should
> be
> +            obtained from the connected logical router port, as specified
> by
> +            peer <ref column="peer"
> +            table="Transit_Switch_Port" db="OVN_IC_Northbound"/> column.
> +          </dd>
> +        </dl>
> +
> +        <p>
> +          Multiple addresses may be set.  If no <var>address</var>
> argument is
> +          given, <var>port</var> will have no addresses associated with
> it.
> +        </p>
> +      </dd>
>      </dl>
>
>      <h1>Database Commands</h1>
> diff --git a/utilities/ovn-ic-nbctl.c b/utilities/ovn-ic-nbctl.c
> index 50e975283..4bf922212 100644
> --- a/utilities/ovn-ic-nbctl.c
> +++ b/utilities/ovn-ic-nbctl.c
> @@ -336,6 +336,11 @@ Transit switch commands:\n\
>    ts-add SWITCH              create a transit switch named SWITCH\n\
>    ts-del SWITCH              delete SWITCH\n\
>    ts-list                    print all transit switches\n\
> +  tsp-add SWITCH PORT [COLUMN[:KEY]=VALUE]...\n\
> +                             add a transit switch PORT\n\
> +  tsp-set-addr PORT ADDRESS...\n\
> +                             set a transit switch PORT address\n\
> +  tsp-del PORT               delete a transit switch PORT\n\
>  \n\
>  Transit router commands:\n\
>    tr-add ROUTER              create a transit router named ROUTER\n\
> @@ -400,6 +405,7 @@ struct ic_nbctl_context {
>       * ic_nbctl_context_invalidate_cache() or manually update the cache to
>       * maintain its correctness. */
>      bool cache_valid;
> +    struct shash tsp_to_ts_map;
>  };
>
>  static struct cmd_show_table cmd_show_tables[] = {
> @@ -617,6 +623,38 @@ trp_by_name_or_uuid(struct ctl_context *ctx, const
> char *id, bool must_exist,
>      return NULL;
>  }
>
> +static char *
> +tsp_by_name_or_uuid(struct ctl_context *ctx, const char *id, bool
> must_exist,
> +                    const struct icnbrec_transit_switch_port **tsp_p)
> +{
> +    const struct icnbrec_transit_switch_port *tsp = NULL;
> +    *tsp_p = NULL;
> +    struct uuid tsp_uuid;
> +    bool is_uuid = uuid_from_string(&tsp_uuid, id);
> +    if (is_uuid) {
> +        tsp = icnbrec_transit_switch_port_get_for_uuid(ctx->idl,
> &tsp_uuid);
> +    }
> +
> +    if (!tsp) {
> +        const struct icnbrec_transit_switch_port *iter;
> +
> +        ICNBREC_TRANSIT_SWITCH_PORT_FOR_EACH (iter, ctx->idl) {
> +            if (!strcmp(iter->name, id)) {
> +                tsp = iter;
> +                break;
> +            }
> +        }
> +    }
> +
> +    if (!tsp && must_exist) {
> +        return xasprintf("%s: switch port %s not found", id,
> +                         is_uuid ? "UUID" : "name");
> +    }
> +
> +    *tsp_p = tsp;
> +    return NULL;
> +}
> +
>  static void
>  ic_nbctl_tr_del(struct ctl_context *ctx)
>  {
> @@ -664,6 +702,74 @@ ic_nbctl_trp_del(struct ctl_context *ctx)
>      icnbrec_transit_router_port_delete(trp);
>  }
>
> +static struct ic_nbctl_context *
> +ic_nbctl_context_get(struct ctl_context *base)
> +{
> +    struct ic_nbctl_context *icnbctx
> +        = CONTAINER_OF(base, struct ic_nbctl_context, base);
> +    if (icnbctx->cache_valid) {
> +        return icnbctx;
> +    }
> +
> +    const struct icnbrec_transit_switch *ts;
> +    ICNBREC_TRANSIT_SWITCH_FOR_EACH (ts, base->idl) {
> +        for (size_t i = 0; i < ts->n_ports; i++) {
> +            shash_add_once(&icnbctx->tsp_to_ts_map, ts->ports[i]->name,
> ts);
> +        }
> +    }
> +
> +    icnbctx->cache_valid = true;
> +    return icnbctx;
> +}
> +
> +/* Returns the transit switch that contains 'tsp'. */
> +static char * OVS_WARN_UNUSED_RESULT
> +tsp_to_ts(struct ctl_context *ctx,
> +          const struct icnbrec_transit_switch_port *tsp,
> +          const struct icnbrec_transit_switch **ts_p)
> +{
> +    struct ic_nbctl_context *icnbctx = ic_nbctl_context_get(ctx);
> +    const struct icnbrec_transit_switch *ts;
> +    *ts_p = NULL;
> +
> +    ts = shash_find_data(&icnbctx->tsp_to_ts_map, tsp->name);
> +    if (ts) {
> +        *ts_p = ts;
> +        return NULL;
> +    }
> +    /* Can't happen because of the database schema */
> +    return xasprintf("transit port %s is not part of any transit switch",
> +                     tsp->name);
> +}
> +
> +static void
> +ic_nbctl_tsp_del(struct ctl_context *ctx)
> +{
> +    bool must_exist = !shash_find(&ctx->options, "--if-exists");
> +    const char *tsp_name = ctx->argv[1];
> +    const struct icnbrec_transit_switch_port *tsp = NULL;
> +
> +    char *error = tsp_by_name_or_uuid(ctx, tsp_name, must_exist, &tsp);
> +    if (error) {
> +        ctx->error = error;
> +        return;
> +    }
> +
> +    if (!tsp) {
> +        return;
> +    }
> +
> +    const struct icnbrec_transit_switch *ts = NULL;
> +    error = tsp_to_ts(ctx, tsp, &ts);
> +    if (error) {
> +        ctx->error = error;
> +        return;
> +    }
> +
> +    icnbrec_transit_switch_update_ports_delvalue(ts, tsp);
> +    icnbrec_transit_switch_port_delete(tsp);
> +}
> +
>  static void
>  ic_nbctl_tr_list(struct ctl_context *ctx)
>  {
> @@ -802,6 +908,139 @@ ic_nbctl_trp_add(struct ctl_context *ctx)
>      icnbrec_transit_router_update_ports_addvalue(tr, trp);
>  }
>
> +static void
> +ic_nbctl_tsp_add(struct ctl_context *ctx)
> +{
> +    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
> +    const char *ts_name = ctx->argv[1];
> +    const char *tsp_name = ctx->argv[2];
> +    const struct icnbrec_transit_switch *ts;
> +
> +    ctx->error = ts_by_name_or_uuid(ctx, ts_name, true, &ts);
> +    if (ctx->error) {
> +        return;
> +    }
> +
> +    const struct icnbrec_transit_switch_port *tsp;
> +    ctx->error = tsp_by_name_or_uuid(ctx, tsp_name, false, &tsp);
> +    if (ctx->error) {
> +        return;
> +    }
> +
> +    if (tsp) {
> +        if (!may_exist) {
> +            ctl_error(ctx, "%s: a port with this name already exists",
> +                      tsp_name);
> +            return;
> +        }
> +    } else {
> +        tsp = icnbrec_transit_switch_port_insert(ctx->txn);
> +        icnbrec_transit_switch_port_set_name(tsp, tsp_name);
> +    }
> +
> +    int n_settings = ctx->argc - 3;
> +    char **settings = (char **) &ctx->argv[3];
> +    for (size_t i = 0; i < n_settings; i++) {
> +        ctx->error = ctl_set_column("Transit_Switch_Port", &tsp->header_,
> +                                    settings[i], ctx->symtab);
> +        if (ctx->error) {
> +            return;
> +        }
> +    }
> +
> +    icnbrec_transit_switch_update_ports_addvalue(ts, tsp);
> +}
> +
> +static char *
> +tsp_contains_duplicates(const struct icnbrec_transit_switch *ts,
> +                        const struct icnbrec_transit_switch_port *tsp,
> +                        const char *address)
> +{
> +    char *sub_error = NULL;
> +    struct lport_addresses laddrs;
> +    if (!extract_lsp_addresses(address, &laddrs)) {
> +        return NULL;
> +    }
> +
> +    for (size_t i = 0; i < ts->n_ports; i++) {
> +        struct icnbrec_transit_switch_port *tsp_test = ts->ports[i];
> +        if (tsp_test == tsp) {
> +            continue;
> +        }
> +
> +        for (size_t j = 0; j < tsp_test->n_addresses; j++) {
> +            struct lport_addresses laddrs_test;
> +            char *addr = tsp_test->addresses[j];
> +            if (extract_lsp_addresses(addr, &laddrs_test)) {
> +                bool has_duplicate =
> +                    port_contains_duplicate_ip(&laddrs, &laddrs_test,
> +                                             tsp_test->name, &sub_error);
> +                destroy_lport_addresses(&laddrs_test);
> +                if (has_duplicate) {
> +                    goto err_out;
> +                }
> +            }
> +        }
> +    }
> +
> +err_out: ;
> +    char *error = NULL;
> +    if (sub_error) {
> +        error = xasprintf("Error on switch %s: %s", ts->name, sub_error);
> +        free(sub_error);
> +    }
> +    destroy_lport_addresses(&laddrs);
> +    return error;
> +}
> +
> +static void
> +ic_nbctl_tsp_set_addr(struct ctl_context *ctx)
> +{
> +    const char *tsp_name = ctx->argv[1];
> +
> +    const struct icnbrec_transit_switch_port *tsp;
> +    ctx->error = tsp_by_name_or_uuid(ctx, tsp_name, true, &tsp);
> +    if (ctx->error) {
> +        return;
> +    }
> +
> +    const struct icnbrec_transit_switch *ts = NULL;
> +    char *error = tsp_to_ts(ctx, tsp, &ts);
> +    if (error) {
> +        ctx->error = error;
> +        return;
> +    }
> +
> +    for (size_t i = 2; i < ctx->argc; i++) {
> +        char ipv6_s[IPV6_SCAN_LEN + 1];
> +        struct eth_addr ea;
> +        ovs_be32 ip;
> +
> +        if (strcmp(ctx->argv[i], "unknown") && strcmp(ctx->argv[i],
> "dynamic")
> +            && strcmp(ctx->argv[i], "router")
> +            && !ovs_scan(ctx->argv[i], ETH_ADDR_SCAN_FMT,
> +                         ETH_ADDR_SCAN_ARGS(ea))
> +            && !ovs_scan(ctx->argv[i], "dynamic "IPV6_SCAN_FMT, ipv6_s)
> +            && !ovs_scan(ctx->argv[i], "dynamic "IP_SCAN_FMT,
> +                         IP_SCAN_ARGS(&ip))) {
> +            ctl_error(ctx, "%s: Invalid address format. See ovn-nb(5). "
> +                      "Hint: An Ethernet address must be "
> +                      "listed before an IP address, together as a single "
> +                      "argument.", ctx->argv[i]);
> +            return;
> +        }
> +
> +        ctx->error = tsp_contains_duplicates(ts, tsp, ctx->argv[i]);
> +        if (ctx->error) {
> +            return;
> +        }
> +    }
> +
> +    icnbrec_transit_switch_port_set_addresses(tsp,
> +                                              (const char **) ctx->argv +
> 2,
> +                                              ctx->argc - 2);
> +}
> +
>  static void
>  verify_connections(struct ctl_context *ctx)
>  {
> @@ -1036,6 +1275,7 @@ ic_nbctl_context_init(struct ic_nbctl_context
> *ic_nbctl_ctx,
>      ctl_context_init(&ic_nbctl_ctx->base, command, idl, txn, symtab,
>                       NULL);
>      ic_nbctl_ctx->cache_valid = false;
> +    shash_init(&ic_nbctl_ctx->tsp_to_ts_map);
>  }
>
>  static void
> @@ -1077,6 +1317,7 @@ ic_nbctl_context_done(struct ic_nbctl_context
> *ic_nbctl_ctx,
>                     struct ctl_command *command)
>  {
>      ctl_context_done(&ic_nbctl_ctx->base, command);
> +    shash_destroy(&ic_nbctl_ctx->tsp_to_ts_map);
>  }
>
>  static void
> @@ -1317,7 +1558,12 @@ static const struct ctl_command_syntax
> ic_nbctl_commands[] = {
>      { "ts-add", 1, 1, "SWITCH", NULL, ic_nbctl_ts_add, NULL,
> "--may-exist", RW },
>      { "ts-del", 1, 1, "SWITCH", NULL, ic_nbctl_ts_del, NULL,
> "--if-exists", RW },
>      { "ts-list", 0, 0, "", NULL, ic_nbctl_ts_list, NULL, "", RO },
> -
> +    { "tsp-add", 2, INT_MAX, "SWITCH PORT [COLUMN[:KEY]=VALUE]...",
> +        NULL, ic_nbctl_tsp_add, NULL, "--may-exist", RW },
> +    { "tsp-set-addr", 1, INT_MAX, "PORT ADDRESS...",
> +        NULL, ic_nbctl_tsp_set_addr, NULL, "", RW },
> +    { "tsp-del", 1, 1, "PORT", NULL, ic_nbctl_tsp_del, NULL,
> "--if-exists",
> +        RW },
>      /* transit router commands. */
>      { "tr-add", 1, 1, "ROUTER", NULL, ic_nbctl_tr_add, NULL,
> "--may-exist",
>          RW },
> diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
> index fe71b06f3..fc0a60202 100644
> --- a/utilities/ovn-nbctl.c
> +++ b/utilities/ovn-nbctl.c
> @@ -1514,50 +1514,6 @@ nbctl_pre_lsp_set_addresses(struct ctl_context *ctx)
>
> &nbrec_logical_switch_port_col_dynamic_addresses);
>  }
>
> -static bool
> -lsp_contains_duplicate_ip(struct lport_addresses *laddrs1,
> -                          struct lport_addresses *laddrs2,
> -                          const struct nbrec_logical_switch_port
> *lsp_test,
> -                          char **error_str)
> -{
> -    for (size_t i = 0; i < laddrs1->n_ipv4_addrs; i++) {
> -        for (size_t j = 0; j < laddrs2->n_ipv4_addrs; j++) {
> -            if (laddrs1->ipv4_addrs[i].addr ==
> laddrs2->ipv4_addrs[j].addr) {
> -                if (error_str) {
> -                    *error_str = xasprintf("duplicate IPv4 address '%s' "
> -                                           "found on logical switch "
> -                                           "port '%s'",
> -                                           laddrs1->ipv4_addrs[i].addr_s,
> -                                           lsp_test->name);
> -                }
> -                return true;
> -            }
> -        }
> -    }
> -
> -    for (size_t i = 0; i < laddrs1->n_ipv6_addrs; i++) {
> -        for (size_t j = 0; j < laddrs2->n_ipv6_addrs; j++) {
> -            if (IN6_ARE_ADDR_EQUAL(&laddrs1->ipv6_addrs[i].addr,
> -                                   &laddrs2->ipv6_addrs[j].addr)) {
> -                if (error_str) {
> -                    *error_str = xasprintf("duplicate IPv6 address "
> -                                           "'%s' found on logical "
> -                                           "switch port '%s'",
> -                                           laddrs1->ipv6_addrs[i].addr_s,
> -                                           lsp_test->name);
> -                }
> -                return true;
> -            }
> -        }
> -    }
> -
> -    if (error_str) {
> -        *error_str = NULL;
> -    }
> -
> -    return false;
> -}
> -
>  static char *
>  lsp_contains_duplicates(const struct nbrec_logical_switch *ls,
>                          const struct nbrec_logical_switch_port *lsp,
> @@ -1582,8 +1538,8 @@ lsp_contains_duplicates(const struct
> nbrec_logical_switch *ls,
>              }
>              if (extract_lsp_addresses(addr, &laddrs_test)) {
>                  bool has_duplicate =
> -                    lsp_contains_duplicate_ip(&laddrs, &laddrs_test,
> -                                              lsp_test, &sub_error);
> +                    port_contains_duplicate_ip(&laddrs, &laddrs_test,
> +                                             lsp_test->name, &sub_error);
>                  destroy_lport_addresses(&laddrs_test);
>                  if (has_duplicate) {
>                      goto err_out;
> @@ -8885,8 +8841,8 @@ lsp_health_check_parse_target_address(
>              goto cleanup;
>          }
>
> -        if (lsp_contains_duplicate_ip(&target_address,
> -                                      &lsp_address, lsp, NULL)) {
> +        if (port_contains_duplicate_ip(&target_address,
> +                                      &lsp_address, lsp->name, NULL)) {
>              ip_found_on_port = true;
>          }
>
> --
> 2.54.0
>
>
The testing additions look good.

Regards,
Ales
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to