From: Srikar Tati <[email protected]>

Static routes can choose the fields that their ECMP group hashes on
with the "selection_fields" column.  With selection fields the group
uses the "hash" selection method instead of "dp_hash".  "dp_hash"
needs a recirculation, which the tc datapath cannot offload, so only
groups that use "hash" can be offloaded to a NIC.

Learned routes have no NB row, so they always use "dp_hash".  Add the
"dynamic-routing-ecmp-selection-fields" option to Logical Routers.  It
takes the same fields as "selection_fields" and applies them to the
ECMP groups of the router's learned routes.  If the option has a field
that is not supported, northd logs a warning and keeps "dp_hash".

Assisted-by: Claude Opus 5.5, Claude Code
Signed-off-by: Srikar Tati <[email protected]>
---
 NEWS                           |  4 ++
 northd/en-learned-route-sync.c | 23 +++++++++--
 ovn-nb.xml                     | 22 +++++++++++
 tests/ovn-northd.at            | 71 ++++++++++++++++++++++++++++++++++
 4 files changed, 116 insertions(+), 4 deletions(-)

diff --git a/NEWS b/NEWS
index 13ffd6e2e..5a822cc21 100644
--- a/NEWS
+++ b/NEWS
@@ -28,6 +28,10 @@ Post v26.09.0
        now written to the SB MAC_Binding table and consumed at the
        same priority as dynamic entries, making the preference option
        obsolete.
+     * Add the "options:dynamic-routing-ecmp-selection-fields" option to
+       Logical Routers.  It selects the fields that ECMP groups of learned
+       routes hash on, the same as the "selection_fields" column of a
+       static route.
    - Removed OVN's ovs-bugtool plugin and helper scripts.
    - Removed ovn-sim utility scripts.
    - Removed ovn-docker utility scripts.
diff --git a/northd/en-learned-route-sync.c b/northd/en-learned-route-sync.c
index cbd516b68..959cadf9f 100644
--- a/northd/en-learned-route-sync.c
+++ b/northd/en-learned-route-sync.c
@@ -199,10 +199,25 @@ parse_route_from_sbrec_route(struct hmap 
*parsed_routes_out,
         return NULL;
     }
 
-    return parsed_route_add(od, nexthop, &prefix, plen, false, lrp_addr_s,
-                            out_port, 0, false, false, false, NULL,
-                            ROUTE_SOURCE_LEARNED, true, &route->header_, NULL,
-                            parsed_routes_out);
+    struct sset ecmp_selection_fields =
+        SSET_INITIALIZER(&ecmp_selection_fields);
+    const char *fields_s = smap_get(&od->nbr->options,
+                                    "dynamic-routing-ecmp-selection-fields");
+    if (fields_s && !ovn_ecmp_selection_fields_parse(fields_s,
+                                                     &ecmp_selection_fields)) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+        VLOG_WARN_RL(&rl, "bad 'dynamic-routing-ecmp-selection-fields' %s "
+                     "on router %s, using the default selection method",
+                     fields_s, od->nbr->name);
+    }
+
+    struct parsed_route *pr =
+        parsed_route_add(od, nexthop, &prefix, plen, false, lrp_addr_s,
+                         out_port, 0, false, false, false,
+                         &ecmp_selection_fields, ROUTE_SOURCE_LEARNED, true,
+                         &route->header_, NULL, parsed_routes_out);
+    sset_destroy(&ecmp_selection_fields);
+    return pr;
 }
 
 static void
diff --git a/ovn-nb.xml b/ovn-nb.xml
index feea50a25..0a3154858 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -3716,6 +3716,28 @@ or
         </p>
       </column>
 
