I've been thinking about this more and I still think it's a great solution but I just want to highlight (as you did in comments) that the main weakness of this algorithm is that pretty much the only case in which the assigning IDs loop will reach ECMP_ID_MAX_ROUNDS=32 is if some elements have the same hash. In which case there would be 32 rounds of the same failure to find a unique ID repeated over and over, and deriving new hashes incorporating the round on each iteration would be futile. And then in the end they would be assigned IDs based on processing order, which is okay as a last-resort fallback, but that's 32 rounds of wasted cycles to get there.

So Lucas, sorry to ask for more changes, but I have two ideas/requests:

The only way I can think of avoiding the extra rounds is having a variable per-round that tracks the total number of claims made in that round, and if there are no claims made in a round, it breaks out of the claiming phase early and jumps straight to the fallback. If zero claims are made in a round, it's extremely likely that those remaining elements have the same hash as another. WDYT?

Another last minute ask I have is could you change 'size_t n' to 'size_t num_elements' or 'ecmp_elements' or something, so it has a name that conveys what it is more? Becuase we have a lot of loops that run n times and it's easy to lose track of how many times they're running when n is being passed into the functions containing those loops. I guess this would also involve changing 'n_left' as well.

Thank you for bearing with me. :)
- Rosemarie O'Riorden

On 10/20/26 11:57 AM, Rosemarie O'Riorden wrote:
Hi Lucas, thanks for the new version!

I'm happy with how it looks now. Thank you for taking my feedback! I think this is a solid algorithm.

Since I've been the main reviewer, others might request changes, but from my side:
Acked-by: Rosemarie O'Riorden <[email protected]>

(Assuming this passes CI because it's in a long queue right now (the changes are minor since v4 so it should))

On 10/20/26 10:19 AM, Lucas Vargas Dias wrote:
The ECMP group id and the member ids are written into the
lr_in_ip_routing and lr_in_ip_routing_ecmp logical flows, but they
were assigned from the order in which routes reached
en_group_ecmp_route: the group id was hmap_count() at creation time
and the member id was the insertion position.  On a full recompute
the routes are walked in hmap order, while incremental processing
appends them in event order, so the same set of routes could get a
different numbering.  Every forced or fallback recompute could then
delete and re-insert Logical_Flow rows in the Southbound DB even
though nothing had changed.

Derive the ids from hashes instead.  The group id comes from the
group key, i.e. prefix, prefix length, route table, source or
destination route and source class, which does not change as members
are added or removed.  The member id comes from the UUID of the
route's source row, its nexthop and its output port: a NAT or LB row
produces one route per path to the peer router, so the UUID alone is
not unique within a group.

When two elements derive the same id, all of them retry in the next
round with an id derived from their hash and the round number, so
that the ids only depend on the set of routes and not on the order
in which they were processed.  The members are listed in the select()
action in id order for the same reason.

Besides making a recompute produce the same flows as incremental
processing, adding or removing a route no longer changes the ids,
and so the flows, of unrelated groups or members.

Fixes: 6979a96138a2 ("northd: Support I+P for group_ecmp_route engine.")
Fixes: 5395e12f247c ("northd: Drop traffic for ECMP group with "discard" route.")
Suggested-by: Rosemarie O'Riorden <[email protected]>
Assisted-by: Claude Opus 5.5, Claude Code
Signed-off-by: Lucas Vargas Dias <[email protected]>
---
v5:
   - Implement route_sources_ecmp_compatible() on top of
     ecmp_group_source_class() so that the NAT/LB rule lives in one place.
   - Hash the whole source row UUID with hash_bytes() in
     ecmp_member_hash().
   - Derive each id once per round in ecmp_assign_ids().
   - Cross-reference ecmp_group_hash() and ecmp_groups_find().
v4:
   - Derive the ECMP group and member ids from hashes instead of sorting,
     so that adding or removing a route doesn't change the ids of
     unrelated groups or members (suggested by Rosemarie).
   - Added a test with NAT and LB routes reachable over several paths,
     whose ECMP members share their source row.
   - Added ovn_strip_ecmp_ids(), ecmp_ids() and ecmp_member_id() for the
     tests.
v3:
   - Dropped nullable_strcmp() and the patch converting its callers.
   - Removed the unused 'route_count' field from struct ecmp_groups_node.
v2:
   - Moved nullable_strcmp() to lib/ovn-util.h and documented its NULL
     handling.
   - Clarified in ecmp_groups_node_cmp() that it can't return 0 for two
     distinct groups.

  northd/en-group-ecmp-route.c | 262 ++++++++++++++++++++++++++----
  northd/en-group-ecmp-route.h |   1 -
  tests/ovn-macros.at          |  34 ++++
  tests/ovn-northd.at          | 300 +++++++++++++++++++++++++++--------
  4 files changed, 494 insertions(+), 103 deletions(-)

diff --git a/northd/en-group-ecmp-route.c b/northd/en-group-ecmp-route.c
index aca197318..28c6d8ce5 100644
--- a/northd/en-group-ecmp-route.c
+++ b/northd/en-group-ecmp-route.c
@@ -24,6 +24,7 @@
  #include "en-advertised-route-sync.h"
  #include "en-group-ecmp-route.h"
  #include "en-learned-route-sync.h"
+#include "hash.h"
  #include "hmapx.h"
  #include "openvswitch/hmap.h"
@@ -182,22 +183,26 @@ unique_routes_destroy(struct hmap *unique_routes)
      hmap_destroy(unique_routes);
  }
-/* Allow LB and NAT forwarding routes for the same prefix to coexist as
+/* Routes for the same prefix share an ecmp group if their sources map to the
+ * same class.
+ *
+ * LB and NAT forwarding routes are in the same class so that they coexist as    * ECMP members instead of one silently replacing the other.  This is an
   * edge case: it arises only when two different peer LRs independently
   * use the same IP as a VIP and a NAT external IP, and a CMS would
   * typically prevent such overlap.  Without this guard, one route
   * would silently win and traffic to the other service would be
   * black-holed. */
