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]>
---

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);
+            } 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);
+    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);
+                }
+
+                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) {
+            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

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

Reply via email to