Transit_Router_Port entries with an empty "chassis" column are
now instantiated as distributed router ports in every availability
zone. A single ISB port binding, created by the AZ leader, is
shared across all AZs so that they agree on the tunnel key.

This is useful for connecting a transit router to a transit switch
without pinning the port to a specific chassis.

Assisted-by: Claude Opus 5, Claude Code
Reported-at: https://redhat.atlassian.net/browse/FDP-2881
Signed-off-by: Mairtin O'Loingsigh <[email protected]>
---
 NEWS                     |  5 +++
 ic/ovn-ic.c              | 48 ++++++++++++++++++++--
 ovn-ic-nb.xml            |  6 ++-
 tests/ovn-ic-nbctl.at    |  8 +++-
 tests/ovn-ic.at          | 86 ++++++++++++++++++++++++++++++++++++++++
 utilities/ovn-ic-nbctl.c |  2 +-
 6 files changed, 149 insertions(+), 6 deletions(-)

diff --git a/NEWS b/NEWS
index 7f94d0b14..7e320d361 100644
--- a/NEWS
+++ b/NEWS
@@ -10,6 +10,11 @@ Post v26.09.0
      (e.g. via the ovs-delete-transient-ports.service on RHEL/Fedora) will
      automatically remove stale tunnel ports on reboot, preventing them from
      interfering with BFD and HA failover after a gateway chassis reboot.
+   - Transit_Router_Port entries with an empty "chassis" column are now
+     instantiated as distributed router ports in every availability zone,
+     instead of being ignored.  Such ports are useful to connect a transit
+     router to a transit switch.  Accordingly, "ovn-ic-nbctl trp-add" no
+     longer requires a trailing network or chassis argument.
    - Dynamic Routing:
      * Remove the "other_config:dynamic-routing-arp-prefer-local"
        option from Logical Switches.  EVPN-learned MAC bindings are
diff --git a/ic/ovn-ic.c b/ic/ovn-ic.c
index 2a2664ea3..3bbc8e1c7 100644
--- a/ic/ovn-ic.c
+++ b/ic/ovn-ic.c
@@ -1024,6 +1024,21 @@ sync_tsp_pb(const struct icnbrec_transit_switch_port 
*tsp,
     }
 }
 
+/* Sync a transit router port's fields from ICNB->ISB. */
+static void
+sync_trp_pb(const struct icnbrec_transit_router_port *trp,
+            const struct icsbrec_port_binding *isb_pb)
+{
+    if (!isb_pb) {
+        return;
+    }
+
+    /* Sync address to ISB. */
+    if (strcmp(trp->mac, isb_pb->address)) {
+        icsbrec_port_binding_set_address(isb_pb, trp->mac);
+    }
+}
+
 /* For each local port:
  *   - Sync from NB to ISB.
  *   - Sync gateway from SB to ISB.
@@ -1229,7 +1244,7 @@ sync_router_port(const struct icsbrec_port_binding 
*isb_pb,
             nbrec_logical_router_port_update_options_setkey(
                 lrp, "requested-chassis", trp->chassis);
         }
-    } else {
+    } else if (smap_get(&lrp->options, "requested-chassis")) {
         nbrec_logical_router_port_update_options_delkey(
             lrp, "requested-chassis");
     }
@@ -1630,7 +1645,27 @@ port_binding_run(struct ic_context *ctx)
         for (size_t i = 0; i < tr->n_ports; i++) {
             const struct icnbrec_transit_router_port *trp = tr->ports[i];
 
-            if (chassis_is_remote(ctx, trp->chassis)) {
+            if (!trp->chassis[0]) {
+                /* The port is not bound to any chassis, so it is local to
+                 * every AZ.  A single ISB port binding, created by the AZ
+                 * leader, is shared by all the AZs, which guarantees that
+                 * they all use the same tunnel key for the port. */
+                isb_pb = shash_find_and_delete(&local_pbs, trp->name);
+                if (!isb_pb) {
+                    isb_pb = shash_find_and_delete(&remote_pbs, trp->name);
+                }
+
+                if (ctx->ovnisb_txn && is_az_leader(ctx->ovnisb_txn)) {
+                    if (!isb_pb) {
+                        isb_pb = create_isb_pb(ctx->ovnisb_txn, trp->name,
+                                               ctx->runned_az,
+                                               tr->name, &tr->header_.uuid,
+                                               "transit-router-port",
+                                               &pb_tnlids);
+                    }
+                    sync_trp_pb(trp, isb_pb);
+                }
+            } else if (chassis_is_remote(ctx, trp->chassis)) {
                 isb_pb = shash_find_and_delete(&remote_pbs, trp->name);
             } else {
                 isb_pb = shash_find_and_delete(&local_pbs, trp->name);
@@ -1639,8 +1674,8 @@ port_binding_run(struct ic_context *ctx)
                                            ctx->runned_az,
                                            tr->name, &tr->header_.uuid,
                                            "transit-router-port", &pb_tnlids);