+static enum route_source
+ecmp_group_source_class(enum route_source source)
+{
+    return source == ROUTE_SOURCE_LB ? ROUTE_SOURCE_NAT : source;
+}
+
  static bool
  route_sources_ecmp_compatible(enum route_source a, enum route_source b)
  {
-    if (a == b) {
-        return true;
-    }
-
-    return (a == ROUTE_SOURCE_NAT || a == ROUTE_SOURCE_LB) &&
-           (b == ROUTE_SOURCE_NAT || b == ROUTE_SOURCE_LB);
+    return ecmp_group_source_class(a) == ecmp_group_source_class(b);
  }
  /* Remove the unique_routes_node from the group, and return the parsed_route @@ -228,14 +233,12 @@ ecmp_groups_add_route(struct ecmp_groups_node *group,
                        const struct parsed_route *route)
  {
      static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-    if (group->route_count == UINT16_MAX) {
+    if (vector_len(&group->route_list) == UINT16_MAX) {
          VLOG_WARN_RL(&rl, "too many routes in a single ecmp group.");
          return;
      }
      if (route->is_discard_route) {
-        group->has_discard_route = true;
-
          char *prefix = normalize_v46_prefix(&route->prefix, route- >plen);
          VLOG_WARN_RL(&rl, "The ECMP route \"%s\" contains \"discard\" "
                       "route, the whole group will drop traffic.", prefix); @@ -244,22 +247,13 @@ ecmp_groups_add_route(struct ecmp_groups_node *group,
      struct ecmp_route_list_node er = (struct ecmp_route_list_node) {
          .route = route,
-        .id = ++group->route_count,
      };
-    if (group->route_count == 1) {
-        sset_clone(&group->selection_fields, &route- >ecmp_selection_fields);
-    } else {
-        sset_intersect(&group->selection_fields,
-                       &route->ecmp_selection_fields);
-    }
-
      vector_push(&group->route_list, &er);
  }
-/* Removes a route from an ecmp group. If the ecmp group should persist
- * afterwards you must call ecmp_groups_update_ids before any further
- * insertions. */
+/* Removes a route from an ecmp group.  The ids of the group and of its
+ * members are refreshed by group_ecmp_datapath_finalize(). */
  static const struct parsed_route *
  ecmp_groups_remove_route(struct ecmp_groups_node *group,
                           const struct parsed_route *pr)
@@ -278,15 +272,217 @@ ecmp_groups_remove_route(struct ecmp_groups_node *group,
      return NULL;
  }
+/* Hashes the key of 'eg', which is unique within a datapath.  It must cover + * the fields that ecmp_groups_find() compares.  Unlike the members, the key + * does not change when routes are added to or removed from the group. */
+static uint32_t
+ecmp_group_hash(const struct ecmp_groups_node *eg)
+{
+    uint32_t hash = hash_bytes(&eg->prefix, sizeof eg->prefix, 0);
+    hash = hash_add(hash, eg->plen);
+    hash = hash_add(hash, eg->route_table_id);
+    hash = hash_add(hash, eg->is_src_route);
+    hash = hash_add(hash, ecmp_group_source_class(eg->source));
+    return hash_finish(hash, 0);
+}
+
+/* Hashes what makes 'route' unique within its ecmp group.  A NAT or LB row + * produces one route per path to the peer router, so its UUID alone is not
+ * enough. */
+static uint32_t
+ecmp_member_hash(const struct parsed_route *route)
+{
+    const struct uuid *uuid = &route->source_hint->uuid;
+    uint32_t hash = hash_bytes(uuid, sizeof *uuid, 0);
+    if (route->nexthop) {
+        hash = hash_bytes(route->nexthop, sizeof *route->nexthop, hash);
+    }
+    if (route->out_port) {
+        hash = hash_string(route->out_port->key, hash);
+    }
+    return hash;
+}
+
+/* Number of rounds after which ecmp_assign_ids() stops retrying hashes that
+ * collide and falls back to the next free id. */
+#define ECMP_ID_MAX_ROUNDS 32
+
+struct ecmp_id_slot {
+    struct hmap_node hmap_node;
+    uint16_t id;
+    bool taken;         /* Assigned to an element. */
+    size_t n_claims;    /* Elements that derived 'id' in the current round. */
+};
+
+static struct ecmp_id_slot *
+ecmp_id_slot_get(struct hmap *slots, uint16_t id)
+{
+    struct ecmp_id_slot *slot;
+    HMAP_FOR_EACH_WITH_HASH (slot, hmap_node, id, slots) {
+        if (slot->id == id) {
+            return slot;
+        }
+    }
+    slot = xzalloc(sizeof *slot);
+    slot->id = id;
+    hmap_insert(slots, &slot->hmap_node, id);
+    return slot;
+}
+
+static uint16_t
+ecmp_id_from_hash(uint32_t hash, unsigned int round)
+{
+    return hash_int(round, hash) % UINT16_MAX + 1;
+}
+
+/* Gives each of the 'n' elements a distinct id in [1, UINT16_MAX] derived + * from 'hashes', so that the id of an element does not depend on the order + * in which the elements are passed, nor on the ids of unrelated elements.
+ *
+ * An element keeps the id derived from its hash unless that id is already + * taken or another element derives the same id in the same round. In that + * case all of them retry in the next round, with an id derived from their + * hash and the round number.  The result therefore only depends on the set + * of hashes.  Elements whose hashes are equal can't be told apart and, after + * ECMP_ID_MAX_ROUNDS rounds, take the next free id in the order they are
+ * passed. */
+static void
+ecmp_assign_ids(const uint32_t *hashes, uint16_t *ids, size_t n)
+{
+    struct hmap slots = HMAP_INITIALIZER(&slots);
+    /* The slot that each element claims in the current round. */
+    struct ecmp_id_slot **claims = xmalloc(n * sizeof *claims);
+    struct ecmp_id_slot *slot;
+    size_t n_left = n;
+
+    memset(ids, 0, n * sizeof *ids);
+    for (unsigned int round = 0; n_left && round < ECMP_ID_MAX_ROUNDS;
+         round++) {
+        for (size_t i = 0; i < n; i++) {
+            if (!ids[i]) {
+                uint16_t id = ecmp_id_from_hash(hashes[i], round);
+                claims[i] = ecmp_id_slot_get(&slots, id);
+                claims[i]->n_claims++;
+            }
+        }
+        for (size_t i = 0; i < n; i++) {
+            if (!ids[i]) {
+                slot = claims[i];
+                if (!slot->taken && slot->n_claims == 1) {
+                    slot->taken = true;
+                    ids[i] = slot->id;
+                    n_left--;
+                }
+            }
+        }
+        HMAP_FOR_EACH (slot, hmap_node, &slots) {
+            slot->n_claims = 0;
+        }
+    }
+    free(claims);
+
+    for (size_t i = 0; n_left && i < n; i++) {
+        if (ids[i]) {
+            continue;
+        }
+        uint16_t id = ecmp_id_from_hash(hashes[i], 0);
+        while ((slot = ecmp_id_slot_get(&slots, id))->taken) {
+            id = id % UINT16_MAX + 1;
+        }
+        slot->taken = true;
+        ids[i] = id;
+        n_left--;
+    }
+
+    HMAP_FOR_EACH_POP (slot, hmap_node, &slots) {
+        free(slot);
+    }
+    hmap_destroy(&slots);
+}
+
+static int
+ecmp_route_list_node_id_cmp(const void *a_, const void *b_)
+{
+    const struct ecmp_route_list_node *a = a_;
+    const struct ecmp_route_list_node *b = b_;
+
+    return a->id < b->id ? -1 : a->id > b->id;
+}
+
+/* Recomputes everything in 'group' that is derived from its members, so that
+ * the result only depends on the set of members. */
  static void
