Hi Rosemarie,
Thanks for your review.

Em sex., 16 de out. de 2026 às 17:11, Rosemarie O'Riorden <
[email protected]> escreveu:

> Hi Lucas,
>
> Thanks so much for the new version! Overall I like the design. Since it
> is a complete rework and thus entirely new code, I have some comments
> below.
>
> On 10/15/26 4:02 PM, 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]>
> > ---
> > 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 | 247 +++++++++++++++++++++++++---
> >   northd/en-group-ecmp-route.h |   1 -
> >   tests/ovn-macros.at          |  34 ++++
> >   tests/ovn-northd.at          | 300 +++++++++++++++++++++++++++--------
> >   4 files changed, 486 insertions(+), 96 deletions(-)
> >
> > diff --git a/northd/en-group-ecmp-route.c b/northd/en-group-ecmp-route.c
> > index aca197318..bf553dded 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"
> >
> > @@ -228,14 +229,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 +243,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 +268,221 @@ ecmp_groups_remove_route(struct ecmp_groups_node
> *group,
> >       return NULL;
> >   }
> >
> > +/* NAT and LB routes for the same prefix share a group, see
> > + * route_sources_ecmp_compatible(). */
> > +static enum route_source
> > +ecmp_group_source_class(enum route_source source)
> > +{
> > +    return source == ROUTE_SOURCE_LB ? ROUTE_SOURCE_NAT : source;
>
> So, this function relies follows the rules of
> route_sources_ecmp_compatible(), which makes them independent yet
> dependent in a way, because whenever someone updates one they also have
> to update the other... I think it would make sense to make one actually
> call the other or something like that to logically intertwine them, to
> make it so only one function has to be changed in that case. Because
> they're quite simple.
>

I Agree,


> I guess the same is kind of true of ecmp_group_hash() and
> ecmp_group_hash(), that they need to stay consistent and updates need to
> be made in two separate places. But I don't see any way to easily
> logically link them so oh well.
>

I didn't find a simple way to link them either, so I added a comment to
each one pointing to the other.


>
> > +}
> > +
> > +/* Hashes the key of 'eg', which is unique within a datapath, see
> > + * ecmp_groups_find().  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)
> > +{
> > +    uint32_t hash = uuid_hash(&route->source_hint->uuid);
> > +    if (route->nexthop) {
> > +        hash = hash_bytes(route->nexthop, sizeof *route->nexthop, hash);
> > +    }
> > +    if (route->out_port) {
> > +        hash = hash_string(route->out_port->key, hash);
> > +    }
>
> Shouldn't the hash be finished here like it is in the other hashing
> function?
>
> hash_bytes() and hash_string() already call hash_finish(), but uuid_hash()
just returns the first word of the UUID.
So a route with no nexthop and no output port ended up with an unmixed
hash. I switched to hash_bytes() over
the whole UUID, so every path now ends with a finished hash, and all of the
UUID is used.

> +    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_group_update_ids(struct ecmp_groups_node *group)
> > +ecmp_assign_ids(const uint32_t *hashes, uint16_t *ids, size_t n)
> >   {
> > +    struct hmap slots = HMAP_INITIALIZER(&slots);
> > +    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);
> > +                ecmp_id_slot_get(&slots, id)->n_claims++;
> > +            }
> > +        }
> > +        for (size_t i = 0; i < n; i++) {
> > +            if (!ids[i]) {
> > +                uint16_t id = ecmp_id_from_hash(hashes[i], round);
>
> Nit: The id values are being calculated twice per round. This could be
> done at the beginning of each round. Although it's not a huge deal
> because since collisions are so unlikely there will usually only be one
> round. But you could still call it once per round rather than twice.
>

Fixed. The first pass now keeps the slot each element claims, and the
second pass reuses
it, which also avoids the second lookup.

I'll send these in v5.

Regards,
Lucas


> > +                slot = ecmp_id_slot_get(&slots, id);
> > +                if (!slot->taken && slot->n_claims == 1) {
> > +                    slot->taken = true;
> > +                    ids[i] = id;
> > +                    n_left--;
> > +                }
> > +            }
> > +        }
> > +        HMAP_FOR_EACH (slot, hmap_node, &slots) {
> > +            slot->n_claims = 0;
> > +        }
> > +    }
> > +
> > +    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_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) {
> > -        er->id = i++;
> > +        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 = ids[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);
> > +        }
> >       }
> > -    group->route_count = i;
> > +}
> > +
> > +/* 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;
> > @@ -396,6 +591,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 +671,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 +679,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 +731,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 +784,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 +841,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;)
> >   ])
> >
>
>

-- 




_‘Esta mensagem é direcionada apenas para os endereços constantes no 
cabeçalho inicial. Se você não está listado nos endereços constantes no 
cabeçalho, pedimos-lhe que desconsidere completamente o conteúdo dessa 
mensagem e cuja cópia, encaminhamento e/ou execução das ações citadas estão 
imediatamente anuladas e proibidas’._


* **‘Apesar do Magazine Luiza tomar 
todas as precauções razoáveis para assegurar que nenhum vírus esteja 
presente nesse e-mail, a empresa não poderá aceitar a responsabilidade por 
quaisquer perdas ou danos causados por esse e-mail ou por seus anexos’.*



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

Reply via email to