-                    icsbrec_port_binding_set_address(isb_pb, trp->mac);
                 }
+                sync_trp_pb(trp, isb_pb);
             }
 
             /* Don't allow remote ports to create NB LRP until ICSB entry is
@@ -1661,6 +1696,13 @@ port_binding_run(struct ic_context *ctx)
             nbrec_logical_router_update_ports_delvalue(lr, node->data);
         }
 
+        /* Delete extra port-binding from ISB.  Any local port binding that
+         * is not claimed by a transit router port above belongs to a port
+         * that has been removed, or that is no longer local to this AZ. */
+        SHASH_FOR_EACH (node, &local_pbs) {
+            icsbrec_port_binding_delete(node->data);
+        }
+
         shash_destroy(&nb_ports);
         shash_destroy(&local_pbs);
         shash_destroy(&remote_pbs);
diff --git a/ovn-ic-nb.xml b/ovn-ic-nb.xml
index f6110324c..53b9f88d1 100644
--- a/ovn-ic-nb.xml
+++ b/ovn-ic-nb.xml
@@ -165,7 +165,11 @@
     </column>
 
     <column name="chassis">
-      The chassis this router port should be bound to.
+      The chassis this router port should be bound to.  If empty, the port
+      is local to all availability zones, that is, every availability zone
+      instantiates it as a distributed router port of its own copy of the
+      transit router.  This is useful for ports that connect a transit
+      router to a transit switch.
     </column>
 
     <column name="tr_uuid">