-ecmp_group_update_ids(struct ecmp_groups_node *group)
+ecmp_group_finalize(struct ecmp_groups_node *group)
  {
+    size_t n = vector_len(&group->route_list);
+    uint32_t *hashes = xmalloc(n * sizeof *hashes);
+    uint16_t *ids = xmalloc(n * sizeof *ids);
      struct ecmp_route_list_node *er;
      size_t i = 0;
+
+    VECTOR_FOR_EACH_PTR (&group->route_list, er) {
+        hashes[i++] = ecmp_member_hash(er->route);
+    }
+    ecmp_assign_ids(hashes, ids, n);
+    i = 0;
      VECTOR_FOR_EACH_PTR (&group->route_list, er) {
-        er->id = i++;
+        er->id = ids[i++];
      }
-    group->route_count = i;
+    free(hashes);
+    free(ids);
+
+    /* The members are listed in this order in the select() action. */
+    vector_qsort(&group->route_list, ecmp_route_list_node_id_cmp);
+
+    bool first = true;
+    group->has_discard_route = false;
+    VECTOR_FOR_EACH_PTR (&group->route_list, er) {
+        if (er->route->is_discard_route) {
+            group->has_discard_route = true;
+        }
+        if (first) {
+            sset_destroy(&group->selection_fields);
+            sset_clone(&group->selection_fields,
+                       &er->route->ecmp_selection_fields);
+            group->source = er->route->source;
+            first = false;
+        } else {
+            sset_intersect(&group->selection_fields,
+                           &er->route->ecmp_selection_fields);
+        }
+    }
+}
+
+/* Assigns the group and member ids of all ecmp groups of 'gn'.  Must be
+ * called after the ecmp groups of 'gn' changed, both on full recompute and on + * incremental processing, so that both yield the same ids for the same set + * of routes and the generated logical flows do not change on recompute. */
+static void
+group_ecmp_datapath_finalize(struct group_ecmp_datapath *gn)
+{
+    size_t n = hmap_count(&gn->ecmp_groups);
+    if (!n) {
+        return;
+    }
+
+    struct ecmp_groups_node **groups = xmalloc(n * sizeof *groups);
+    uint32_t *hashes = xmalloc(n * sizeof *hashes);
+    uint16_t *ids = xmalloc(n * sizeof *ids);
+    struct ecmp_groups_node *eg;
+    size_t i = 0;
+
+    HMAP_FOR_EACH (eg, hmap_node, &gn->ecmp_groups) {
+        ecmp_group_finalize(eg);
+        groups[i] = eg;
+        hashes[i++] = ecmp_group_hash(eg);
+    }
+    ecmp_assign_ids(hashes, ids, n);
+    for (i = 0; i < n; i++) {
+        groups[i]->id = ids[i];
+    }
+    free(groups);
+    free(hashes);
+    free(ids);
  }
  static struct ecmp_groups_node *