+      <column name="options" key="dynamic-routing-ecmp-selection-fields"
+              type='{"type": "string"}'>
+        <p>
+          Only relevant if <ref column="options" key="dynamic-routing"/>
+          is set to <code>true</code>.
+        </p>
+
+        <p>
+          A comma separated list of the fields that select the nexthop of an
+          ECMP group of learned routes, for example
+          <code>ip_proto,ip_src,ip_dst,tp_src,tp_dst</code>.  The supported
+          fields and their meaning are the same as for the
+          <ref column="selection_fields" table="Logical_Router_Static_Route"/>
+          column of a static route.  When set, the ECMP groups of learned
+          routes use the <code>hash</code> selection method on these fields
+          instead of <code>dp_hash</code>.  This lets a NIC offload the
+          group, because <code>hash</code> does not need a recirculation.
+          If the list contains a field that is not supported, OVN uses
+          <code>dp_hash</code>.
+        </p>
+      </column>
+
       <column name="options" key="dynamic-routing-v4-prefix-nexthop"
               type='{"type": "string"}'>
         <p>
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index ec04d4d67..6e99e9aa4 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -17552,6 +17552,77 @@ OVN_CLEANUP_NORTHD
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - learned route ecmp selection fields])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 option:dynamic-routing=true
+check ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24
+check ovn-nbctl --wait=sb lrp-add lr0 lr0-sw1 00:00:00:00:ff:02 10.0.1.1/24
+sw0=$(fetch_column port_binding _uuid logical_port=lr0-sw0)
+sw1=$(fetch_column port_binding _uuid logical_port=lr0-sw1)
+datapath=$(fetch_column datapath_binding _uuid external_ids:name=lr0)
+
+# Learn 192.168.1.0/24 through both ports, so that it is an ECMP group.
+check_uuid ovn-sbctl create Learned_Route \
+    datapath=$datapath                    \
+    logical_port=$sw0                     \
+    ip_prefix=192.168.1.0/24              \
+    nexthop=10.0.0.20
+check_uuid ovn-sbctl create Learned_Route \
+    datapath=$datapath                    \
+    logical_port=$sw1                     \
+    ip_prefix=192.168.1.0/24              \
+    nexthop=10.0.1.20
+check ovn-nbctl --wait=sb sync
+
+# Without selection fields the group uses dp_hash.
+ovn-sbctl dump-flows lr0 > lr0flows
+AT_CHECK([grep -e "(lr_in_ip_routing   ).*192.168.1.0" lr0flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1838 , match=(reg7 == 0 && ip4.dst 
== 192.168.1.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; 
reg8[[16..31]] = select(1, 2);)
+])
+
+check ovn-nbctl --wait=sb set Logical_Router lr0 \
+    options:dynamic-routing-ecmp-selection-fields="ip_proto,ip_src,ip_dst"
+ovn-sbctl dump-flows lr0 > lr0flows
+AT_CHECK([grep -e "(lr_in_ip_routing   ).*192.168.1.0" lr0flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1838 , match=(reg7 == 0 && ip4.dst 
== 192.168.1.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; 
reg8[[16..31]] = select(values=(1, 2); hash_fields="ip_dst,ip_proto,ip_src");)
+])
+
+# tp_src and tp_dst give one flow for each L4 protocol.
+check ovn-nbctl --wait=sb set Logical_Router lr0 \
+    options:dynamic-routing-ecmp-selection-fields="ip_src,ip_dst,tp_src,tp_dst"
+ovn-sbctl dump-flows lr0 > lr0flows
+AT_CHECK([grep -e "(lr_in_ip_routing   ).*192.168.1.0" lr0flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1838 , match=(reg7 == 0 && ip4.dst 
== 192.168.1.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; 
reg8[[16..31]] = select(values=(1, 2); hash_fields="ip_dst,ip_proto,ip_src");)
+  table=??(lr_in_ip_routing   ), priority=1839 , match=(reg7 == 0 && ip4.dst 
== 192.168.1.0/24 && sctp), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] 
= 1; reg8[[16..31]] = select(values=(1, 2); 
hash_fields="ip_dst,ip_proto,ip_src,sctp_dst,sctp_src");)
+  table=??(lr_in_ip_routing   ), priority=1839 , match=(reg7 == 0 && ip4.dst 
== 192.168.1.0/24 && tcp), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] 
= 1; reg8[[16..31]] = select(values=(1, 2); 
hash_fields="ip_dst,ip_proto,ip_src,tcp_dst,tcp_src");)
+  table=??(lr_in_ip_routing   ), priority=1839 , match=(reg7 == 0 && ip4.dst 
== 192.168.1.0/24 && udp), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] 
= 1; reg8[[16..31]] = select(values=(1, 2); 
hash_fields="ip_dst,ip_proto,ip_src,udp_dst,udp_src");)
+])
+
+# A field that is not supported falls back to dp_hash.
+check ovn-nbctl --wait=sb set Logical_Router lr0 \
+    options:dynamic-routing-ecmp-selection-fields="ip_src,foo"
+ovn-sbctl dump-flows lr0 > lr0flows
+AT_CHECK([grep -e "(lr_in_ip_routing   ).*192.168.1.0" lr0flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1838 , match=(reg7 == 0 && ip4.dst 
== 192.168.1.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; 
reg8[[16..31]] = select(1, 2);)
+])
+check grep -q "bad 'dynamic-routing-ecmp-selection-fields' ip_src,foo" \
+    northd/ovn-northd.log
+
+check ovn-nbctl --wait=sb remove Logical_Router lr0 \
+    options dynamic-routing-ecmp-selection-fields
+ovn-sbctl dump-flows lr0 > lr0flows
+AT_CHECK([grep -e "(lr_in_ip_routing   ).*192.168.1.0" lr0flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1838 , match=(reg7 == 0 && ip4.dst 
== 192.168.1.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; 
reg8[[16..31]] = select(1, 2);)
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([dynamic-routing - learned routes on unnumbered LRP])
 AT_KEYWORDS([dynamic-routing])
-- 
2.54.0

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

Reply via email to