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]>
---
 NEWS                         |   4 +
 ic/ovn-ic.c                  | 280 ++++++++++++++++++++++++++---------
 lib/ovn-util.c               |  44 ++++++
 lib/ovn-util.h               |   4 +
 ovn-ic-nb.ovsschema          |  23 ++-
 ovn-ic-nb.xml                |  41 +++++
 tests/multinode.at           |  14 +-
 tests/ovn-ic-nbctl.at        |  47 ++++++
 tests/ovn-ic.at              | 129 ++++++++++++++++
 utilities/ovn-ic-nbctl.8.xml |  50 +++++++
 utilities/ovn-ic-nbctl.c     | 247 ++++++++++++++++++++++++++++++
 utilities/ovn-nbctl.c        |  52 +------
 12 files changed, 805 insertions(+), 130 deletions(-)

diff --git a/NEWS b/NEWS
index 748ae30eb..115bda281 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,9 @@
 Post v26.03.0
 -------------
+   - Added Transit Switch port support:
+     * Support the creation of Transit Switch Ports.
+     * Added new ovn-ic-nbctl 'tsp-add', 'tsp-del' and 'tsp-set-addr' commands
+       to manage Transit Switch Ports.
    - 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..09199b926 100644
--- a/ic/ovn-ic.c
+++ b/ic/ovn-ic.c
@@ -733,6 +733,18 @@ 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;
+    } else {
+        return NULL;
+    }
+}
+
 static const struct sbrec_chassis *
 find_sb_chassis(struct ic_context *ctx, const char *name)
 {
@@ -817,52 +829,81 @@ 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, const char *address)
 {
-    /* Sync address from NB to ISB */
-    const char *address = get_lp_address_for_sb_pb(ctx, 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]) {
+        if (isb_pb->address && 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 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);
-        }
-    } 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]) {
+    /* 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 external_ids:router-id to ISB */
-    update_isb_pb_external_ids(ctx, sb_pb, isb_pb);
+    /* Sync external_ids:router-id to ISB (requires sb_pb). */
+    if (sb_pb) {
+        update_isb_pb_external_ids(ctx, sb_pb, isb_pb);
+    }
 
-    /* Sync back tunnel key from ISB to NB */
+    /* Sync tunnel key from ISB to NB. */
     sync_lsp_tnl_key(lsp, isb_pb->tunnel_key);
+
+    /* Sync type and peer from TSP to NB. */
+    if (tsp) {
+        if (strcmp(lsp->type, tsp->type)) {
+            nbrec_logical_switch_port_set_type(lsp, tsp->type);
+        }
+        if (tsp->peer[0] && (!lsp->peer || strcmp(lsp->peer, tsp->peer))) {
+            nbrec_logical_switch_port_set_peer(lsp, tsp->peer);
+        }
+    }
 }
 
 /* For each remote port:
@@ -1035,9 +1076,8 @@ trp_is_remote(struct ic_context *ctx, const char 
*chassis_name)
             find_sb_chassis(ctx, chassis_name);
         if (chassis) {
             return smap_get_bool(&chassis->other_config, "is-remote", false);
-        } else {
-            return true;
         }
+        return true;
     }
 
     return false;
@@ -1057,28 +1097,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");
 
-    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);
+    struct nbrec_logical_switch_port *lsp =
+        nbrec_logical_switch_port_insert(ctx->ovnnb_txn);
+    nbrec_logical_switch_port_set_name(lsp, tsp->name);
+
+    nbrec_logical_switch_port_update_options_setkey(lsp, "interconn-ts",
+                                                    tsp->name);
+    nbrec_logical_switch_port_set_type(lsp, tsp->type);
+    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 +1168,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 +1175,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 +1205,73 @@ 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];
+            /* Only create ICSB if
+             *  chassis is not set and all ports should be local
+             *  or
+             *  chassis is set and local to this instance's AZ
+             *  */
+            if (!tsp->chassis[0] || !trp_is_remote(ctx, tsp->chassis)) {
+                isb_pb = shash_find_and_delete(&local_pbs, tsp->name);
+                if (!isb_pb) {
+                    isb_pb = create_isb_pb(ctx, tsp->name, ctx->runned_az,
+                                           ts->name, &ts->header_.uuid,
+                                           "transit-switch-port", &pb_tnlids);
+                    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 char *address = get_ts_port_address(ctx, tsp, NULL);
+                const struct sbrec_port_binding *sb_pb =
+                    find_lsp_in_sb(ctx, lsp);
+                /* Sync address to NB (skip for router type, which
+                 * uses "router" as its NB address). */
+                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_local_port(ctx, tsp, isb_pb, lsp, sb_pb, address);
+            } else {
+                /* Remote port sync */
+                isb_pb = shash_find_and_delete(&remote_pbs, tsp->name);
+                if (isb_pb) {
+                    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;
+                    sb_pb = find_lsp_in_sb(ctx, lsp);
+                    if (sb_pb) {
+                        sync_remote_port(ctx, isb_pb, lsp, sb_pb);
+                    }
+                }
+            }
+        }
+
+        /* 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")) {
@@ -1161,20 +1285,9 @@ port_binding_run(struct ic_context *ctx)
                     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);
                 }
+                const char *address = get_ts_port_address(ctx, NULL, sb_pb);
+                sync_local_port(ctx, NULL, isb_pb, lsp, sb_pb, address);
             } else if (!strcmp(lsp->type, "remote")) {
                 /* The port is remote. */
                 isb_pb = shash_find_and_delete(&remote_pbs, lsp->name);
@@ -1186,13 +1299,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 +1331,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) {
@@ -1275,7 +1405,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 +2759,22 @@ 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 || !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 +3711,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,
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..ae7f38377 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": "1916436818 6015",
     "tables": {
         "IC_NB_Global": {
             "columns": {
@@ -24,9 +24,28 @@
                              "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"}}},
+            "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..494cfc02b 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,43 @@
     </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"/>
+    </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..8db2fc54f 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 \
@@ -2434,6 +2434,9 @@ check m_central_as ovn-ic-nbctl trp-add tr tr-gw2 \
     chassis=ovn-chassis-2
 check m_central_as ovn-ic-nbctl --wait=sb sync
 
+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
+
 for i in 1 2; do
     chassis="ovn-chassis-$i"
 
@@ -2446,14 +2449,7 @@ 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
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..841aaf226 100644
--- a/tests/ovn-ic.at
+++ b/tests/ovn-ic.at
@@ -177,6 +177,135 @@ 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
+
+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"]]
+])
+
+# 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
+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"]]
+])
+
+# 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
+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])
 
diff --git a/utilities/ovn-ic-nbctl.8.xml b/utilities/ovn-ic-nbctl.8.xml
index 633863294..160b99fc5 100644
--- a/utilities/ovn-ic-nbctl.8.xml
+++ b/utilities/ovn-ic-nbctl.8.xml
@@ -50,6 +50,56 @@
       <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 ports names must be unique. Adding a duplicated name 
results
+          in 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 logical switch
+            port is <code>router</code>.  This indicates that the Ethernet,
+            IPv4, and IPv6 addresses for this logical 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..a445ccc88 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 logical switch that contains 'lsp'. */
+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;
+    }
+
+    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,13 @@ 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