On Mon, Oct 5, 2026 at 5:45 PM Mairtin O'Loingsigh via dev <
[email protected]> wrote:

> 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
>
>
Thakn you Mairtin,

applied to main.

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

Reply via email to