On Fri, Jul 24, 2026 at 12:41 PM Lucas Vargas Dias <[email protected]>
wrote:

> Until now any change to a logical router's ports fell back to a full
> northd recompute, because lr_changes_can_be_handled() rejected the
> LOGICAL_ROUTER "ports" column and any change to a router port row.
> Enable incremental processing for creation and deletion of regular
> (non-gateway) logical router ports (LRPs), mirroring the router logical
> switch port path added by the preceding commit.
>
> Like a router switch port, an LRP is not self-contained: a full
> recompute assigns a tunnel key and SB Port_Binding in build_ports(),
> populates od->router_ips, and adds a connected route per LRP network.
> The incremental path now performs (and tears down) this work itself:
> lr_port_create()/lr_port_init() mirror the LRP branch of
> join_logical_ports() plus the build_ports() post-processing, and the
> connected routes are added/removed incrementally through the
> routes -> group_ecmp_route -> lflow chain.
>
> The LRP's own flows are regenerated in the lflow port-change handler
> (build_lswitch_and_lrouter_iterate_by_lrp() on op->lflow_ref and
> build_lbnat_lflows_iterate_by_lrp() on op->stateful_lflow_ref), while
> the per-datapath flows (e.g. lr_in_network_id) are regenerated by
> re-tracking the router datapath to the lflow engine.
>
> Dependencies that live outside the LRP's lflow_ref and that this path
> does not keep in sync trigger a fall back to a full recompute
> (lrp_needs_recompute()): distributed gateway ports (gateway_chassis /
> ha_chassis_group), gateway routers, LRP<->LRP peering, NAT, load
> balancers, static or dynamic routing, policies, IPv6 RA, prefix
> delegation, custom route tables, mcast relay, a pre-existing peer
> switch port, or an NB Static_MAC_Binding referencing the port.  Updates
> to an existing LRP also fall back to recompute; only create and delete
> are incremental.
>
> While enabling this, fix a latent bug in the group_ecmp route engine:
> unique_routes_remove() matched a route to delete by prefix hash only, so
> deleting one of several connected routes that share a prefix but differ
> in out_port (e.g. the automatic fe80::/64 link-local route present on
> every LRP) could remove the wrong entry.  Match the out_port exactly on
> deletion.
>
> Add tests covering incremental create/delete of a router port (including
> the resulting connected-route flows) and the recompute fallbacks for the
> distributed-gateway, dynamic-routing, pre-existing-peer and
> modification cases.
>

is this bug easy to recreate? is it possible to add a test for it?