@@ -302,7 +498,6 @@ ecmp_groups_add(struct group_ecmp_datapath *gn,
      struct ecmp_groups_node *eg = xzalloc(sizeof *eg);
      hmap_insert(&gn->ecmp_groups, &eg->hmap_node, route->hash);
-    eg->id = hmap_count(&gn->ecmp_groups);
      eg->prefix = route->prefix;
      eg->plen = route->plen;
      eg->is_src_route = route->is_src_route;
@@ -321,6 +516,7 @@ ecmp_groups_find(struct group_ecmp_datapath *gn,
  {
      struct ecmp_groups_node *eg;
      HMAP_FOR_EACH_WITH_HASH (eg, hmap_node, route->hash, &gn- >ecmp_groups) {
+        /* Keep in sync with ecmp_group_hash(). */
          if (ipv6_addr_equals(&eg->prefix, &route->prefix) &&
              eg->plen == route->plen &&
              eg->is_src_route == route->is_src_route &&
@@ -396,6 +592,10 @@ group_ecmp_route(struct group_ecmp_route_data *data,
          gn = group_ecmp_datapath_lookup_or_add(data, pr->od);
          add_route(gn, pr);
      }
+
+    HMAP_FOR_EACH (gn, hmap_node, &data->datapaths) {
+        group_ecmp_datapath_finalize(gn);
+    }
  }
  enum engine_node_state
@@ -472,9 +672,7 @@ handle_deleted_route(struct group_ecmp_route_data *data,                * unique route. Otherwise it stays an ecmp group with just one
               * member. */
              ecmp_groups_remove_route(eg, pr);
-            if (ecmp_group_has_symmetric_reply(eg)) {
-                ecmp_group_update_ids(eg);
-            } else {
+            if (!ecmp_group_has_symmetric_reply(eg)) {
                  const struct ecmp_route_list_node *er =
                      vector_get_ptr(&eg->route_list, 0);
                  unique_routes_add(node, er->route);
@@ -482,11 +680,8 @@ handle_deleted_route(struct group_ecmp_route_data *data,
                  ecmp_groups_node_free(eg);
              }
          } else {
-            /* We can just remove the member from the group. We need to update -             * the indices of all routes so that future insertions directly
-             * have a new index. */
+            /* We can just remove the member from the group. */
              ecmp_groups_remove_route(eg, pr);
-            ecmp_group_update_ids(eg);
          }
      }
@@ -537,6 +732,7 @@ group_ecmp_route_learned_route_change_handler(struct engine_node *eng_node,
              hmapx_add(&data->trk_data.deleted_datapath_routes, node);
              hmap_remove(&data->datapaths, &node->hmap_node);
          } else {
+            group_ecmp_datapath_finalize(node);
              hmapx_add(&data->trk_data.crupdated_datapath_routes, node);
          }
      }
@@ -589,6 +785,7 @@ group_ecmp_route_routes_change_handler(struct engine_node *eng_node,
              hmapx_add(&data->trk_data.deleted_datapath_routes, node);
              hmap_remove(&data->datapaths, &node->hmap_node);
          } else {
+            group_ecmp_datapath_finalize(node);
              hmapx_add(&data->trk_data.crupdated_datapath_routes, node);
          }
      }
@@ -645,6 +842,7 @@ group_ecmp_route_dynamic_routes_change_handler(struct engine_node *eng_node,
              hmapx_add(&gdata->trk_data.deleted_datapath_routes, node);
              hmap_remove(&gdata->datapaths, &node->hmap_node);
          } else {
+            group_ecmp_datapath_finalize(node);
              hmapx_add(&gdata->trk_data.crupdated_datapath_routes, node);
          }
      }
diff --git a/northd/en-group-ecmp-route.h b/northd/en-group-ecmp-route.h
index b40935be2..13f84a036 100644
--- a/northd/en-group-ecmp-route.h
+++ b/northd/en-group-ecmp-route.h
@@ -37,7 +37,6 @@ struct ecmp_groups_node {
      bool has_discard_route;
      enum route_source source;
      uint32_t route_table_id;
-    uint16_t route_count;
      struct vector route_list; /* Contains ecmp_route_list_node */
      struct sset selection_fields;
  };
diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
index 7a19c0130..1f36f127a 100644
--- a/tests/ovn-macros.at
+++ b/tests/ovn-macros.at
@@ -1568,6 +1568,40 @@ ovn_strip_lflows() {
       sed 's/table=[[0-9]]\{1,2\}\s\?/table=??/g' | sort
  }
+# The ECMP group and member ids are derived from hashes of the routes, replace
+# them with <gid> and <mid>.
+ovn_strip_ecmp_ids() {
+    sed -e 's/\(reg8\[[0\.\.15\]] =\{1,2\} \)[[1-9]][[0-9]]*/\1<gid>/' \
+        -e 's/\(reg8\[[16\.\.31\]] =\{1,2\} \)[[1-9]][[0-9]]*/ \1<mid>/' \
+        -e ':a' \
+        -e 's/\(select[[^)]]*[[(, ]]\)[[0-9]][[0-9]]*/\1<mid>/' \
+        -e 'ta'
+}
+
+# ecmp_ids LR PREFIX: prints the group id and the comma separated member ids +# of the ECMP group of PREFIX on LR, and fails if the members listed in its +# select() action are not the ones that have a flow in lr_in_ip_routing_ecmp.
+ecmp_ids() {
+    local flows gid sel mids
+    flows=$(ovn-sbctl dump-flows $1)
+    gid=$(echo "$flows" | sed -n "s|.*ip[[46]]\.dst == $2).*reg8\[[0\.\.15\]] = \([[0-9]]*\);.*|\1|p") +    sel=$(echo "$flows" | sed -n "s|.*ip[[46]]\.dst == $2).*select(\([[^)]]*\));.*|\1|p" | tr -d ' ') +    mids=$(echo "$flows" | sed -n "s|.*(reg8\[[0\.\.15\]] == $gid && reg8\[[16\.\.31\]] == \([[0-9]]*\)).*|\1|p" | sort -n | paste -sd, -)
+    if test -z "$gid" || test "$sel" != "$mids"; then
+        echo "$2: group '$gid', select($sel) != members($mids)" >&2
+        return 1
+    fi
+    echo "$gid $sel"
+}
+
+# ecmp_member_id LR PREFIX NEXTHOP: prints the member id of NEXTHOP in the
+# ECMP group of PREFIX on LR.
+ecmp_member_id() {
+    local gid
+    gid=$(ecmp_ids $1 $2 | cut -d' ' -f1)
+    ovn-sbctl dump-flows $1 | sed -n "s|.*(reg8\[[0\.\.15\]] == $gid && reg8\[[16\.\.31\]] == \([[0-9]]*\)), action=(reg0 = $3;.*|\1|p"
+}
+
  ovn_strip_collector_set() {
      sed 's/collector_set=[[0-9]]*,\?/collector_set=??,/g'
  }
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 7a0a400c4..e00627e42 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -7799,18 +7799,18 @@ check_row_count ECMP_Nexthop 1
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing   ), priority=0    , match=(1), action=(drop;)     table=??(lr_in_ip_routing   ), priority=10300, match=(ct_mark.ecmp_reply_port == 1 && reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; eth.src = 00:00:20:20:12:13; reg5 = 192.168.0.1; outport = "lr0-public"; next;)     table=??(lr_in_ip_routing   ), priority=10550, match=(nd_rs || nd_ra), action=(drop;)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0- public"; flags.loopback = 1; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing   ), priority=1936 , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = 1; next;) +  table=??(lr_in_ip_routing   ), priority=1936 , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = <mid>; next;)     table=??(lr_in_ip_routing   ), priority=2322 , match=(inport == "lr0-public" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; reg9[[9]] = 0; next;)
  ])
-AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 1), action=(reg0 = 192.168.0.10; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; reg9[[9]] = 1; reg9[[10]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 192.168.0.10; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; reg9[[9]] = 1; reg9[[10]] = 1; next;)     table=??(lr_in_ip_routing_ecmp), priority=150  , match=(reg8[[0..15]] == 0), action=(next;)
  ])
@@ -7825,18 +7825,18 @@ check_column 192.168.0.10 ECMP_Nexthop nexthop nexthop=192.168.0.10
  check_column 192.168.0.20 ECMP_Nexthop nexthop nexthop=192.168.0.20
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing   ), priority=0    , match=(1), action=(drop;)     table=??(lr_in_ip_routing   ), priority=10300, match=(ct_mark.ecmp_reply_port == 1 && reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; eth.src = 00:00:20:20:12:13; reg5 = 192.168.0.1; outport = "lr0-public"; next;)     table=??(lr_in_ip_routing   ), priority=10550, match=(nd_rs || nd_ra), action=(drop;)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0- public"; flags.loopback = 1; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing   ), priority=1936 , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);) +  table=??(lr_in_ip_routing   ), priority=1936 , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)     table=??(lr_in_ip_routing   ), priority=2322 , match=(inport == "lr0-public" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; reg9[[9]] = 0; next;)
  ])
-AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed 's/192\.168\.0\..0/192.168.0.??/' | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed 's/192\.168\.0\..0/192.168.0.??/' | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 1), action=(reg0 = 192.168.0.??; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; reg9[[9]] = 1; reg9[[10]] = 1; next;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 2), action=(reg0 = 192.168.0.??; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; reg9[[9]] = 1; reg9[[10]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 192.168.0.??; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; reg9[[9]] = 1; reg9[[10]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 192.168.0.??; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; reg9[[9]] = 1; reg9[[10]] = 1; next;)     table=??(lr_in_ip_routing_ecmp), priority=150  , match=(reg8[[0..15]] == 0), action=(next;)
  ])
@@ -7867,18 +7867,18 @@ check_column 192.168.0.10 ECMP_Nexthop nexthop nexthop=192.168.0.10
  check_column 192.168.0.20 ECMP_Nexthop nexthop nexthop=192.168.0.20
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing   ), priority=0    , match=(1), action=(drop;)     table=??(lr_in_ip_routing   ), priority=10300, match=(ct_mark.ecmp_reply_port == 1 && reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; eth.src = 00:00:20:20:12:13; reg5 = 192.168.0.1; outport = "lr0-public"; next;)     table=??(lr_in_ip_routing   ), priority=10550, match=(nd_rs || nd_ra), action=(drop;)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0- public"; flags.loopback = 1; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing   ), priority=1936 , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);) +  table=??(lr_in_ip_routing   ), priority=1936 , match=(reg7 == 0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)     table=??(lr_in_ip_routing   ), priority=2322 , match=(inport == "lr0-public" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; reg9[[9]] = 0; next;)
  ])
-AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed 's/192\.168\.0\..0/192.168.0.??/' | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed 's/192\.168\.0\..0/192.168.0.??/' | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 1), action=(reg0 = 192.168.0.??; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; reg9[[9]] = 1; reg9[[10]] = 1; next;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 2), action=(reg0 = 192.168.0.??; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; reg9[[9]] = 1; reg9[[10]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 192.168.0.??; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; reg9[[9]] = 1; reg9[[10]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 192.168.0.??; reg5 = 192.168.0.1; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; reg9[[9]] = 1; reg9[[10]] = 1; next;)     table=??(lr_in_ip_routing_ecmp), priority=150  , match=(reg8[[0..15]] == 0), action=(next;)
  ])
@@ -7916,8 +7916,8 @@ check ovn-nbctl set logical_router_static_route $route2_uuid selection_fields="i
  check ovn-nbctl --wait=sb sync
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep -e "(lr_in_ip_routing   ).*3.3.0.0" lr0flows | sed 's/ table=../table=??/' | sort], [0], [dnl -  table=??(lr_in_ip_routing   ), priority=1744 , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16), 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");) +AT_CHECK([grep -e "(lr_in_ip_routing   ).*3.3.0.0" lr0flows | ovn_strip_ecmp_ids | sed 's/table=../table=??/' | sort], [0], [dnl +  table=??(lr_in_ip_routing   ), priority=1744 , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(values=(<mid>, <mid>); hash_fields="ip_dst,ip_proto,ip_src");)
  ])
  check ovn-nbctl set logical_router_static_route $route1_uuid selection_fields="ip_src,ip_dst,tp_src,tp_dst" @@ -7925,11 +7925,11 @@ check ovn-nbctl set logical_router_static_route $route2_uuid selection_fields="i
  check ovn-nbctl --wait=sb sync
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep -e "(lr_in_ip_routing   ).*3.3.0.0" lr0flows | sed 's/ table=../table=??/' | sort], [0], [dnl -  table=??(lr_in_ip_routing   ), priority=1744 , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16), 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=1745 , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16 && 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=1745 , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16 && 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=1745 , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16 && 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");) +AT_CHECK([grep -e "(lr_in_ip_routing   ).*3.3.0.0" lr0flows | ovn_strip_ecmp_ids | sed 's/table=../table=??/' | sort], [0], [dnl +  table=??(lr_in_ip_routing   ), priority=1744 , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(values=(<mid>, <mid>); hash_fields="ip_dst,ip_proto,ip_src");) +  table=??(lr_in_ip_routing   ), priority=1745 , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16 && sctp), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(values=(<mid>, <mid>); hash_fields="ip_dst,ip_proto,ip_src,sctp_dst,sctp_src");) +  table=??(lr_in_ip_routing   ), priority=1745 , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16 && tcp), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(values=(<mid>, <mid>); hash_fields="ip_dst,ip_proto,ip_src,tcp_dst,tcp_src");) +  table=??(lr_in_ip_routing   ), priority=1745 , match=(reg7 == 0 && ip4.dst == 3.3.0.0/16 && udp), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(values=(<mid>, <mid>); hash_fields="ip_dst,ip_proto,ip_src,udp_dst,udp_src");)
  ])
  AT_CHECK([grep -e "lr_in_ecmp_stateful_egr" lr0flows | ovn_strip_lflows], [0], [dnl @@ -17440,19 +17440,19 @@ datapath=$(fetch_column datapath_binding _uuid external_ids:name=lr0)
  # Validate the routes.
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing   ), priority=0    , match=(1), action=(drop;)     table=??(lr_in_ip_routing   ), priority=10550, match=(nd_rs || nd_ra), action=(drop;) -  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);) +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 10.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 10.0.0.1; eth.src = 00:00:00:00:ff:01; outport = "lr0-sw0"; flags.loopback = 1; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 10.0.1.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 10.0.1.1; eth.src = 00:00:00:00:ff:02; outport = "lr0-sw1"; flags.loopback = 1; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing   ), priority=2322 , match=(inport == "lr0-sw0" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:ff01; eth.src = 00:00:00:00:ff:01; outport = "lr0-sw0"; flags.loopback = 1; reg9[[9]] = 0; next;)     table=??(lr_in_ip_routing   ), priority=2322 , match=(inport == "lr0-sw1" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:ff02; eth.src = 00:00:00:00:ff:02; outport = "lr0-sw1"; flags.loopback = 1; reg9[[9]] = 0; next;)
  ])
-AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed -e 's/10\.0\..\./10.0.??./g' -e 's/lr0-sw./lr0-sw??/' -e 's/00:ff:0./00:ff:0?/' | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed -e 's/10\.0\..\./10.0.??./g' -e 's/lr0-sw./lr0-sw??/' -e 's/00:ff:0./00:ff:0?/' | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 1), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 2), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing_ecmp), priority=150  , match=(reg8[[0..15]] == 0), action=(next;)
  ])
@@ -17466,20 +17466,20 @@ check ovn-nbctl --wait=sb sync
  check_row_count Advertised_Route 4
  check_row_count Learned_Route 1
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing   ), priority=0    , match=(1), action=(drop;)     table=??(lr_in_ip_routing   ), priority=10550, match=(nd_rs || nd_ra), action=(drop;)     table=??(lr_in_ip_routing   ), priority=1838 , match=(reg7 == 0 && ip4.dst == 192.168.1.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 10.0.0.20; reg5 = 10.0.0.1; eth.src = 00:00:00:00:ff:01; outport = "lr0-sw0"; flags.loopback = 1; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);) +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 10.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 10.0.0.1; eth.src = 00:00:00:00:ff:01; outport = "lr0-sw0"; flags.loopback = 1; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 10.0.1.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 10.0.1.1; eth.src = 00:00:00:00:ff:02; outport = "lr0-sw1"; flags.loopback = 1; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing   ), priority=2322 , match=(inport == "lr0-sw0" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:ff01; eth.src = 00:00:00:00:ff:01; outport = "lr0-sw0"; flags.loopback = 1; reg9[[9]] = 0; next;)     table=??(lr_in_ip_routing   ), priority=2322 , match=(inport == "lr0-sw1" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:ff02; eth.src = 00:00:00:00:ff:02; outport = "lr0-sw1"; flags.loopback = 1; reg9[[9]] = 0; next;)
  ])
-AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed -e 's/10\.0\..\./10.0.??./g' -e 's/lr0-sw./lr0-sw??/' -e 's/00:ff:0./00:ff:0?/' | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed -e 's/10\.0\..\./10.0.??./g' -e 's/lr0-sw./lr0-sw??/' -e 's/00:ff:0./00:ff:0?/' | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 1), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 2), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing_ecmp), priority=150  , match=(reg8[[0..15]] == 0), action=(next;)
  ])