diff --git a/tests/ovn-ic-nbctl.at b/tests/ovn-ic-nbctl.at
index 4615f73f4..ce10e0d78 100644
--- a/tests/ovn-ic-nbctl.at
+++ b/tests/ovn-ic-nbctl.at
@@ -172,9 +172,15 @@ AT_CHECK([ovn-ic-nbctl trp-add tr0 tr0-p0 
00:11:22:11:22:33 192.168.10.10/24 cha
 
 AT_CHECK([ovn-ic-nbctl tr-add tr0])
 AT_CHECK([ovn-ic-nbctl trp-add tr0 tr0-p0], [1], [],
-  [ovn-ic-nbctl: 'trp-add' command requires at least 5 arguments
+  [ovn-ic-nbctl: 'trp-add' command requires at least 3 arguments
 ])
 
+dnl Both the networks and the chassis are optional.
+AT_CHECK([ovn-ic-nbctl trp-add tr0 tr0-p1 00:11:22:11:22:44 192.168.20.10/24])
+AT_CHECK([ovn-ic-nbctl --bare --columns=chassis list Transit_Router_Port 
tr0-p1], [0], [
+])
+AT_CHECK([ovn-ic-nbctl trp-del tr0-p1])
+
 AT_CHECK([ovn-ic-nbctl trp-add tr0 tr0-p0 00:11:22:11:22:33 192.168.10.10/24 
chassis=chassis])
 AT_CHECK([ovn-ic-nbctl trp-add tr0 tr0-p0 00:11:22:11:22:33 192.168.10.10/24 
chassis=chassis], [1], [],
   [ovn-ic-nbctl: tr0-p0: a port with this name already exists
diff --git a/tests/ovn-ic.at b/tests/ovn-ic.at
index 638d96f79..001f729dc 100644
--- a/tests/ovn-ic.at
+++ b/tests/ovn-ic.at
@@ -5549,6 +5549,92 @@ OVN_CLEANUP_IC([az1], [az2])
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn-ic -- Add transit router port local in all AZs])
+
+ovn_init_ic_db
+net_add n1
+net_add n2
+ovn_start az1
+ovn_start az2
+
+OVS_WAIT_FOR_OUTPUT([ovn-ic-sbctl show], [0], [dnl
+availability-zone az1
+availability-zone az2
+])
+check ovn-ic-nbctl --wait=sb sync
+
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_az_attach az1 n1 br-phys 192.168.0.1
+ovs-vsctl set open . external-ids:ovn-is-interconn=true
+wait_row_count Chassis 1 name=hv1
+
+sim_add hv2
+as hv2
+ovs-vsctl add-br br-phys
+ovn_az_attach az2 n2 br-phys 192.168.0.2
+ovs-vsctl set open . external-ids:ovn-is-interconn=true
+wait_row_count Chassis 1 name=hv2
+
+# No chassis is given, so the port must be local in every AZ.
+check ovn-ic-nbctl tr-add tr0
+check ovn-ic-nbctl trp-add tr0 tr0-p0 00:00:00:11:22:00 192.168.10.10/24
+
+# A single IC-SB port binding is shared by all the AZs.
+wait_row_count ic-sb:Port_Binding 1 logical_port=tr0-p0
+check_row_count ic-sb:Port_Binding 1 type=transit-router-port
+wait_column "00:00:00:11:22:00" ic-sb:Port_Binding address logical_port=tr0-p0
+
+ovn_as az1
+wait_row_count Port_Binding 1 logical_port=tr0-p0
+check_row_count sb:Port_Binding 1 logical_port=tr0-p0 type=patch
+check_column "00:00:00:11:22:00" nb:Logical_Router_Port mac name=tr0-p0
+AT_CHECK([ovn-nbctl get Logical_Router_Port tr0-p0 options:requested-chassis],
+[1], [], [ovn-nbctl: no key "requested-chassis" in Logical_Router_Port record 
"tr0-p0" column options
+])
+az1_tunnel_key=$(ovn-sbctl --bare --columns=tunnel_key find Port_Binding 
logical_port=tr0-p0)
+
+ovn_as az2
+wait_row_count Port_Binding 1 logical_port=tr0-p0
+check_row_count sb:Port_Binding 1 logical_port=tr0-p0 type=patch
+check_column "00:00:00:11:22:00" nb:Logical_Router_Port mac name=tr0-p0
+AT_CHECK([ovn-nbctl get Logical_Router_Port tr0-p0 options:requested-chassis],
+[1], [], [ovn-nbctl: no key "requested-chassis" in Logical_Router_Port record 
"tr0-p0" column options
+])
+az2_tunnel_key=$(ovn-sbctl --bare --columns=tunnel_key find Port_Binding 
logical_port=tr0-p0)
+
+# Both AZs must agree on the tunnel key of the shared port.
+check test "${az1_tunnel_key}" = "${az2_tunnel_key}"
+
+# A MAC change in IC-NB is propagated to the shared IC-SB port binding and
+# from there to the logical router port of every AZ.
+check ovn-ic-nbctl set Transit_Router_Port tr0-p0 mac='"00:00:00:11:22:01"'
+wait_column "00:00:00:11:22:01" ic-sb:Port_Binding address logical_port=tr0-p0
+
+ovn_as az1
+wait_column "00:00:00:11:22:01" nb:Logical_Router_Port mac name=tr0-p0
+
+ovn_as az2
+wait_column "00:00:00:11:22:01" nb:Logical_Router_Port mac name=tr0-p0
+
+# Deleting the port removes it from the IC-SB and from every AZ.
+check ovn-ic-nbctl --wait=sb trp-del tr0-p0
+wait_row_count ic-sb:Port_Binding 0 logical_port=tr0-p0
+
+ovn_as az1
+wait_row_count nb:Logical_Router_Port 0 name=tr0-p0
+wait_row_count Port_Binding 0 logical_port=tr0-p0
+
+ovn_as az2
+wait_row_count nb:Logical_Router_Port 0 name=tr0-p0
+wait_row_count Port_Binding 0 logical_port=tr0-p0
+
+OVN_CLEANUP_IC([az1], [az2])
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([ovn-ic -- Add transit router remote port - race condition])
 
diff --git a/utilities/ovn-ic-nbctl.c b/utilities/ovn-ic-nbctl.c
index 6f5bc0826..79acebdf9 100644
--- a/utilities/ovn-ic-nbctl.c
+++ b/utilities/ovn-ic-nbctl.c
@@ -1570,7 +1570,7 @@ static const struct ctl_command_syntax 
ic_nbctl_commands[] = {
     { "tr-del", 1, 1, "ROUTER", NULL, ic_nbctl_tr_del, NULL, "--if-exists",
         RW },
     { "tr-list", 0, 0, "", NULL, ic_nbctl_tr_list, NULL, "", RO },
-    { "trp-add", 5, INT_MAX,
+    { "trp-add", 3, INT_MAX,
         "ROUTER PORT MAC [NETWORK]...[COLUMN[:KEY]=VALUE]...",
         NULL, ic_nbctl_trp_add, NULL, "--may-exist", RW },
     { "trp-del", 1, 1, "PORT", NULL, ic_nbctl_trp_del, NULL, "--if-exists",
-- 
2.55.0

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

Reply via email to