>
> Assisted-by: Claude Opus 4.8, ClaudeCode
> Signed-off-by: Lucas Vargas Dias <[email protected]>
> ---
>  northd/en-group-ecmp-route.c |  29 +-
>  northd/en-lflow.c            |   7 +
>  northd/en-multicast.c        |  11 +
>  northd/en-northd.c           |  66 +++-
>  northd/en-sync-sb.c          |   2 +
>  northd/northd.c              | 572 +++++++++++++++++++++++++++++++++--
>  northd/northd.h              |  27 +-
>  tests/ovn-northd.at          | 111 ++++++-
>  8 files changed, 775 insertions(+), 50 deletions(-)
>
> diff --git a/northd/en-group-ecmp-route.c b/northd/en-group-ecmp-route.c
> index 87dade486..1318aa7c0 100644
> --- a/northd/en-group-ecmp-route.c
> +++ b/northd/en-group-ecmp-route.c
> @@ -180,11 +180,19 @@ unique_routes_destroy(struct hmap *unique_routes)
>      hmap_destroy(unique_routes);
>  }
>
> -/* Remove the unique_routes_node from the group, and return the
> parsed_route
> - * pointed by the removed node. */
> +/* Remove a unique_routes_node from the group, and return the parsed_route
> + * pointed by the removed node.
> + *
> + * 'route->prefix'/plen/is_src_route/source/route_table_id are always
> matched.
> + * When 'exact' is true the output port is matched too: several routes can
> + * share the same prefix (e.g. the IPv6 link-local fe80::/64 connected
> route
> + * present on every router port) and differ only by their output port, so
> a
> + * deletion must target the specific route.  When 'exact' is false any
> route
> + * to the prefix is returned, which is what ECMP-group formation needs
> when a
> + * second next hop for an existing prefix is added. */
>  static const struct parsed_route *
> -unique_routes_remove(struct group_ecmp_datapath *gn,
> -                     const struct parsed_route *route)
> +unique_routes_remove__(struct group_ecmp_datapath *gn,
> +                       const struct parsed_route *route, bool exact)
>  {
>      struct unique_routes_node *ur;
>      HMAP_FOR_EACH_WITH_HASH (ur, hmap_node, route->hash,
> &gn->unique_routes) {
> @@ -192,7 +200,8 @@ unique_routes_remove(struct group_ecmp_datapath *gn,
>              route->plen == ur->route->plen &&
>              route->is_src_route == ur->route->is_src_route &&
>              route->source == ur->route->source &&
> -            route->route_table_id == ur->route->route_table_id) {
> +            route->route_table_id == ur->route->route_table_id &&
> +            (!exact || route->out_port == ur->route->out_port)) {
>              hmap_remove(&gn->unique_routes, &ur->hmap_node);
>              const struct parsed_route *existed_route = ur->route;
>              free(ur);
> @@ -202,6 +211,13 @@ unique_routes_remove(struct group_ecmp_datapath *gn,
>      return NULL;
>  }
>
> +static const struct parsed_route *
> +unique_routes_remove(struct group_ecmp_datapath *gn,
> +                     const struct parsed_route *route)
> +{
> +    return unique_routes_remove__(gn, route, false);
> +}
> +
>  static void
>  ecmp_groups_add_route(struct ecmp_groups_node *group,
>                        const struct parsed_route *route)
> @@ -415,7 +431,8 @@ handle_deleted_route(struct group_ecmp_route_data
> *data,
>          return false;
>      }
>
> -    const struct parsed_route *existing = unique_routes_remove(node, pr);
> +    const struct parsed_route *existing = unique_routes_remove__(node, pr,
> +                                                                 true);
>      if (!existing) {
>          /* The route must be part of an ecmp group. */
>          if (pr->source == ROUTE_SOURCE_CONNECTED) {
> diff --git a/northd/en-lflow.c b/northd/en-lflow.c
> index 8cb987777..99df5f08f 100644
> --- a/northd/en-lflow.c
> +++ b/northd/en-lflow.c
> @@ -169,6 +169,13 @@ lflow_northd_handler(struct engine_node *node,
>          return EN_UNHANDLED;
>      }
>
> +    if (!lflow_handle_northd_lrp_changes(eng_ctx->ovnsb_idl_txn,
> +                                         &northd_data->trk_data.trk_lrps,
> +                                         &lflow_input,
> +                                         lflow_data->lflow_table)) {
> +        return EN_UNHANDLED;
> +    }
> +
>      if (!lflow_handle_northd_lb_changes(
>              eng_ctx->ovnsb_idl_txn, &northd_data->trk_data.trk_lbs,
>              &lflow_input, lflow_data->lflow_table)) {
> diff --git a/northd/en-multicast.c b/northd/en-multicast.c
> index 5148d8840..bfe3c4d92 100644
> --- a/northd/en-multicast.c
> +++ b/northd/en-multicast.c
> @@ -160,6 +160,17 @@ multicast_igmp_northd_handler(struct engine_node
> *node, void *data OVS_UNUSED)
>          return EN_UNHANDLED;
>      }
>
> +    /* A created/deleted logical router port may join or leave the
> router's
> +     * multicast groups; recompute this (cheap) node.  It does not force a
> +     * recompute of the lflow node, which consumes multicast_igmp via its
> own
> +     * incremental handler. */
> +    struct tracked_ovn_ports *trk_lrps = &northd_data->trk_data.trk_lrps;
> +    if (hmapx_count(&trk_lrps->created) ||
> +        hmapx_count(&trk_lrps->updated) ||
> +        hmapx_count(&trk_lrps->deleted)) {
> +        return EN_UNHANDLED;
> +    }
> +
>      /* This node uses the below data from the en_northd engine node.
>       *      - northd_data->lr_datapaths
>       *      - northd_data->ls_ports
> diff --git a/northd/en-northd.c b/northd/en-northd.c
> index 2bee1a996..bda4c9e80 100644
> --- a/northd/en-northd.c
> +++ b/northd/en-northd.c
> @@ -199,15 +199,17 @@ northd_nb_logical_router_handler(struct engine_node
> *node,
>  {
>      struct northd_data *nd = data;
>      struct northd_input input_data;
> +    const struct engine_context *eng_ctx = engine_get_context();
>
>      northd_get_input_data(node, &input_data);
>
> -    if (!northd_handle_lr_changes(&input_data, nd)) {
> +    if (!northd_handle_lr_changes(eng_ctx->ovnsb_idl_txn, &input_data,
> nd)) {
>          return EN_UNHANDLED;
>      }
>
>      if (northd_has_lr_nats_in_tracked_data(&nd->trk_data) ||
>          northd_has_lrouters_in_tracked_data(&nd->trk_data) ||
> +        northd_has_lrps_in_tracked_data(&nd->trk_data) ||
>          northd_has_lr_route_in_tracked_data(&nd->trk_data)) {
>          return EN_HANDLED_UPDATED;
>      }
> @@ -332,23 +334,61 @@ enum engine_input_handler_result
>  routes_northd_change_handler(struct engine_node *node,
>                               void *data OVS_UNUSED)
>  {
> +    struct routes_data *routes_data = data;
>      struct northd_data *northd_data = engine_get_input_data("northd",
> node);
>      if (!northd_has_tracked_data(&northd_data->trk_data)) {
>          return EN_UNHANDLED;
>      }
>
> -    /* This node uses the below data from the en_northd engine node.
> -     * See (lr_stateful_get_input_data())
> -     *   1. northd_data->lr_datapaths
> -     *   2. northd_data->lr_ports
> -     *      This data gets updated when a logical router or logical
> router port
> -     *      is created or deleted.
> -     *      Northd engine node presently falls back to full recompute when
> -     *      this happens and so does this node.
> -     *      Note: When we add I-P to the created/deleted logical routers
> or
> -     *      logical router ports, we need to revisit this handler.
> -     *
> -     */
> +    /* This node uses northd_data->lr_datapaths and northd_data->lr_ports.
> +     * Creating or deleting a regular logical router port changes the set
> of
> +     * directly-connected routes; handle that incrementally.  Any other
> change
> +     * to this data is either irrelevant to routes (e.g. a portless router
> +     * create/delete has no connected routes) or already forced a full
> +     * recompute by the northd node. */
> +    struct tracked_ovn_ports *trk_lrps = &northd_data->trk_data.trk_lrps;
> +    if (hmapx_is_empty(&trk_lrps->created) &&
> +        hmapx_is_empty(&trk_lrps->updated) &&
> +        hmapx_is_empty(&trk_lrps->deleted)) {
> +        return EN_HANDLED_UNCHANGED;
> +    }
> +
> +    /* LRP modifications are not incrementally processed by the northd
> node
> +     * (they force a recompute), so trk_lrps->updated is always empty
> here. */
> +    ovs_assert(hmapx_is_empty(&trk_lrps->updated));
> +
> +    routes_data->tracked = true;
> +
> +    struct hmapx_node *hmapx_node;
> +    struct ovn_port *op;
> +
> +    /* Deleted LRPs: drop their connected routes.  An LRP with N networks
> has
> +     * N connected routes that share the LRP's uuid as source hint, so
> loop
> +     * until none remains. */
> +    HMAPX_FOR_EACH (hmapx_node, &trk_lrps->deleted) {
> +        op = hmapx_node->data;
> +        struct parsed_route *pr;
> +        while ((pr = parsed_route_lookup_by_source(
> +                        ROUTE_SOURCE_CONNECTED, &op->nbrp->header_,
> +                        &routes_data->parsed_routes))) {
> +            hmap_remove(&routes_data->parsed_routes, &pr->key_node);
> +            hmapx_add(&routes_data->trk_data.trk_deleted_parsed_route,
> pr);
> +        }
> +    }
> +
> +    /* Created LRPs: add their connected routes. */
> +    HMAPX_FOR_EACH (hmapx_node, &trk_lrps->created) {
> +        op = hmapx_node->data;
> +        parsed_routes_add_connected(
> +            op->od, op, &routes_data->parsed_routes,
> +            &routes_data->trk_data.trk_crupdated_parsed_route);
> +    }
> +
> +    if
> (!hmapx_is_empty(&routes_data->trk_data.trk_crupdated_parsed_route) ||
> +        !hmapx_is_empty(&routes_data->trk_data.trk_deleted_parsed_route))
> {
> +        return EN_HANDLED_UPDATED;
> +    }
> +
>      return EN_HANDLED_UNCHANGED;
>  }
>
> diff --git a/northd/en-sync-sb.c b/northd/en-sync-sb.c
> index db9cf5cf3..b51ece452 100644
> --- a/northd/en-sync-sb.c
> +++ b/northd/en-sync-sb.c
> @@ -413,6 +413,8 @@ sync_to_sb_pb_northd_handler(struct engine_node *node,
> void *data OVS_UNUSED)
>
>      sync_pbs_for_northd_changed_ovn_ports(&nd->trk_data.trk_lsps,
>                                            &lr_stateful_data->table);
> +    sync_pbs_for_northd_changed_lrps(&nd->trk_data.trk_lrps,
> +                                     &lr_stateful_data->table);
>      return EN_HANDLED_UPDATED;
>  }
>
> diff --git a/northd/northd.c b/northd/northd.c
> index 404ab50a9..eca5d6f0e 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -4363,6 +4363,27 @@ sync_pbs_for_northd_changed_ovn_ports(
>      }
>  }
>
> +/* Set the SB Port_Binding options (peer, dynamic-routing, ...) of
> created and
> + * updated logical router ports.  Deleted LRPs already had their SB row
> removed
> + * by lr_handle_lrp_changes(). */
> +void
> +sync_pbs_for_northd_changed_lrps(
> +    struct tracked_ovn_ports *trk_lrps,
> +    const struct lr_stateful_table *lr_stateful_table)
> +{
> +    struct hmapx_node *hmapx_node;
> +    struct ovn_port *op;
> +
> +    HMAPX_FOR_EACH (hmapx_node, &trk_lrps->created) {
> +        op = hmapx_node->data;
> +        sync_pb_for_lrp(op, lr_stateful_table);
> +    }
> +    HMAPX_FOR_EACH (hmapx_node, &trk_lrps->updated) {
> +        op = hmapx_node->data;
> +        sync_pb_for_lrp(op, lr_stateful_table);
> +    }
> +}
> +
>  void
>  sync_pbs_for_lr_stateful_changes(const struct ovn_datapath *od,
>                                   const struct lr_stateful_table
> *lr_stateful)
> @@ -4652,6 +4673,7 @@ destroy_northd_data_tracked_changes(struct
> northd_data *nd)
>  {
>      struct northd_tracked_data *trk_changes = &nd->trk_data;
>      destroy_tracked_ovn_ports(&trk_changes->trk_lsps);
> +    destroy_tracked_ovn_ports(&trk_changes->trk_lrps);
>      destroy_tracked_lbs(&trk_changes->trk_lbs);
>      hmapx_clear(&trk_changes->trk_nat_lrs);
>      hmapx_clear(&trk_changes->trk_lrs_routes);
> @@ -4675,6 +4697,9 @@ init_northd_tracked_data(struct northd_data *nd)
>      hmapx_init(&trk_data->trk_lsps.created);
>      hmapx_init(&trk_data->trk_lsps.updated);
>      hmapx_init(&trk_data->trk_lsps.deleted);
> +    hmapx_init(&trk_data->trk_lrps.created);
> +    hmapx_init(&trk_data->trk_lrps.updated);
> +    hmapx_init(&trk_data->trk_lrps.deleted);
>      hmapx_init(&trk_data->trk_lbs.crupdated);
>      hmapx_init(&trk_data->trk_lbs.deleted);
>      hmapx_init(&trk_data->trk_nat_lrs);
> @@ -4694,6 +4719,9 @@ destroy_northd_tracked_data(struct northd_data *nd)
>      hmapx_destroy(&trk_data->trk_switches.deleted);
>      hmapx_destroy(&trk_data->trk_lsps.updated);
>      hmapx_destroy(&trk_data->trk_lsps.deleted);
> +    hmapx_destroy(&trk_data->trk_lrps.created);
> +    hmapx_destroy(&trk_data->trk_lrps.updated);
> +    hmapx_destroy(&trk_data->trk_lrps.deleted);
>      hmapx_destroy(&trk_data->trk_lbs.crupdated);
>      hmapx_destroy(&trk_data->trk_lbs.deleted);
>      hmapx_destroy(&trk_data->trk_nat_lrs);
> @@ -5091,6 +5119,220 @@ ls_port_reinit(struct ovn_port *op, struct
> ovsdb_idl_txn *ovnsb_txn,
>                          sbrec_encap_by_ip);
>  }
>
> +/* Find the logical router port 'nbrp' among the ports of the logical
> router
> + * datapath 'od'.  Mirror of ovn_port_find_in_datapath() for LRPs. */
> +static struct ovn_port *
> +ovn_lrp_find_in_datapath(struct ovn_datapath *od,
> +                         const struct nbrec_logical_router_port *nbrp)
> +{
> +    struct ovn_port *op;
> +    HMAP_FOR_EACH_WITH_HASH (op, dp_node, hash_string(nbrp->name, 0),
> +                             &od->ports) {
> +        if (op->nbrp == nbrp && !strcmp(op->key, nbrp->name)) {
> +            return op;
> +        }
> +    }
> +    return NULL;
> +}
> +
> +/* Find a logical switch port of type "router" that peers with the logical
> + * router port 'nbrp' (i.e. its options:router-port names 'nbrp'). */
> +static struct ovn_port *
> +lrp_find_peer_lsp(const struct hmap *ls_ports,
> +                  const struct nbrec_logical_router_port *nbrp)
> +{
> +    struct ovn_port *op;
> +    HMAP_FOR_EACH (op, key_node, ls_ports) {
> +        if (op->nbsp && lsp_is_router(op->nbsp)) {
> +            const char *rp = smap_get(&op->nbsp->options, "router-port");
> +            if (rp && !strcmp(rp, nbrp->name)) {
> +                return op;
> +            }
> +        }
> +    }
> +    return NULL;
> +}
> +
> +/* Symmetric counterpart of router_lsp_needs_recompute(): a logical
> router port
> + * (LRP) is not self-contained either.  A full recompute wires its peer
> + * relationship (to a "router" LSP) and generates flows owned by
> lflow_refs
> + * other than the port's own.  Return true when the LRP pulls in a
> dependency
> + * that this incremental path does not keep in sync, so the caller falls
> back
> + * to a full recompute. */
> +static bool
> +lrp_needs_recompute(struct ovn_datapath *od,
> +                    const struct nbrec_logical_router_port *nbrp,
> +                    const struct hmap *ls_ports,
> +                    const struct nbrec_static_mac_binding_table
> *nb_smb_table)
> +{
> +    /* A Static_MAC_Binding referencing this port is synced to the SB by
> +     * build_static_mac_binding_table(), which only runs on a full northd
> +     * recompute; creating/deleting the port incrementally would leave
> the SB
> +     * Static_MAC_Binding stale.  These are rare, so fall back to
> recompute. */
> +    const struct nbrec_static_mac_binding *nb_smb;
> +    NBREC_STATIC_MAC_BINDING_TABLE_FOR_EACH (nb_smb, nb_smb_table) {
> +        if (!strcmp(nb_smb->logical_port, nbrp->name)) {
> +            return true;
> +        }
> +    }
> +
> +    /* Distributed gateway ports / cr-ports (gateway_chassis,
> ha_chassis_group)
> +     * pull in chassisredirect handling not supported here. */
> +    if (nbrp->n_gateway_chassis || nbrp->ha_chassis_group) {
> +        return true;
> +    }
> +
> +    /* LRP-to-LRP peering, disabled ports, prefix delegation,
> redirect-type and
> +     * per-port route tables are not supported. */
> +    if (nbrp->peer || !lrport_is_enabled(nbrp) ||
> +        smap_get_bool(&nbrp->options, "prefix_delegation", false) ||
> +        smap_get(&nbrp->options, "redirect-type") ||
> +        smap_get(&nbrp->options, "route_table")) {
> +        return true;
> +    }
> +
> +    /* IPv6 RA flows are owned by the port's lflow_ref but also flip
> +     * datapath-level state; keep it simple and fall back. */
> +    if (!smap_is_empty(&nbrp->ipv6_ra_configs)) {
> +        return true;
> +    }
> +
> +    /* Gateway-router / distributed-gateway complexity on this router. */
> +    if (od->is_gw_router || smap_get(&od->nbr->options, "chassis") ||
> +        !vector_is_empty(&od->l3dgw_ports)) {
> +        return true;
> +    }
> +
> +    /* NAT, static routes, route policies, LBs and dynamic routing on this
> +     * router pull in stateful/routable/advertised-route/policy
> dependencies
> +     * that live outside the port's lflow_ref and are not tracked here. */
> +    const struct nbrec_logical_router *nbr = od->nbr;
> +    if (nbr->n_nat || nbr->n_static_routes || nbr->n_policies ||
> +        nbr->n_load_balancer || nbr->n_load_balancer_group) {
> +        return true;
> +    }
> +    if (od->dynamic_routing ||
> +        od->dynamic_routing_redistribute != DRRM_NONE) {
> +        return true;
> +    }
> +
> +    /* mcast relay flips od->mcast_info and the peer switch's
> flood_relay. */
> +    if (od->mcast_info.rtr.relay) {
> +        return true;
> +    }
> +
> +    /* Wiring an already-present peer "router" LSP from the LRP side is
> not
> +     * supported; fall back to recompute.  The common ordering creates
> the LRP
> +     * first (no peer yet), so the peer LSP is wired later by the
> incremental
> +     * LSP path (ls_router_port_wire_peer()). */
> +    if (lrp_find_peer_lsp(ls_ports, nbrp)) {
> +        return true;
> +    }
> +
> +    return false;
> +}
> +
> +/* Initialize a newly created ovn_port for a regular (non-gateway) logical
> + * router port, mirroring the LRP path of join_logical_ports_lrp() plus
> the
> + * tunnel-key and SB port-binding steps of build_ports().  Ownership of
> + * '*lrp_networks' is transferred to 'op'.  'op->nbrp' and 'op->key' must
> be
> + * set.  Returns false (leaving cleanup to the caller) on failure. */
> +static bool
> +lr_port_init(struct ovn_port *op, struct ovsdb_idl_txn *ovnsb_txn,
> +             struct ovn_datapath *od, const struct sbrec_port_binding *sb,
> +             struct lport_addresses *lrp_networks,
> +             struct ovsdb_idl_index *sbrec_chassis_by_name,
> +             struct ovsdb_idl_index *sbrec_chassis_by_hostname,
> +             struct ovsdb_idl_index *sbrec_encap_by_ip)
> +{
> +    op->od = od;
> +    op->lrp_networks = *lrp_networks;
> +    op->prefix_delegation = smap_get_bool(&op->nbrp->options,
> +                                          "prefix_delegation", false);
> +    op->dynamic_routing_redistribute =
> +        parse_dynamic_routing_redistribute(&op->nbrp->options,
> +
>  od->dynamic_routing_redistribute,
> +                                           op->nbrp->name);
> +
> +    for (size_t j = 0; j < op->lrp_networks.n_ipv4_addrs; j++) {
> +        sset_add(&op->od->router_ips,
> op->lrp_networks.ipv4_addrs[j].addr_s);
> +    }
> +    for (size_t j = 0; j < op->lrp_networks.n_ipv6_addrs; j++) {
> +        /* Exclude the LLA. */
> +        if (!in6_is_lla(&op->lrp_networks.ipv6_addrs[j].addr)) {
> +            sset_add(&op->od->router_ips,
> +                     op->lrp_networks.ipv6_addrs[j].addr_s);
> +        }
> +    }
> +
> +    /* Assign explicitly requested tunnel ids first. */
> +    if (!ovn_port_assign_requested_tnl_id(op)) {
> +        return false;
> +    }
> +    /* Keep a nonconflicting tunnel ID that is already assigned. */
> +    if (sb && !op->tunnel_key) {
> +        ovn_port_add_tnlid(op, sb->tunnel_key);
> +    }
> +    /* Assign a new tunnel id if needed. */
> +    if (!ovn_port_allocate_key(op)) {
> +        return false;
> +    }
> +    /* Create the SB port binding, if needed. */
> +    if (sb) {
> +        op->sb = sb;
> +    } else {
> +        op->sb = sbrec_port_binding_insert(ovnsb_txn);
> +        sbrec_port_binding_set_logical_port(op->sb, op->key);
> +    }
> +    /* A regular LRP takes the non-gateway "patch" branch of
> +     * ovn_port_update_sbrec(), so the ha-chassis-group / mirror /
> queue-id
> +     * arguments are unused and passing NULL is safe.  The SB options are
> set
> +     * later by sync_pb_for_lrp(). */
> +    ovn_port_update_sbrec(ovnsb_txn, sbrec_chassis_by_name,
> +                          sbrec_chassis_by_hostname, NULL, NULL,
> +                          sbrec_encap_by_ip, op, NULL, NULL);
> +    return true;
> +}
> +
> +/* Create an ovn_port for a regular logical router port and insert it into
> + * 'lr_ports' and 'od->ports'.  Returns NULL (after cleaning up) on
> failure. */
> +static struct ovn_port *
> +lr_port_create(struct ovsdb_idl_txn *ovnsb_txn, struct hmap *lr_ports,
> +               const char *key,
> +               const struct nbrec_logical_router_port *nbrp,
> +               struct ovn_datapath *od, struct lport_addresses
> *lrp_networks,
> +               struct ovsdb_idl_index *sbrec_chassis_by_name,
> +               struct ovsdb_idl_index *sbrec_chassis_by_hostname,
> +               struct ovsdb_idl_index *sbrec_encap_by_ip)
> +{
> +    struct ovn_port *op = ovn_port_create(lr_ports, key, NULL, nbrp,
> NULL);
> +    hmap_insert(&od->ports, &op->dp_node, hmap_node_hash(&op->key_node));
> +    if (!lr_port_init(op, ovnsb_txn, od, NULL, lrp_networks,
> +                      sbrec_chassis_by_name, sbrec_chassis_by_hostname,
> +                      sbrec_encap_by_ip)) {
> +        ovn_port_destroy(lr_ports, op);
> +        return NULL;
> +    }
> +    ipam_add_lrp_port_addresses(op);
> +    return op;
> +}
> +
> +/* Remove the router-port IPs of 'op' from od->router_ips on deletion. */
> +static void
> +lr_port_remove_router_ips(struct ovn_port *op)
> +{
> +    for (size_t j = 0; j < op->lrp_networks.n_ipv4_addrs; j++) {
> +        sset_find_and_delete(&op->od->router_ips,
> +                             op->lrp_networks.ipv4_addrs[j].addr_s);
> +    }
> +    for (size_t j = 0; j < op->lrp_networks.n_ipv6_addrs; j++) {
> +        if (!in6_is_lla(&op->lrp_networks.ipv6_addrs[j].addr)) {
> +            sset_find_and_delete(&op->od->router_ips,
> +                                 op->lrp_networks.ipv6_addrs[j].addr_s);
> +        }
> +    }
> +}
> +
>  /* Returns true if the logical switch has changes which can be
>   * incrementally handled.
>   * Presently supports i-p for the below changes:
> @@ -5834,6 +6076,7 @@ lr_changes_can_be_handled(const struct
> nbrec_logical_router *lr)
>              if (col == NBREC_LOGICAL_ROUTER_COL_LOAD_BALANCER
>                  || col == NBREC_LOGICAL_ROUTER_COL_LOAD_BALANCER_GROUP
>                  || col == NBREC_LOGICAL_ROUTER_COL_NAT
> +                || col == NBREC_LOGICAL_ROUTER_COL_PORTS
>                  || col == NBREC_LOGICAL_ROUTER_COL_STATIC_ROUTES) {
>                  continue;
>              }
> @@ -5841,14 +6084,12 @@ lr_changes_can_be_handled(const struct
> nbrec_logical_router *lr)
>          }
>      }
>
> +    /* Note: changes to the referenced logical router port rows are
> classified
> +     * per-port in lr_handle_lrp_changes() (created ports are handled;
> modified
> +     * ports fall back to a full recompute). */
> +
>      /* Check if the referenced rows are changed.
>         XXX: Need a better OVSDB IDL interface for this check. */
> -    for (size_t i = 0; i < lr->n_ports; i++) {
> -        if (nbrec_logical_router_port_row_get_seqno(lr->ports[i],
> -                                OVSDB_IDL_CHANGE_MODIFY) > 0) {
> -            return false;
> -        }
> -    }
>      if (lr->copp && nbrec_copp_row_get_seqno(lr->copp,
>                                  OVSDB_IDL_CHANGE_MODIFY) > 0) {
>          return false;
> @@ -5906,6 +6147,131 @@ is_lr_static_routes_changed(const struct
> nbrec_logical_router *nbr)
>             || is_lr_static_routes_seqno_changed(nbr);
>  }
>
> +/* Handles logical router port changes of a changed (created, updated or
> + * deleted) logical router 'changed_lr' with datapath 'od'.  Regular
> + * (non-gateway) LRPs are created and deleted incrementally; anything that
> + * pulls in dependencies not tracked here (see lrp_needs_recompute()) or a
> + * modification of an existing LRP falls back to a full recompute.
> + *
> + * Returns false if any port can't be incrementally handled. */
> +static bool
> +lr_handle_lrp_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
> +                      const struct nbrec_logical_router *changed_lr,
> +                      const struct northd_input *ni, struct northd_data
> *nd,
> +                      struct ovn_datapath *od,
> +                      struct tracked_ovn_ports *trk_lrps)
> +{
> +    bool lr_deleted = nbrec_logical_router_is_deleted(changed_lr);
> +    bool lr_ports_changed = lr_deleted;
> +    if (!nbrec_logical_router_is_updated(changed_lr,
> +                                         NBREC_LOGICAL_ROUTER_COL_PORTS))
> {
> +        for (size_t i = 0; i < changed_lr->n_ports; i++) {
> +            if (nbrec_logical_router_port_row_get_seqno(
> +                    changed_lr->ports[i], OVSDB_IDL_CHANGE_MODIFY) > 0 ||
> +                !ovn_lrp_find_in_datapath(od, changed_lr->ports[i])) {
> +                lr_ports_changed = true;
> +                break;
> +            }
> +        }
> +    } else {
> +        lr_ports_changed = true;
> +    }
> +
> +    if (!lr_ports_changed) {
> +        return true;
> +    }
> +
> +    struct ovn_port *op;
> +    HMAP_FOR_EACH (op, dp_node, &od->ports) {
> +        op->visited = false;
> +    }
> +
> +    /* Create newly added LRPs.  Modifications fall back to recompute. */
> +    if (!lr_deleted) {
> +        for (size_t j = 0; j < changed_lr->n_ports; j++) {
> +            struct nbrec_logical_router_port *new_nbrp =
> changed_lr->ports[j];
> +            op = ovn_lrp_find_in_datapath(od, new_nbrp);
> +
> +            if (!op) {
> +                if (lrp_needs_recompute(od, new_nbrp, &nd->ls_ports,
> +
> ni->nbrec_static_mac_binding_table)) {
> +                    goto fail;
> +                }
> +                struct lport_addresses lrp_networks;
> +                if (!extract_lrp_networks(new_nbrp, &lrp_networks)) {
> +                    goto fail;
> +                }
> +                if (!lrp_networks.n_ipv4_addrs &&
> +                    !lrp_networks.n_ipv6_addrs) {
> +                    /* join_logical_ports_lrp() skips such ports. */
> +                    destroy_lport_addresses(&lrp_networks);
> +                    goto fail;
> +                }
> +                op = lr_port_create(ovnsb_idl_txn, &nd->lr_ports,
> +                                    new_nbrp->name, new_nbrp, od,
> +                                    &lrp_networks,
> +                                    ni->sbrec_chassis_by_name,
> +                                    ni->sbrec_chassis_by_hostname,
> +                                    ni->sbrec_encap_by_ip);
> +                if (!op) {
> +                    goto fail;
> +                }
> +                add_op_to_northd_tracked_ports(&trk_lrps->created, op);
> +                /* Datapath-level router flows that iterate the router's
> ports
> +                 * (e.g. build_lrouter_network_id_flows()) live on
> +                 * od->datapath_lflows; retrack the router so the lflow
> engine
> +                 * regenerates them. */
> +                hmapx_add(&nd->trk_data.trk_routers.crupdated, od);
> +            } else if (nbrec_logical_router_port_row_get_seqno(
> +                           new_nbrp, OVSDB_IDL_CHANGE_MODIFY) > 0) {
> +                /* Re-initializing an LRP (re-wiring its peer, networks,
> ...)
> +                 * is not supported yet. */
> +                goto fail;
> +            }
> +            op->visited = true;
> +        }
> +    }
> +
> +    /* Delete removed LRPs. */
> +    bool lrp_deleted = false;
> +    HMAP_FOR_EACH_SAFE (op, dp_node, &od->ports) {
> +        if (op->visited || !op->nbrp) {
> +            continue;
> +        }
> +        /* cr-ports are synthetic and never appear in nbr->ports; a router
> +         * with one is a distributed gateway router which is out of
> scope. */
> +        if (is_cr_port(op) ||
> +            lrp_needs_recompute(od, op->nbrp, &nd->ls_ports,
> +                                ni->nbrec_static_mac_binding_table)) {
> +            goto fail;
> +        }
> +        lr_port_remove_router_ips(op);
> +        add_op_to_northd_tracked_ports(&trk_lrps->deleted, op);
> +        hmap_remove(&nd->lr_ports, &op->key_node);
> +        hmap_remove(&od->ports, &op->dp_node);
> +        sbrec_port_binding_delete(op->sb);
> +        /* Regenerate the router's datapath-level flows (see the create
> +         * path above). */
> +        hmapx_add(&nd->trk_data.trk_routers.crupdated, od);
> +        lrp_deleted = true;
> +    }
> +
> +    /* Purge SB MAC_Bindings learned on the just-deleted LRPs.  A full
> +     * recompute does this in build_ports() via cleanup_mac_bindings();
> the
> +     * incremental path must do it too, otherwise MAC_Bindings
> referencing a
> +     * deleted logical router port are leaked in the southbound DB. */
> +    if (lrp_deleted) {
> +        cleanup_mac_bindings(ni->sbrec_mac_binding_table,
> +                             &nd->lr_datapaths.datapaths, &nd->lr_ports);
> +    }
> +
> +    return true;
> +
> +fail:
> +    destroy_tracked_ovn_ports(trk_lrps);
> +    return false;
> +}
> +
>  /* Return true if changes are handled incrementally, false otherwise.
>   *
>   * Note: Changes to load balancer and load balancer groups associated with
> @@ -5913,7 +6279,8 @@ is_lr_static_routes_changed(const struct
> nbrec_logical_router *nbr)
>   * handler -  northd_handle_lb_data_changes().
>   * */
>  bool
> -northd_handle_lr_changes(const struct northd_input *ni,
> +northd_handle_lr_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
> +                         const struct northd_input *ni,
>                           struct northd_data *nd)
>  {
>      const struct nbrec_logical_router *changed_lr;
> @@ -5929,9 +6296,9 @@ northd_handle_lr_changes(const struct northd_input
> *ni,
>          const struct ovn_synced_logical_router *synced = node->data;
>          const struct nbrec_logical_router *new_lr = synced->nb;
>
> -        /* If the logical router is create with the below columns set,
> +        /* If the logical router is created with the below columns set,
>           * then we can't handle it in the incremental processor goto
> fail. */
> -        if (new_lr->copp || (new_lr->n_ports > 0)) {
> +        if (new_lr->copp) {
>              goto fail;
>          }
>          if (sparse_array_get(&nd->lr_datapaths.dps, synced->sdp->index)) {
> @@ -5950,6 +6317,12 @@ northd_handle_lr_changes(const struct northd_input
> *ni,
>                                                 od->nbr->name);
>          hmapx_add(&nd->trk_data.trk_nat_lrs,od);
>          hmapx_add(&nd->trk_data.trk_routers.crupdated, od);
> +
> +        /* Create any logical router ports the new router already has. */
> +        if (!lr_handle_lrp_changes(ovnsb_idl_txn, new_lr, ni, nd, od,
> +                                   &nd->trk_data.trk_lrps)) {
> +            goto fail;
> +        }
>      }
>
>      HMAPX_FOR_EACH (node, &ni->synced_lrs->updated) {
> @@ -5957,11 +6330,27 @@ northd_handle_lr_changes(const struct northd_input
> *ni,
>          changed_lr = synced->nb;
>
>          /* Presently only able to handle load balancer,
> -         * load balancer group changes and NAT changes. */
> +         * load balancer group changes, NAT changes and (regular) logical
> +         * router port creation/deletion. */
>          if (!lr_changes_can_be_handled(changed_lr)) {
>              goto fail;
>          }
>
> +        struct ovn_datapath *lrp_od = ovn_datapath_find_(
> +                                &nd->lr_datapaths.datapaths,
> +                                &changed_lr->header_.uuid);
> +        if (!lrp_od) {
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> +            VLOG_WARN_RL(&rl, "Internal error: a tracked updated LR "
> +                        "doesn't exist in lr_datapaths: "UUID_FMT,
> +                        UUID_ARGS(&changed_lr->header_.uuid));
> +            goto fail;
> +        }
> +        if (!lr_handle_lrp_changes(ovnsb_idl_txn, changed_lr, ni, nd,
> lrp_od,
> +                                   &nd->trk_data.trk_lrps)) {
> +            goto fail;
> +        }
> +
>          if (is_lr_nats_changed(changed_lr)) {
>              struct ovn_datapath *od = ovn_datapath_find_(
>                                      &nd->lr_datapaths.datapaths,
> @@ -6036,6 +6425,10 @@ northd_handle_lr_changes(const struct northd_input
> *ni,
>      if (!hmapx_is_empty(&nd->trk_data.trk_lrs_routes)) {
>          nd->trk_data.type |= NORTHD_TRACKED_LR_ROUTES;
>      }
> +    if (!hmapx_is_empty(&nd->trk_data.trk_lrps.created) ||
> +        !hmapx_is_empty(&nd->trk_data.trk_lrps.deleted)) {
> +        nd->trk_data.type |= NORTHD_TRACKED_LR_PORTS;
> +    }
>      if (!hmapx_is_empty(&nd->trk_data.trk_routers.crupdated) ||
>          !hmapx_is_empty(&nd->trk_data.trk_routers.deleted)) {
>          nd->trk_data.type |= NORTHD_TRACKED_ROUTERS;
> @@ -13074,29 +13467,41 @@ parsed_routes_add_static(const struct
> ovn_datapath *od,
>      return pr;
>  }
>
> -static void
> +/* Add the directly-connected routes for the logical router port 'op' to
> + * 'routes'.  When 'trk_crupdated' is non-NULL each newly created
> parsed_route
> + * is added to it (used by the incremental routes handler). */
> +void
>  parsed_routes_add_connected(const struct ovn_datapath *od,
>                              const struct ovn_port *op,
> -                            struct hmap *routes)
> +                            struct hmap *routes,
> +                            struct hmapx *trk_crupdated)
>  {
>      for (size_t i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
>          const struct ipv4_netaddr *addr = &op->lrp_networks.ipv4_addrs[i];
>          struct in6_addr prefix;
>
>          in6_addr_set_mapped_ipv4(&prefix, addr->network);
> -        parsed_route_add(od, NULL, &prefix, addr->plen,
> +        struct parsed_route *pr = parsed_route_add(
> +                         od, NULL, &prefix, addr->plen,
>                           false, addr->addr_s, op, 0, false, false,
>                           false, NULL, ROUTE_SOURCE_CONNECTED,
>                           true, &op->nbrp->header_, NULL, routes);
> +        if (trk_crupdated && pr) {
> +            hmapx_add(trk_crupdated, pr);
> +        }
>      }
>
>      for (size_t i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
>          const struct ipv6_netaddr *addr = &op->lrp_networks.ipv6_addrs[i];
>
> -        parsed_route_add(od, NULL, &addr->network, addr->plen, false,
> +        struct parsed_route *pr = parsed_route_add(
> +                         od, NULL, &addr->network, addr->plen, false,
>                           addr->addr_s, op, 0, false, false, false,
>                           NULL, ROUTE_SOURCE_CONNECTED, true,
>                           &op->nbrp->header_, NULL, routes);
> +        if (trk_crupdated && pr) {
> +            hmapx_add(trk_crupdated, pr);
> +        }
>      }
>  }
>
> @@ -13114,7 +13519,7 @@ build_parsed_routes(const struct ovn_datapath *od,
> const struct hmap *lr_ports,
>
>      const struct ovn_port *op;
>      HMAP_FOR_EACH (op, dp_node, &od->ports) {
> -        parsed_routes_add_connected(od, op, routes);
> +        parsed_routes_add_connected(od, op, routes, NULL);
>      }
>  }
>
> @@ -15707,11 +16112,22 @@ static void
>  build_route_flows_for_lrouter(
>          struct ovn_datapath *od, struct lflow_table *lflows,
>          const struct group_ecmp_route_data *route_data,
> -        struct simap *route_tables, const struct sset *bfd_ports)
> +        struct simap *route_tables, const struct sset *bfd_ports,
> +        bool skip_data_route_flows)
>  {
>      ovs_assert(od->nbr);
> +    /* The default route drop flows are owned by od->datapath_lflows. */
>      build_default_route_flows_for_lrouter(od, lflows, route_tables);
>
> +    /* The per-route flows are owned by the group_ecmp_route node's
> +     * lflow_ref, which is managed incrementally by
> +     * lflow_group_ecmp_route_change_handler().  Skip them when
> regenerating a
> +     * router's datapath flows incrementally to avoid double-adding them
> to
> +     * that ref. */
> +    if (skip_data_route_flows) {
> +        return;
> +    }
> +
>      const struct group_ecmp_datapath *datapath_node =
>          group_ecmp_datapath_lookup(route_data, od);
>      if (!datapath_node) {
> @@ -20327,6 +20743,14 @@ struct lswitch_flow_build_info {
>      struct hmap *route_policies;
>      struct simap *route_tables;
>      const struct sbrec_acl_id_table *sbrec_acl_id_table;
> +
> +    /* When true, build_lswitch_and_lrouter_iterate_by_lr() skips the
> logical
> +     * router route flows.  Those flows are owned by the group_ecmp_route
> +     * node's lflow_ref, which is managed incrementally by
> +     * lflow_group_ecmp_route_change_handler(); regenerating them here
> (e.g.
> +     * when a router is retracked because its ports changed) would
> double-add
> +     * them to that ref and corrupt its refcounts. */
> +    bool skip_route_flows;
>  };
>
>  /* Helper function to combine all lflow generation which is iterated by
> @@ -20387,7 +20811,7 @@ build_lswitch_and_lrouter_iterate_by_lr(struct
> ovn_datapath *od,
>                                             od->datapath_lflows);
>      build_route_flows_for_lrouter(od, lsi->lflows,
>                                    lsi->route_data, lsi->route_tables,
> -                                  lsi->bfd_ports);
> +                                  lsi->bfd_ports, lsi->skip_route_flows);
>      build_mcast_lookup_flows_for_lrouter(od, lsi->lflows, &lsi->match,
>                                           od->datapath_lflows);
>      build_ingress_policy_flows_for_lrouter(od, lsi->lflows, lsi->lr_ports,
> @@ -21077,12 +21501,18 @@ lflow_handle_northd_lr_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>      }
>
>      struct lswitch_flow_build_info lsi = {
> +        .ls_ports = lflow_input->ls_ports,
> +        .lr_ports = lflow_input->lr_ports,
>          .lr_datapaths = lflow_input->lr_datapaths,
>          .lr_stateful_table = lflow_input->lr_stateful_table,
> +        .meter_groups = lflow_input->meter_groups,
> +        .bfd_ports = lflow_input->bfd_ports,
> +        .features = lflow_input->features,
>          .lflows = lflows,
>          .route_data = lflow_input->route_data,
>          .route_tables = lflow_input->route_tables,
>          .route_policies = lflow_input->route_policies,
> +        .skip_route_flows = true,
>          .match = DS_EMPTY_INITIALIZER,
>          .actions = DS_EMPTY_INITIALIZER,
>      };
> @@ -21286,6 +21716,114 @@ lflow_handle_northd_port_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>      return handled;
>  }
>
> +/* Regenerate the logical flows for created and deleted logical router
> ports
> + * tracked in 'trk_lrps'.  Mirrors lflow_handle_northd_port_changes() but
> for
> + * LRPs: their flows live on op->lflow_ref (per-port routing/ARP/...
> flows via
> + * build_lswitch_and_lrouter_iterate_by_lrp()) and op->stateful_lflow_ref
> + * (LB/NAT flows via build_lbnat_lflows_iterate_by_lrp()). */
> +bool
> +lflow_handle_northd_lrp_changes(struct ovsdb_idl_txn *ovnsb_txn,
> +                                struct tracked_ovn_ports *trk_lrps,
> +                                struct lflow_input *lflow_input,
> +                                struct lflow_table *lflows)
> +{
> +    struct hmapx_node *hmapx_node;
> +    struct ovn_port *op;
> +
> +    struct lswitch_flow_build_info lsi = {
> +        .ls_ports = lflow_input->ls_ports,
> +        .lr_ports = lflow_input->lr_ports,
> +        .lr_datapaths = lflow_input->lr_datapaths,
> +        .lr_stateful_table = lflow_input->lr_stateful_table,
> +        .meter_groups = lflow_input->meter_groups,
> +        .bfd_ports = lflow_input->bfd_ports,
> +        .lflows = lflows,
> +        .match = DS_EMPTY_INITIALIZER,
> +        .actions = DS_EMPTY_INITIALIZER,
> +    };
> +
> +    HMAPX_FOR_EACH (hmapx_node, &trk_lrps->deleted) {
> +        op = hmapx_node->data;
> +        ovs_assert(op->nbrp);
> +        bool handled = lflow_ref_resync_flows(
> +            op->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
> +            lflow_input->ovn_internal_version_changed,
> +            lflow_input->sbrec_logical_flow_table,
> +            lflow_input->sbrec_logical_dp_group_table);
> +        if (handled) {
> +            handled = lflow_ref_resync_flows(
> +                op->stateful_lflow_ref, lflows, ovnsb_txn,
> lflow_input->dps,
> +                lflow_input->ovn_internal_version_changed,
> +                lflow_input->sbrec_logical_flow_table,
> +                lflow_input->sbrec_logical_dp_group_table);
> +        }
> +        if (!handled) {
> +            goto out;
> +        }
> +    }
> +
> +    HMAPX_FOR_EACH (hmapx_node, &trk_lrps->updated) {
>

trk_lrps->updated should always be empty, should we enforce the invariant
here too? It is enforced in routes_northd_change_handler() with
ovs_assert(hmapx_is_empty(&trk_lrps->updated)) but I think we should in
this function to catch future breakage. At the very least, add a comment

+        op = hmapx_node->data;
> +        ovs_assert(op->nbrp);
> +        lflow_ref_unlink_lflows(op->lflow_ref);
> +        build_lswitch_and_lrouter_iterate_by_lrp(op, &lsi);
> +        bool handled = lflow_ref_sync_lflows(
> +            op->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
> +            lflow_input->ovn_internal_version_changed,
> +            lflow_input->sbrec_logical_flow_table,
> +            lflow_input->sbrec_logical_dp_group_table);
> +        if (handled) {
> +            lflow_ref_unlink_lflows(op->stateful_lflow_ref);
> +            build_lbnat_lflows_iterate_by_lrp(
> +                op, lflow_input->lr_stateful_table,
> +                lflow_input->meter_groups, lflow_input->bfd_ports,
> +                &lsi.match, &lsi.actions, lflows);
> +            handled = lflow_ref_sync_lflows(
> +                op->stateful_lflow_ref, lflows, ovnsb_txn,
> lflow_input->dps,
> +                lflow_input->ovn_internal_version_changed,
> +                lflow_input->sbrec_logical_flow_table,
> +                lflow_input->sbrec_logical_dp_group_table);
> +        }
> +        if (!handled) {
> +            goto out;
> +        }
> +    }
> +
> +    HMAPX_FOR_EACH (hmapx_node, &trk_lrps->created) {
> +        op = hmapx_node->data;
> +        ovs_assert(op->nbrp);
> +        build_lswitch_and_lrouter_iterate_by_lrp(op, &lsi);
> +        bool handled = lflow_ref_sync_lflows(
> +            op->lflow_ref, lflows, ovnsb_txn, lflow_input->dps,
> +            lflow_input->ovn_internal_version_changed,
> +            lflow_input->sbrec_logical_flow_table,
> +            lflow_input->sbrec_logical_dp_group_table);
> +        if (handled) {
> +            build_lbnat_lflows_iterate_by_lrp(
> +                op, lflow_input->lr_stateful_table,
> +                lflow_input->meter_groups, lflow_input->bfd_ports,
> +                &lsi.match, &lsi.actions, lflows);
> +            handled = lflow_ref_sync_lflows(
> +                op->stateful_lflow_ref, lflows, ovnsb_txn,
> lflow_input->dps,
> +                lflow_input->ovn_internal_version_changed,
> +                lflow_input->sbrec_logical_flow_table,
> +                lflow_input->sbrec_logical_dp_group_table);
> +        }
> +        if (!handled) {
> +            goto out;
> +        }
> +    }
> +
> +    ds_destroy(&lsi.match);
> +    ds_destroy(&lsi.actions);
> +    return true;
> +
> +out:
> +    ds_destroy(&lsi.match);
> +    ds_destroy(&lsi.actions);
> +    return false;
> +}
> +
>  bool
>  lflow_handle_northd_lb_changes(struct ovsdb_idl_txn *ovnsb_txn,
>                                 struct tracked_lbs *trk_lbs,
> diff --git a/northd/northd.h b/northd/northd.h
> index d27f519d6..61546bdd2 100644
> --- a/northd/northd.h
> +++ b/northd/northd.h
> @@ -160,6 +160,7 @@ enum northd_tracked_data_type {
>      NORTHD_TRACKED_SWITCHES = (1 << 5),
>      NORTHD_TRACKED_ROUTERS  = (1 << 6),
>      NORTHD_TRACKED_LR_ROUTES = (1 << 7),
> +    NORTHD_TRACKED_LR_PORTS = (1 << 8),
>  };
>
>  /* Track what's changed in the northd engine node.
> @@ -171,6 +172,10 @@ struct northd_tracked_data {
>      struct tracked_dps trk_switches;
>      struct tracked_dps trk_routers;
>      struct tracked_ovn_ports trk_lsps;
> +
> +    /* Tracked created/updated/deleted logical router ports.
> +     * hmapx node data is 'struct ovn_port *'. */
> +    struct tracked_ovn_ports trk_lrps;
>      struct tracked_lbs trk_lbs;
>
>      /* Tracked logical routers whose NATs have changed.
> @@ -914,6 +919,11 @@ struct parsed_route *parsed_routes_add_static(
>      struct hmap *routes, struct simap *route_tables,
>      struct hmap *bfd_active_connections);
>
> +void parsed_routes_add_connected(const struct ovn_datapath *od,
> +                                 const struct ovn_port *op,
> +                                 struct hmap *routes,
> +                                 struct hmapx *trk_crupdated);
> +
>  struct svc_monitors_map_data {
>      const struct hmap *local_svc_monitors_map;
>      const struct hmap *ic_learned_svc_monitors_map;
> @@ -939,7 +949,8 @@ void ovnsb_db_run(struct ovsdb_idl_txn *ovnsb_txn,
>  bool northd_handle_ls_changes(struct ovsdb_idl_txn *,
>                                const struct northd_input *,
>                                struct northd_data *);
> -bool northd_handle_lr_changes(const struct northd_input *,
> +bool northd_handle_lr_changes(struct ovsdb_idl_txn *,
> +                              const struct northd_input *,
>                                struct northd_data *);
>  bool northd_handle_pgs_acl_changes(const struct northd_input *ni,
>                                     struct northd_data *nd);
> @@ -994,6 +1005,10 @@ bool lflow_handle_northd_port_changes(struct
> ovsdb_idl_txn *ovnsb_txn,
>                                        struct tracked_ovn_ports *,
>                                        struct lflow_input *,
>                                        struct lflow_table *lflows);
> +bool lflow_handle_northd_lrp_changes(struct ovsdb_idl_txn *ovnsb_txn,
> +                                     struct tracked_ovn_ports *,
> +                                     struct lflow_input *,
> +                                     struct lflow_table *lflows);
>  bool lflow_handle_northd_lb_changes(struct ovsdb_idl_txn *ovnsb_txn,
>                                      struct tracked_lbs *,
>                                      struct lflow_input *,
> @@ -1045,6 +1060,10 @@ void sync_pbs_for_northd_changed_ovn_ports(
>      struct tracked_ovn_ports *,
>      const struct lr_stateful_table *);
>
> +void sync_pbs_for_northd_changed_lrps(
> +    struct tracked_ovn_ports *,
> +    const struct lr_stateful_table *);
> +
>  void sync_pbs_for_lr_stateful_changes(
>      const struct ovn_datapath *od,
>      const struct lr_stateful_table *lr_stateful);
> @@ -1066,6 +1085,12 @@ northd_has_lsps_in_tracked_data(struct
> northd_tracked_data *trk_nd_changes)
>      return trk_nd_changes->type & NORTHD_TRACKED_PORTS;
>  }
>
> +static inline bool
> +northd_has_lrps_in_tracked_data(struct northd_tracked_data
> *trk_nd_changes)
> +{
> +    return trk_nd_changes->type & NORTHD_TRACKED_LR_PORTS;
> +}
> +
>  static inline bool
>  northd_has_lr_nats_in_tracked_data(struct northd_tracked_data
> *trk_nd_changes)
>  {
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 13df2b712..9a0ed2f01 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -12411,6 +12411,95 @@ CHECK_NO_CHANGE_AFTER_RECOMPUTE
>  OVN_CLEANUP_NORTHD
>  AT_CLEANUP
>
> +AT_SETUP([Logical router port incremental processing])
> +AT_KEYWORDS([incremental processing])
> +ovn_start
> +
> +check ovn-nbctl ls-add sw0
> +check ovn-nbctl --wait=sb lr-add lr0
> +
> +# Adding a regular router port to a bare router (no peer switch port yet)
> is
> +# incrementally processed.
> +check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
> +check ovn-nbctl --wait=sb lrp-add lr0 lr0-sw0 00:00:00:00:ff:01
> 10.0.0.1/24
> +check_engine_compute northd incremental
> +check_engine_compute lflow incremental
> +
> +# The router port's SB Port_Binding was created.
> +AT_CHECK([ovn-sbctl get port_binding lr0-sw0 type], [0], [dnl
> +patch
> +])
> +# The directly-connected route flow is present on the router.
> +AT_CHECK([ovn-sbctl dump-flows lr0 | grep -c 'ip4.dst == 10.0.0.0/24'],
> [0],
> +    [1
> +])
> +CHECK_NO_CHANGE_AFTER_RECOMPUTE
> +
> +# Adding a second regular router port is also incremental.
> +check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
> +check ovn-nbctl --wait=sb lrp-add lr0 lr0-sw1 00:00:00:00:ff:02
> 20.0.0.1/24
> +check_engine_compute northd incremental
> +check_engine_compute lflow incremental
> +CHECK_NO_CHANGE_AFTER_RECOMPUTE
> +
> +# Deleting a router port is incrementally processed.
> +check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
> +check ovn-nbctl --wait=sb lrp-del lr0-sw1
> +check_engine_compute northd incremental
> +check_engine_compute lflow incremental
> +AT_CHECK([ovn-sbctl find port_binding logical_port=lr0-sw1], [0], [])
> +AT_CHECK([ovn-sbctl dump-flows lr0 | grep -c 'ip4.dst == 20.0.0.0/24'],
> [1],
> +    [0
> +])
> +CHECK_NO_CHANGE_AFTER_RECOMPUTE
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +
> +AT_SETUP([Logical router port incremental processing fallback cases])
> +AT_KEYWORDS([incremental processing])
> +ovn_start
> +
> +check ovn-sbctl chassis-add gw1 geneve 127.0.0.1
> +
> +# Adding a distributed gateway port falls back to recompute (cr-port).
> +check ovn-nbctl --wait=sb lr-add lr0
> +check ovn-nbctl ls-add public
> +check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
> +check ovn-nbctl lrp-add lr0 lr0-public 00:00:20:20:12:13 172.168.0.100/24
> +check ovn-nbctl --wait=sb lrp-set-gateway-chassis lr0-public gw1 20
> +check_engine_compute northd recompute
> +CHECK_NO_CHANGE_AFTER_RECOMPUTE
> +
> +# Adding a router port to a dynamic-routing router falls back to
> recompute.
> +check ovn-nbctl --wait=sb lr-add lr1 \
> +    -- set Logical_Router lr1 options:dynamic-routing=true
> +check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
> +check ovn-nbctl --wait=sb lrp-add lr1 lr1-sw0 00:00:00:00:ff:11
> 10.1.0.1/24
> +check_engine_compute northd recompute
> +CHECK_NO_CHANGE_AFTER_RECOMPUTE
> +
> +# Adding a router port whose peer "router" LSP already exists falls back
> to
> +# recompute (wiring the peer from the LRP side is not supported).
> +check ovn-nbctl --wait=sb lr-add lr2
> +check ovn-nbctl ls-add sw2
> +check ovn-nbctl lsp-add sw2 sw2-lr2
> +check ovn-nbctl lsp-set-type sw2-lr2 router
> +check ovn-nbctl --wait=sb lsp-set-options sw2-lr2 router-port=lr2-sw2
> +check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
> +check ovn-nbctl --wait=sb lrp-add lr2 lr2-sw2 00:00:00:00:ff:21
> 10.2.0.1/24
> +check_engine_compute northd recompute
> +CHECK_NO_CHANGE_AFTER_RECOMPUTE
> +
> +# Modifying an existing router port falls back to recompute.
> +check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
> +check ovn-nbctl --wait=sb set logical_router_port lr2-sw2 options:foo=bar
> +check_engine_compute northd recompute
> +CHECK_NO_CHANGE_AFTER_RECOMPUTE
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +
>  OVN_FOR_EACH_NORTHD_NO_HV([
>  AT_SETUP([SB Port binding incremental processing])
>  ovn_start
> @@ -12480,13 +12569,14 @@ check ovn-nbctl --wait=sb lsp-set-options e1
> foo=bar
>  check_recompute_counter 1 1
>  CHECK_NO_CHANGE_AFTER_RECOMPUTE
>
> -# Test lrp
> +# Test lrp.  Adding a regular router port to a bare router with no peer
> switch
> +# port is incrementally processed.
>  check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
>  check ovn-nbctl --wait=sb lrp-add lr0 lrp 00:00:02:01:02:03 10.0.0.1/24
> -check_recompute_counter <http://10.0.0.1/24-check_recompute_counter> 1 1
> +check_recompute_counter 0 0
>  CHECK_NO_CHANGE_AFTER_RECOMPUTE
>
> -# Set some options on 'lrp'.  northd should only recompute once.
> +# Set some options on 'lrp'.  A router-port modification falls back to
> recompute.
>  check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
>  check ovn-nbctl --wait=sb lrp-set-options lrp route_table=rtb-1
>  check_recompute_counter 1 1
> @@ -13846,18 +13936,13 @@ check_engine_stats lflow norecompute compute
>  CHECK_NO_CHANGE_AFTER_RECOMPUTE
>
>  check ovn-nbctl --wait=sb lr-add lr0
> -# Adding a logical router port should result in recompute
> +# Adding a regular logical router port to a bare router (no peer switch
> port
> +# yet) is incrementally processed.
>  check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
>  check ovn-nbctl --wait=sb lrp-add lr0 lr0-sw0 00:00:00:00:ff:01
> 10.0.0.1/24
> -# for northd engine there will be both recompute and compute
> -# first it will be recompute to handle lr0-sw0 and then a compute
> -# for the SB port binding change.
> -check_engine_stats northd recompute compute
> -check_engine_stats lr_nat recompute nocompute
> -check_engine_stats lr_stateful recompute nocompute
> -check_engine_stats sync_to_sb_pb recompute nocompute
> -check_engine_stats sync_to_sb_lb recompute nocompute
> -check_engine_stats lflow recompute nocompute
> +check_engine_compute northd incremental
> +check_engine_compute lflow incremental
> +check_engine_stats sync_to_sb_pb norecompute compute
>  CHECK_NO_CHANGE_AFTER_RECOMPUTE
>
>  check ovn-nbctl lsp-add sw0 sw0-lr0
> --
> 2.43.0
>
>
> --
>
>
>
>
> _'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
>
>
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to