@@ -17494,22 +17494,22 @@ check ovn-nbctl --wait=sb sync
  check_row_count Advertised_Route 4
  check_row_count Learned_Route 2
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing   ), priority=0    , match=(1), action=(drop;)     table=??(lr_in_ip_routing   ), priority=10550, match=(nd_rs || nd_ra), action=(drop;) -  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]] = 2; reg8[[16..31]] = select(1, 2);) -  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);) +  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]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);) +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 10.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 10.0.0.1; eth.src = 00:00:00:00:ff:01; outport = "lr0-sw0"; flags.loopback = 1; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 10.0.1.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 10.0.1.1; eth.src = 00:00:00:00:ff:02; outport = "lr0-sw1"; flags.loopback = 1; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing   ), priority=2322 , match=(inport == "lr0-sw0" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:ff01; eth.src = 00:00:00:00:ff:01; outport = "lr0-sw0"; flags.loopback = 1; reg9[[9]] = 0; next;)     table=??(lr_in_ip_routing   ), priority=2322 , match=(inport == "lr0-sw1" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:ff02; eth.src = 00:00:00:00:ff:02; outport = "lr0-sw1"; flags.loopback = 1; reg9[[9]] = 0; next;)
  ])
-AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed -e 's/10\.0\..\./10.0.??./g' -e 's/lr0-sw./lr0-sw??/' -e 's/00:ff:0./00:ff:0?/' | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed -e 's/10\.0\..\./10.0.??./g' -e 's/lr0-sw./lr0-sw??/' -e 's/00:ff:0./00:ff:0?/' | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 1), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == 2), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 2 && reg8[[16..31]] == 1), action=(reg0 = 10.0.??.20; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 2 && reg8[[16..31]] == 2), action=(reg0 = 10.0.??.20; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.??.10; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.??.20; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.??.20; reg5 = 10.0.??.1; eth.src = 00:00:00:00:ff:0?; outport = "lr0-sw??"; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing_ecmp), priority=150  , match=(reg8[[0..15]] == 0), action=(next;)
  ])
@@ -17536,12 +17536,12 @@ check ovn-nbctl --wait=sb sync
  check_row_count Advertised_Route 4
  check_row_count Learned_Route 4
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | sed -e 's/ reg8\[[0..15\]] = [[123]]/reg8\[[0..15\]] = ??/' | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing   ), priority=0    , match=(1), action=(drop;)     table=??(lr_in_ip_routing   ), priority=10550, match=(nd_rs || nd_ra), action=(drop;) -  table=??(lr_in_ip_routing   ), priority=1838 , match=(reg7 == 0 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = ??; reg8[[16..31]] = select(1, 2);) -  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]] = ??; reg8[[16..31]] = select(1, 2);) -  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = ??; reg8[[16..31]] = select(1, 2);) +  table=??(lr_in_ip_routing   ), priority=1838 , match=(reg7 == 0 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);) +  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]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);) +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.0.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 10.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 10.0.0.1; eth.src = 00:00:00:00:ff:01; outport = "lr0-sw0"; flags.loopback = 1; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing   ), priority=1842 , match=(ip4.dst == 10.0.1.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg5 = 10.0.1.1; eth.src = 00:00:00:00:ff:02; outport = "lr0-sw1"; flags.loopback = 1; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing   ), priority=2322 , match=(inport == "lr0-sw0" && ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:ff01; eth.src = 00:00:00:00:ff:01; outport = "lr0-sw0"; flags.loopback = 1; reg9[[9]] = 0; next;)
@@ -17586,13 +17586,13 @@ check_uuid ovn-sbctl create Learned_Route \
  check ovn-nbctl --wait=sb sync
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | grep -F "42.42.42.42" | ovn_strip_lflows], [0], [dnl -  table=??(lr_in_ip_routing   ), priority=1934 , match=(reg7 == 0 && ip4.dst == 42.42.42.42/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);) +AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | grep -F "42.42.42.42" | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl +  table=??(lr_in_ip_routing   ), priority=1934 , match=(reg7 == 0 && ip4.dst == 42.42.42.42/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)
  ])
-AT_CHECK([grep "lr_in_ip_routing_ecmp" lr0flows | grep "42" | sed -e 's/reg8\[[16..31\]] == [[12]]/reg8\[[16..31\]] == ??/g' | ovn_strip_lflows], [0], [dnl -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ??), action=(reg0 = 192.168.2.42; eth.src = 00:00:00:00:ff:02; outport = "lrp2"; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ??), action=(reg0 = 192.168.3.42; eth.src = 00:00:00:00:ff:03; outport = "lrp3"; reg9[[9]] = 1; next;) +AT_CHECK([grep "lr_in_ip_routing_ecmp" lr0flows | grep "42" | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 192.168.2.42; eth.src = 00:00:00:00:ff:02; outport = "lrp2"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 192.168.3.42; eth.src = 00:00:00:00:ff:03; outport = "lrp3"; reg9[[9]] = 1; next;)
  ])
  OVN_CLEANUP_NORTHD
@@ -19350,8 +19350,8 @@ check_row_count Advertised_Route 1 \
  # One ECMP selection flow covers the prefix and two member flows carry the
  # respective neighbor nexthops.
  ovn-sbctl lflow-list lr0 > lr0_flows
-AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows | \
-          grep -c 'select(1, 2)'], [0], [1
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows | ovn_strip_ecmp_ids | \
+          grep -c 'select(<mid>, <mid>)'], [0], [1
  ])
  AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0_flows | \
            grep -E -c 'reg0 = 10\.0\.0\.[[12]];'], [0], [2
@@ -19370,6 +19370,85 @@ OVN_CLEANUP_NORTHD
  AT_CLEANUP
  ])
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - NAT and LB reachable over several paths use ECMP])
+AT_KEYWORDS([dynamic-routing ecmp])
+ovn_start
+
+# lr0 reaches lr1 over the switches "a" and "b" and redistributes NAT and LB
+# addresses on both ports.  The NAT and the LB of lr1 then produce one
+# forwarding route per path on lr0.  The two routes of each ECMP group share +# their source row and only differ in the nexthop and output port, they must
+# still get distinct ids that don't depend on the processing order.
+
+check ovn-nbctl lr-add lr0 -- set Logical_Router lr0 \
+    options:dynamic-routing=true options:chassis=hv1
+check ovn-nbctl lr-add lr1 -- set Logical_Router lr1 options:chassis=hv1
+i=0
+for s in a b; do
+    i=$((i + 1))
+    check ovn-nbctl ls-add $s
+    check ovn-nbctl lrp-add lr0 lr0-$s 00:00:00:00:0$i:01 10.0.$i.1/24
+    check ovn-nbctl lrp-set-options lr0-$s dynamic-routing- redistribute=lb,nat
+    check ovn-nbctl lsp-add-router-port $s $s-lr0 lr0-$s
+    check ovn-nbctl lrp-add lr1 lr1-$s 00:00:00:00:0$i:02 10.0.$i.2/24
+    check ovn-nbctl lsp-add-router-port $s $s-lr1 lr1-$s
+done
+check ovn-nbctl lr-nat-add lr1 dnat_and_snat 172.16.1.10 192.168.1.10
+check ovn-nbctl lb-add lb1 172.16.1.20:80 192.168.1.20:80
+check ovn-nbctl --wait=sb lr-lb-add lr1 lb1
+
+ovn-sbctl dump-flows lr0 > lr0flows
+AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | grep "172\.16\.1\." | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl +  table=??(lr_in_ip_routing   ), priority=1935 , match=(ip4.dst == 172.16.1.10/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);) +  table=??(lr_in_ip_routing   ), priority=1935 , match=(ip4.dst == 172.16.1.20/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)
+])
+AT_CHECK([grep "lr_in_ip_routing_ecmp" lr0flows | grep "reg0 = 10\.0\." | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.1.2; reg5 = 10.0.1.1; eth.src = 00:00:00:00:01:01; outport = "lr0-a"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.1.2; reg5 = 10.0.1.1; eth.src = 00:00:00:00:01:01; outport = "lr0-a"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.2.2; reg5 = 10.0.2.1; eth.src = 00:00:00:00:02:01; outport = "lr0-b"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.2.2; reg5 = 10.0.2.1; eth.src = 00:00:00:00:02:01; outport = "lr0-b"; reg9[[9]] = 1; next;)
+])
+
+dnl Both members of each group come from the same NAT or LB row.
+nat=$(fetch_column nb:NAT _uuid external_ip=172.16.1.10)
+lb=$(fetch_column nb:Load_Balancer _uuid name=lb1)
+check_row_count Logical_Flow 2 external_ids:stage- name=lr_in_ip_routing_ecmp \
+    external_ids:stage-hint=${nat:0:8}
+check_row_count Logical_Flow 2 external_ids:stage- name=lr_in_ip_routing_ecmp \
+    external_ids:stage-hint=${lb:0:8}
+
+dnl ecmp_ids also checks that the members have distinct ids.
+nat_ids=$(ecmp_ids lr0 172.16.1.10/32)
+lb_ids=$(ecmp_ids lr0 172.16.1.20/32)
+AT_CHECK([test -n "$nat_ids" && test -n "$lb_ids"])
+CHECK_NO_CHANGE_AFTER_RECOMPUTE
+
+dnl A third path adds a member to each group without changing the ids of the
+dnl existing members.
+check ovn-nbctl ls-add c
+check ovn-nbctl lrp-add lr0 lr0-c 00:00:00:00:03:01 10.0.3.1/24
+check ovn-nbctl lrp-set-options lr0-c dynamic-routing- redistribute=lb,nat
+check ovn-nbctl lsp-add-router-port c c-lr0 lr0-c
+check ovn-nbctl lrp-add lr1 lr1-c 00:00:00:00:03:02 10.0.3.2/24
+check ovn-nbctl --wait=sb lsp-add-router-port c c-lr1 lr1-c
+CHECK_NO_CHANGE_AFTER_RECOMPUTE
+
+for old in "$nat_ids 172.16.1.10/32" "$lb_ids 172.16.1.20/32"; do
+    set -- $old
+    new=$(ecmp_ids lr0 $3)
+    AT_CHECK([test "${new%% *}" = "$1"])
+    AT_CHECK([echo "${new#* }" | tr , '\n' | wc -l], [0], [3
+])
+    for m in $(echo $2 | tr , ' '); do
+        AT_CHECK([echo "${new#* }" | tr , '\n' | grep -qx $m])
+    done
+done
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
  OVN_FOR_EACH_NORTHD_NO_HV([
  AT_SETUP([dynamic-routing - LB redistribute uses backend LSP as tracked_port])
  AT_KEYWORDS([dynamic-routing])
@@ -24092,6 +24171,87 @@ done
  OVN_CLEANUP_NORTHD
  AT_CLEANUP
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([Static routes - ECMP ids stable across recompute])
+AT_KEYWORDS([ecmp])
+ovn_start
+
+check ovn-nbctl lr-add lr0
+for i in 1 2 3 4; do
+    check ovn-nbctl lrp-add lr0 lr0-sw$i 00:00:00:00:0$i:01 10.0.$i.1/24
+done
+check ovn-nbctl --wait=sb sync
+
+dnl Add the routes in "reverse" order, the ids must not depend on it.
+for p in 192.168.30.0/24 192.168.20.0/24 192.168.10.0/24; do
+    for i in 3 2 1; do
+        check ovn-nbctl --wait=sb --ecmp lr-route-add lr0 $p 10.0.$i.10
+    done
+done
+CHECK_NO_CHANGE_AFTER_RECOMPUTE
+
+AT_CHECK([ovn-sbctl dump-flows lr0 | grep -w lr_in_ip_routing | grep select | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.10.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>, <mid>);) +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.20.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>, <mid>);) +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.30.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>, <mid>);)
+])
+g10=$(ecmp_ids lr0 192.168.10.0/24)
+g20=$(ecmp_ids lr0 192.168.20.0/24)
+g30=$(ecmp_ids lr0 192.168.30.0/24)
+AT_CHECK([test -n "$g10" && test -n "$g20" && test -n "$g30"])
+AT_CHECK([printf '%s\n' "$g10" "$g20" "$g30" | cut -d' ' -f1 | sort - u | wc -l], [0], [3
+])
+
+dnl Remove a member from the middle of a group with more than 2 members, the
+dnl ids of the other members and groups must not change.
+m2=$(ecmp_member_id lr0 192.168.10.0/24 10.0.2.10)
+check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
+check ovn-nbctl --wait=sb lr-route-del lr0 192.168.10.0/24 10.0.2.10
+check_engine_compute group_ecmp_route incremental
+CHECK_NO_CHANGE_AFTER_RECOMPUTE
+g10_ids=$(echo "${g10#* }" | tr , '\n' | grep -vx "$m2" | paste -sd, -)
+AT_CHECK_UNQUOTED([ecmp_ids lr0 192.168.10.0/24], [0], [${g10%% *} $g10_ids
+])
+AT_CHECK_UNQUOTED([ecmp_ids lr0 192.168.20.0/24], [0], [$g20
+])
+AT_CHECK_UNQUOTED([ecmp_ids lr0 192.168.30.0/24], [0], [$g30
+])
+g10=$(ecmp_ids lr0 192.168.10.0/24)
+
+dnl Remove a whole group and then add a new one, the ids of the other groups
+dnl must not change and the group ids must stay unique.
+check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
+check ovn-nbctl --wait=sb lr-route-del lr0 192.168.20.0/24
+check ovn-nbctl --wait=sb --ecmp lr-route-add lr0 192.168.40.0/24 10.0.1.10 +check ovn-nbctl --wait=sb --ecmp lr-route-add lr0 192.168.40.0/24 10.0.4.10
+check_engine_compute group_ecmp_route incremental
+CHECK_NO_CHANGE_AFTER_RECOMPUTE
+AT_CHECK_UNQUOTED([ecmp_ids lr0 192.168.10.0/24], [0], [$g10
+])
+AT_CHECK_UNQUOTED([ecmp_ids lr0 192.168.30.0/24], [0], [$g30
+])
+g40=$(ecmp_ids lr0 192.168.40.0/24)
+AT_CHECK([test -n "$g40"])
+AT_CHECK([printf '%s\n' "$g10" "$g30" "$g40" | cut -d' ' -f1 | sort - u | wc -l], [0], [3
+])
+
+dnl Once no member is a discard route anymore the group must stop dropping. +route=$(ovn-nbctl --bare --columns _uuid find Logical_Router_Static_Route \
+        ip_prefix="192.168.30.0/24" nexthop="10.0.3.10")
+check ovn-nbctl --wait=sb set Logical_Router_Static_Route $route nexthop=discard +AT_CHECK([ovn-sbctl dump-flows lr0 | grep lr_in_ip_routing_ecmp | grep -c "drop;"], [0], [4
+])
+check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
+check ovn-nbctl --wait=sb set Logical_Router_Static_Route $route nexthop=10.0.3.10
+check_engine_compute group_ecmp_route incremental
+AT_CHECK([ovn-sbctl dump-flows lr0 | grep lr_in_ip_routing_ecmp | grep -c "drop;"], [0], [1
+])
+CHECK_NO_CHANGE_AFTER_RECOMPUTE
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
  OVN_FOR_EACH_NORTHD_NO_HV([
  AT_SETUP([Static routes - ECMP with discard])
  ovn_start
@@ -24106,14 +24266,14 @@ check ovn- nbctl                                                          \
  check ovn-nbctl --wait=sb sync
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep 'lr_in_ip_routing' lr0flows | grep 'select' | ovn_strip_lflows], [0], [dnl -  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.10.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);) +AT_CHECK([grep 'lr_in_ip_routing' lr0flows | grep 'select' | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.10.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)
  ])
-AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0flows | sed -E 's/== [[1-9]]+\)/== \?)/' | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ?), action=(reg0 = 10.0.10.10; reg5 = 10.0.10.1; eth.src = 00:00:00:00:10:01; outport = "lr0-sw1"; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ?), action=(reg0 = 10.0.20.10; reg5 = 10.0.20.1; eth.src = 00:00:00:00:20:01; outport = "lr0-sw2"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.10.10; reg5 = 10.0.10.1; eth.src = 00:00:00:00:10:01; outport = "lr0-sw1"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.20.10; reg5 = 10.0.20.1; eth.src = 00:00:00:00:20:01; outport = "lr0-sw2"; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing_ecmp), priority=150  , match=(reg8[[0..15]] == 0), action=(next;)
  ])
@@ -24124,14 +24284,14 @@ ecmp2=$(fetch_column nb:Logical_Router_Static_Route _uuid nexthop="10.0.20.10")   check ovn-nbctl --wait=sb set Logical_Router_Static_Route $ecmp1 nexthop="discard"
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep 'lr_in_ip_routing' lr0flows | grep 'select' | ovn_strip_lflows], [0], [dnl -  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.10.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);) +AT_CHECK([grep 'lr_in_ip_routing' lr0flows | grep 'select' | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.10.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)
  ])
-AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0flows | sed -E 's/== [[1-9]]+\)/== \?)/' | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ?), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ?), action=(drop;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(drop;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(drop;)     table=??(lr_in_ip_routing_ecmp), priority=150  , match=(reg8[[0..15]] == 0), action=(next;)
  ])
@@ -24139,14 +24299,14 @@ AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0flows | sed -E 's/== [[1-9]]+\)/== \?)   check ovn-nbctl --wait=sb set Logical_Router_Static_Route $ecmp2 nexthop="discard"
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep 'lr_in_ip_routing' lr0flows | grep 'select' | ovn_strip_lflows], [0], [dnl -  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.10.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);) +AT_CHECK([grep 'lr_in_ip_routing' lr0flows | grep 'select' | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.10.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)
  ])
-AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0flows | sed -E 's/== [[1-9]]+\)/== \?)/' | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ?), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ?), action=(drop;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(drop;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(drop;)     table=??(lr_in_ip_routing_ecmp), priority=150  , match=(reg8[[0..15]] == 0), action=(next;)
  ])
@@ -24155,14 +24315,14 @@ check ovn-nbctl set Logical_Router_Static_Route $ecmp1 nexthop="10.0.10.10"   check ovn-nbctl --wait=sb set Logical_Router_Static_Route $ecmp2 nexthop="10.0.20.10"
  ovn-sbctl dump-flows lr0 > lr0flows
-AT_CHECK([grep 'lr_in_ip_routing' lr0flows | grep 'select' | ovn_strip_lflows], [0], [dnl -  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.10.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]] = select(1, 2);) +AT_CHECK([grep 'lr_in_ip_routing' lr0flows | grep 'select' | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl +  table=??(lr_in_ip_routing   ), priority=1840 , match=(reg7 == 0 && ip4.dst == 192.168.10.0/24), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = <gid>; reg8[[16..31]] = select(<mid>, <mid>);)
  ])
-AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0flows | sed -E 's/== [[1-9]]+\)/== \?)/' | ovn_strip_lflows], [0], [dnl +AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0flows | ovn_strip_ecmp_ids | ovn_strip_lflows], [0], [dnl     table=??(lr_in_ip_routing_ecmp), priority=0    , match=(1), action=(drop;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ?), action=(reg0 = 10.0.10.10; reg5 = 10.0.10.1; eth.src = 00:00:00:00:10:01; outport = "lr0-sw1"; reg9[[9]] = 1; next;) -  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == 1 && reg8[[16..31]] == ?), action=(reg0 = 10.0.20.10; reg5 = 10.0.20.1; eth.src = 00:00:00:00:20:01; outport = "lr0-sw2"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.10.10; reg5 = 10.0.10.1; eth.src = 00:00:00:00:10:01; outport = "lr0-sw1"; reg9[[9]] = 1; next;) +  table=??(lr_in_ip_routing_ecmp), priority=100  , match=(reg8[[0..15]] == <gid> && reg8[[16..31]] == <mid>), action=(reg0 = 10.0.20.10; reg5 = 10.0.20.1; eth.src = 00:00:00:00:20:01; outport = "lr0-sw2"; reg9[[9]] = 1; next;)     table=??(lr_in_ip_routing_ecmp), priority=150  , match=(reg8[[0..15]] == 0), action=(next;)
  ])


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

Reply via email to