Recheck-request: github-robot-_Build_and_Test

On Friday, June 12th, 2026 at 6:35 AM, Dmitrii Shcherbakov 
<[email protected]> wrote:

> Replace the single Advertised_Route per VIP (tracked_port = peer LRP,
> a chassis-unbound patch port) with one row per (VIP, backend LSP).
> The backend LSP comes from ip_port_mappings, so it is
> chassis-resident wherever the backend is bound.
> 
> For LBs configured with ip_port_mappings, also populate three new
> optional columns on the emitted Advertised_Route rows:
> 
>   - tracked_service_ip       = backend IP
>   - tracked_service_port     = backend L4 port
>   - tracked_service_protocol = LB protocol (tcp/udp/icmp only,
>                                 matching the Service_Monitor.protocol
>                                 enum)
> 
> For LBs whose protocol is outside the Service_Monitor enum (SCTP
> today), the entire selector is left unset on the row, not just the
> protocol column.  A partial selector (ip+port without protocol) could
> match an unrelated Service_Monitor row on the same (ip, port) for a
> different protocol, so the whole tuple is left unset to take the row
> out of controller-side gating entirely.
> 
> The tuple matches what northd uses when creating Service_Monitor rows
> for the same backend, so a controller-side join on
> (logical_port, tracked_service_ip, tracked_service_port,
> tracked_service_protocol) selects the specific Service_Monitor row for
> this VIP.  One backend LSP can participate in several LBs whose health
> states are independent (e.g. one pod backing two K8s Services).
> 
> The per-backend split also means dynamic-routing-redistribute-local-only
> now correctly restricts kernel-route installation to chassis that host
> at least one backend for the VIP.
> 
> SB schema changes: three new optional columns (min-0) in
> Advertised_Route for safe rolling upgrades.  Old northd leaves them
> absent, old controller ignores them.  The unique index is extended to
> include the three selector columns, so multiple rows for the same VIP
> and tracked_port but different selectors are now distinct. The schema
> version is bumped to 21.9.0.
> 
> Also document the "logical-switch-port" value in the
> Service_Monitor.type column description.
> 
> Backends with ip_port_mappings emit per-backend rows. Backends without
> a mapping are skipped. When no backend has a mapping, one
> Advertised_Route covers the VIP with tracked_port set to the peer LRP
> and all three selector columns unset.
> 
> Signed-off-by: Dmitrii Shcherbakov <[email protected]>
> ---
>  northd/en-advertised-route-sync.c | 283 +++++++++++++++++++++++---
>  northd/lb.h                       |  14 ++
>  ovn-sb.ovsschema                  |  18 +-
>  ovn-sb.xml                        |  56 ++++-
>  tests/ovn-northd.at               | 327 ++++++++++++++++++++++++++++++
>  5 files changed, 663 insertions(+), 35 deletions(-)
> 
> diff --git a/northd/en-advertised-route-sync.c 
> b/northd/en-advertised-route-sync.c
> index fa8bcd697..075382625 100644
> --- a/northd/en-advertised-route-sync.c
> +++ b/northd/en-advertised-route-sync.c
> @@ -23,7 +23,11 @@
>  #include "en-lr-stateful.h"
>  #include "lb.h"
>  #include "openvswitch/hmap.h"
> +#include "openvswitch/vlog.h"
>  #include "ovn-util.h"
> +#include "util.h"
> +
> +VLOG_DEFINE_THIS_MODULE(en_advertised_route_sync);
>  
>  struct ar_entry {
>      struct hmap_node hmap_node;
> @@ -36,6 +40,16 @@ struct ar_entry {
>      const struct ovn_port *tracked_port; /* If set, the port whose chassis
>                                            * advertises this route with a
>                                            * higher priority. */
> +    /* Optional backend service selector. Populated for LB-derived routes
> +     * when northd has per-backend information (ip_port_mappings on the
> +     * Load_Balancer). All three fields must be set together or all left
> +     * unset: a partial selector could match an unrelated Service_Monitor
> +     * row, so the entire tuple is omitted when the LB protocol is not
> +     * accepted by lb_service_monitor_protocol_supported(). */
> +    char *tracked_service_ip;
> +    int64_t tracked_service_port;
> +    bool has_tracked_service_port;
> +    char *tracked_service_protocol;
>      enum route_source source;
>  };
>  
> @@ -74,12 +88,23 @@ ar_entry_add(struct hmap *routes, const struct 
> ovn_datapath *od,
>                                 tracked_port, source);
>  }
>  
> +/* Find an ar_entry whose (datapath, logical_port, ip_prefix,
> + * tracked_port, tracked_service_ip, tracked_service_port,
> + * tracked_service_protocol) tuple matches the full key. The SB
> + * Advertised_Route unique index includes the service selector
> + * columns, so multiple rows for the same VIP IP and
> + * backend LSP are allowed when they differ by per-backend
> + * selector, so ar_entry_find must compare the full key. */
>  static struct ar_entry *
>  ar_entry_find(struct hmap *route_map,
>                const struct sbrec_datapath_binding *sb_db,
>                const struct sbrec_port_binding *logical_port,
>                const char *ip_prefix,
> -              const struct sbrec_port_binding *tracked_port)
> +              const struct sbrec_port_binding *tracked_port,
> +              const char *tracked_service_ip,
> +              bool has_tracked_service_port,
> +              int64_t tracked_service_port,
> +              const char *tracked_service_protocol)
>  {
>      struct ar_entry *route_e;
>      uint32_t hash;
> @@ -106,6 +131,24 @@ ar_entry_find(struct hmap *route_map,
>                      tracked_port != route_e->tracked_port->sb) {
>                  continue;
>              }
> +        } else if (route_e->tracked_port) {
> +            continue;
> +        }
> +
> +        if (!nullable_string_is_equal(tracked_service_ip,
> +                                      route_e->tracked_service_ip)) {
> +            continue;
> +        }
> +        if (has_tracked_service_port != route_e->has_tracked_service_port) {
> +            continue;
> +        }
> +        if (has_tracked_service_port &&
> +            tracked_service_port != route_e->tracked_service_port) {
> +            continue;
> +        }
> +        if (!nullable_string_is_equal(tracked_service_protocol,
> +                                      route_e->tracked_service_protocol)) {
> +            continue;
>          }
>  
>          return route_e;
> @@ -118,9 +161,55 @@ static void
>  ar_entry_free(struct ar_entry *route_e)
>  {
>      free(route_e->ip_prefix);
> +    free(route_e->tracked_service_ip);
> +    free(route_e->tracked_service_protocol);
>      free(route_e);
>  }
>  
> +/* Attach a per-backend service selector (ip, l4 port, protocol) to a
> + * previously added ar_entry. All three parameters are required:
> + * a partial selector could match an unrelated Service_Monitor row
> + * on the same (ip, port) for a different protocol.
> + *
> + * protocol must be one of the protocols accepted by
> + * lb_service_monitor_protocol_supported(), or the caller must leave the 
> entire
> + * selector unset (i.e. not invoke this helper) for LB protocols
> + * outside that set. */
> +static void
> +ar_entry_set_service_selector(struct ar_entry *route_e,
> +                               const char *ip, int64_t port,
> +                               const char *protocol)
> +{
> +    if (!ip || !protocol) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
> +        VLOG_WARN_RL(&rl, "Cannot set partial service selector: "
> +                     "ip=%s protocol=%s", ip ? ip : "(null)",
> +                     protocol ? protocol : "(null)");
> +        return;
> +    }
> +
> +    route_e->tracked_service_ip = xstrdup(ip);
> +    route_e->tracked_service_port = port;
> +    route_e->has_tracked_service_port = true;
> +    route_e->tracked_service_protocol = xstrdup(protocol);
> +}
> +
> +static void
> +ar_entry_copy_service_selector(struct ar_entry *dst,
> +                               const struct ar_entry *src)
> +{
> +    if (src->tracked_service_ip) {
> +        dst->tracked_service_ip = xstrdup(src->tracked_service_ip);
> +    }
> +    if (src->has_tracked_service_port) {
> +        dst->tracked_service_port = src->tracked_service_port;
> +        dst->has_tracked_service_port = true;
> +    }
> +    if (src->tracked_service_protocol) {
> +        dst->tracked_service_protocol = 
> xstrdup(src->tracked_service_protocol);
> +    }
> +}
> +
>  static void
>  advertised_route_table_sync(
>      struct ovsdb_idl_txn *ovnsb_txn,
> @@ -204,6 +293,13 @@ add_redistribute_parsed_route(struct hmap 
> *parsed_routes_out,
>      /* Parse the prefix (the VIP/FIP). */
>      struct in6_addr prefix;
>      if (!ip46_parse(ip_address, &prefix)) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
> +        VLOG_WARN_RL(&rl, "Failed to parse IP address '%s' for %s "
> +                     "redistribute forwarding route on datapath %s",
> +                     ip_address,
> +                     source == ROUTE_SOURCE_LB ? "LB" : "NAT",
> +                     advertising_od->nbr ? advertising_od->nbr->name
> +                                         : "<unknown>");
>          return;
>      }
>      bool is_v6 = !IN6_IS_ADDR_V4MAPPED(&prefix);
> @@ -217,6 +313,13 @@ add_redistribute_parsed_route(struct hmap 
> *parsed_routes_out,
>          nexthop_s = tracked_port->lrp_networks.ipv6_addrs[0].addr_s;
>      }
>      if (!nexthop_s) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
> +        VLOG_WARN_RL(&rl, "No %s address on tracked port %s for %s "
> +                     "redistribute forwarding route (prefix %s)",
> +                     is_v6 ? "IPv6" : "IPv4",
> +                     tracked_port->key,
> +                     source == ROUTE_SOURCE_LB ? "LB" : "NAT",
> +                     ip_address);
>          return;
>      }
>  
> @@ -257,7 +360,7 @@ add_redistribute_parsed_route(struct hmap 
> *parsed_routes_out,
>   * parsed_route on advertising_op->od for each NAT external IP whose
>   * nexthop is available from tracked_port (i.e. a peer LRP). This is the
>   * connected-neighbour redistribution case where the advertising LR
> - * needs to forward to the peer's LR.*/
> + * needs to forward to the peer's LR. */
>  static void
>  build_nat_route_for_port(const struct ovn_port *advertising_op,
>                           const struct lr_nat_record *lr_nat,
> @@ -283,10 +386,13 @@ build_nat_route_for_port(const struct ovn_port 
> *advertising_op,
>              ? ovn_port_find(ls_ports, nat->nb->logical_port)
>              : nat->l3dgw_port;
>  
> +        /* NAT routes carry no service selector, so pass NULL/0/NULL for
> +         * the selector portion of the dedup key. */
>          if (!ar_entry_find(routes, advertising_od->sdp->sb_dp,
>                             advertising_op->sb,
>                             nat->nb->external_ip,
> -                           tracked_port ? tracked_port->sb : NULL)) {
> +                           tracked_port ? tracked_port->sb : NULL,
> +                           NULL, false, 0, NULL)) {
>              ar_entry_add(routes, advertising_od, advertising_op,
>                           nat->nb->external_ip, tracked_port,
>                           ROUTE_SOURCE_NAT);
> @@ -397,6 +503,92 @@ build_nat_connected_routes(
>      }
>  }
>  
> +/* For each LB attached to peer_lr_nbr, emit one Advertised_Route per
> + * (VIP, backend LSP) pair, plus one forwarding parsed_route per VIP.
> + * Backends without ip_port_mappings fall back to one Advertised_Route
> + * per VIP with fallback_tracked_port in place of a per-backend LSP.
> + * The forwarding route is emitted once per VIP regardless of backend
> + * count: the data-plane forwarding decision is independent of which
> + * backend ends up serving the flow. */
> +static void
> +build_lb_lr_routes(const struct ovn_port *advertising_op,
> +                   const struct ovn_port *fallback_tracked_port,
> +                   const struct nbrec_logical_router *peer_lr_nbr,
> +                   const struct hmap *lb_datapaths_map,
> +                   const struct hmap *ls_ports,
> +                   struct hmap *routes,
> +                   struct hmap *parsed_routes_out)
> +{
> +    const struct ovn_datapath *advertising_od = advertising_op->od;
> +
> +    if (!peer_lr_nbr) {
> +        return;
> +    }
> +
> +    for (size_t i = 0; i < peer_lr_nbr->n_load_balancer; i++) {
> +        const struct nbrec_load_balancer *nbrec_lb =
> +            peer_lr_nbr->load_balancer[i];
> +        if (!smap_get_bool(&nbrec_lb->options,
> +                           "dynamic-routing-advertise", true)) {
> +            continue;
> +        }
> +        const struct uuid *lb_uuid = &nbrec_lb->header_.uuid;
> +        const struct ovn_lb_datapaths *lb_dps =
> +            ovn_lb_datapaths_find(lb_datapaths_map, lb_uuid);
> +        if (!lb_dps) {
> +            continue;
> +        }
> +        const struct ovn_northd_lb *lb = lb_dps->lb;
> +        for (size_t v = 0; v < lb->n_vips; v++) {
> +            const struct ovn_lb_vip *vip = &lb->vips[v];
> +            const struct ovn_northd_lb_vip *vip_nb = &lb->vips_nb[v];
> +
> +            if (parsed_routes_out) {
> +                add_redistribute_parsed_route(
> +                    parsed_routes_out, advertising_od, advertising_op,
> +                    fallback_tracked_port, vip->vip_str, ROUTE_SOURCE_LB);
> +            }
> +
> +            /* Protocols not accepted by
> +             * lb_service_monitor_protocol_supported() can never produce a
> +             * matching Service_Monitor row, and a partial selector
> +             * (ip+port, no protocol) risks matching an unrelated
> +             * monitor on the same ip/port for a different protocol.
> +             * Leave the whole selector unset in that case. */
> +            bool proto_supported =
> +                lb_service_monitor_protocol_supported(lb->proto);
> +
> +            bool emitted_any = false;
> +            for (size_t b = 0; b < vip_nb->n_backends; b++) {
> +                const char *lsp_name = vip_nb->backends_nb[b].logical_port;
> +                if (!lsp_name) {
> +                    continue;
> +                }
> +                const struct ovn_port *backend_op =
> +                    ovn_port_find(ls_ports, lsp_name);
> +                if (!backend_op) {
> +                    continue;
> +                }
> +                struct ar_entry *route_e =
> +                    ar_entry_add(routes, advertising_od, advertising_op,
> +                                 vip->vip_str, backend_op, ROUTE_SOURCE_LB);
> +                if (proto_supported) {
> +                    const struct ovn_lb_backend *backend =
> +                        vector_get_ptr(&vip->backends, b);
> +                    ar_entry_set_service_selector(route_e, backend->ip_str,
> +                                                  backend->port, lb->proto);
> +                }
> +                emitted_any = true;
> +            }
> +            if (!emitted_any) {
> +                ar_entry_add(routes, advertising_od, advertising_op,
> +                             vip->vip_str, fallback_tracked_port,
> +                             ROUTE_SOURCE_LB);
> +            }
> +        }
> +    }
> +}
> +
>  /* Own-LR entry point used by the own-LR (gateway-router/DGP) path,
>   * which doesn't currently route through a peer LR's LBs. Emits one
>   * Advertised_Route per IP in lb_ips with tracked_port as-is.
> @@ -441,7 +633,8 @@ build_lb_route_for_port(const struct ovn_port 
> *advertising_op,
>   * LB VIPs too.*/
>  static void
>  build_lb_connected_routes(const struct ovn_datapath *od,
> -                          const struct lr_stateful_table *lr_stateful_table,
> +                          const struct hmap *lb_datapaths_map,
> +                          const struct hmap *ls_ports,
>                            struct dynamic_routes_data *data)
>  {
>      const struct ovn_port *op;
> @@ -459,13 +652,11 @@ build_lb_connected_routes(const struct ovn_datapath *od,
>          /* Track the peer datapath for any changes. */
>          dynamic_routes_track_od(data, peer_od);
>  
> -        const struct lr_stateful_record *lr_stateful_rec;
>          /* This is directly connected LR peer. */
>          if (peer_od->nbr) {
> -            lr_stateful_rec = lr_stateful_table_find_by_uuid(
> -                lr_stateful_table, peer_od->key);
> -            build_lb_route_for_port(op, op->peer, lr_stateful_rec->lb_ips,
> -                                    &data->routes, &data->parsed_routes);
> +            build_lb_lr_routes(op, op->peer, peer_od->nbr,
> +                               lb_datapaths_map, ls_ports,
> +                               &data->routes, &data->parsed_routes);
>              continue;
>          }
>  
> @@ -478,11 +669,9 @@ build_lb_connected_routes(const struct ovn_datapath *od,
>                   * function.*/
>                  continue;
>              }
> -            lr_stateful_rec = lr_stateful_table_find_by_uuid(
> -                lr_stateful_table, rp->peer->od->key);
> -
> -            build_lb_route_for_port(op, rp->peer, lr_stateful_rec->lb_ips,
> -                                    &data->routes, &data->parsed_routes);
> +            build_lb_lr_routes(op, rp->peer, rp->peer->od->nbr,
> +                               lb_datapaths_map, ls_ports,
> +                               &data->routes, &data->parsed_routes);
>              /* Track the LR datapath on the other side of LS
>               * for any changes. */
>              dynamic_routes_track_od(data, rp->peer->od);
> @@ -776,7 +965,8 @@ en_dynamic_routes_run(struct engine_node *node, void 
> *data)
>  
>          build_lb_routes(od, lr_stateful_rec->lb_ips,
>                          &dynamic_routes_data->routes);
> -        build_lb_connected_routes(od, &lr_stateful_data->table,
> +        build_lb_connected_routes(od, &northd_data->lb_datapaths_map,
> +                                  &northd_data->ls_ports,
>                                    dynamic_routes_data);
>      }
>  
> @@ -966,9 +1156,13 @@ advertised_route_table_sync(
>          const struct sbrec_port_binding *tracked_port =
>              route->tracked_port ? route->tracked_port->sb : NULL;
>          char *ip_prefix = normalize_v46_prefix(&route->prefix, route->plen);
> + /* Parsed routes (static, connected, NAT) carry no per-backend
> +         * service selector, so pass NULL/0/NULL to compare against
> +         * existing sync entries on the (dp, lp, prefix, tracked_port)
> +         * portion of the key with empty selectors. */
>          if (ar_entry_find(&sync_routes, route->od->sdp->sb_dp,
>                            route->out_port->sb, ip_prefix,
> -                          tracked_port)) {
> +                          tracked_port, NULL, false, 0, NULL)) {
>              free(ip_prefix);
>              continue;
>          }
> @@ -978,7 +1172,10 @@ advertised_route_table_sync(
>                              route->source);
>      }
>  
> -    /* Then add the set of dynamic routes that need sync-ing. */
> +    /* Then add the set of dynamic routes that need sync-ing. The SB
> +     * unique index includes the selector columns, so two rows with
> +     * the same VIP and backend LSP but different selectors are
> +     * distinct entries and both must land in sync_routes. */
>      struct ar_entry *route_e;
>      HMAP_FOR_EACH (route_e, hmap_node, dynamic_routes) {
>          if (!should_advertise_route(route_e->od, route_e->op,
> @@ -990,32 +1187,46 @@ advertised_route_table_sync(
>              route_e->tracked_port ? route_e->tracked_port->sb : NULL;
>          if (ar_entry_find(&sync_routes, route_e->od->sdp->sb_dp,
>                            route_e->op->sb,
> -                          route_e->ip_prefix, tracked_pb)) {
> -            /* We could already have advertised route entry for LRP IP that
> -             * corresponds to "snat" when "connected-as-host" is combined
> -             * with "nat". Skip it. */
> +                          route_e->ip_prefix, tracked_pb,
> +                          route_e->tracked_service_ip,
> +                          route_e->has_tracked_service_port,
> +                          route_e->tracked_service_port,
> +                          route_e->tracked_service_protocol)) {
> +            /* Exact duplicate of an entry already in sync_routes (e.g.
> +             * the snat/connected-as-host overlap, or two LB
> +             * configurations describing the same backend service).
> +             * Skip the redundant insert. */
>              continue;
>          }
> -        ar_entry_add(&sync_routes, route_e->od, route_e->op,
> -                     route_e->ip_prefix, route_e->tracked_port,
> -                     route_e->source);
> +        struct ar_entry *sync_e =
> +            ar_entry_add(&sync_routes, route_e->od, route_e->op,
> +                         route_e->ip_prefix, route_e->tracked_port,
> +                         route_e->source);
> +        /* Preserve the per-backend service selector across the copy
> +         * into sync_routes. build_lb_lr_routes sets it on route_e but
> +         * ar_entry_add starts the new sync_e with NULL/zero fields. */
> +        ar_entry_copy_service_selector(sync_e, route_e);
>      }
>  
>      const struct sbrec_advertised_route *sb_route;
>      SBREC_ADVERTISED_ROUTE_TABLE_FOR_EACH_SAFE (sb_route,
>                                                  
> sbrec_advertised_route_table) {
> +        bool have_port = sb_route->n_tracked_service_port > 0;
> +        int64_t sb_port = have_port ? sb_route->tracked_service_port[0] : 0;
>          route_e = ar_entry_find(&sync_routes, sb_route->datapath,
>                                  sb_route->logical_port, sb_route->ip_prefix,
> -                                sb_route->tracked_port);
> +                                sb_route->tracked_port,
> +                                sb_route->tracked_service_ip,
> +                                have_port, sb_port,
> +                                sb_route->tracked_service_protocol);
>          if (!route_e) {
> +            /* No matching entry in the to-emit set: the LB,
> +             * its backends, or the selector drifted. The
> +             * replacement row (if any) will be inserted below. */
>              sbrec_advertised_route_delete(sb_route);
>              continue;
>          }
> -
> -        if (route_e->tracked_port && !sb_route->tracked_port) {
> -            sbrec_advertised_route_set_tracked_port(
> -                sb_route, route_e->tracked_port->sb);
> -        }
> +        /* Full-key match: nothing to update. */
>          hmap_remove(&sync_routes, &route_e->hmap_node);
>          ar_entry_free(route_e);
>      }
> @@ -1030,6 +1241,18 @@ advertised_route_table_sync(
>              sbrec_advertised_route_set_tracked_port(sr,
>                                                      
> route_e->tracked_port->sb);
>          }
> +        if (route_e->tracked_service_ip) {
> +            sbrec_advertised_route_set_tracked_service_ip(
> +                sr, route_e->tracked_service_ip);
> +        }
> +        if (route_e->has_tracked_service_port) {
> +            int64_t port = route_e->tracked_service_port;
> +            sbrec_advertised_route_set_tracked_service_port(sr, &port, 1);
> +        }
> +        if (route_e->tracked_service_protocol) {
> +            sbrec_advertised_route_set_tracked_service_protocol(
> +                sr, route_e->tracked_service_protocol);
> +        }
>          ar_entry_free(route_e);
>      }
>  
> diff --git a/northd/lb.h b/northd/lb.h
> index 7a98c2f55..5b78a2d61 100644
> --- a/northd/lb.h
> +++ b/northd/lb.h
> @@ -240,4 +240,18 @@ ovn_lb_group_datapaths_add_lr(struct 
> ovn_lb_group_datapaths *lbg_dps,
>      vector_push(&lbg_dps->lr, &lr);
>  }
>  
> +/* Returns true if protocol (the LB's L4 protocol string) is one of the
> + * values in the Service_Monitor.protocol SB schema enum.
> + * Protocols outside the enum (e.g. SCTP) cannot produce Service_Monitor rows
> + * and must not be used to populate the per-backend service selector on
> + * Advertised_Route. */
> +static inline bool
> +lb_service_monitor_protocol_supported(const char *protocol)
> +{
> +    return protocol
> +        && (!strcmp(protocol, "tcp")
> +            || !strcmp(protocol, "udp")
> +            || !strcmp(protocol, "icmp"));
> +}
> +
>  #endif /* OVN_NORTHD_LB_H */
> diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
> index d9a91739c..973fb7e0a 100644
> --- a/ovn-sb.ovsschema
> +++ b/ovn-sb.ovsschema
> @@ -1,7 +1,7 @@
>  {
>      "name": "OVN_Southbound",
> -    "version": "21.8.0",
> -    "cksum": "614397313 36713",
> +    "version": "21.9.0",
> +    "cksum": "341356787 37499",
>      "tables": {
>          "SB_Global": {
>              "columns": {
> @@ -670,11 +670,23 @@
>                                                    "refType": "strong"},
>                                            "min": 0,
>                                            "max": 1}},
> +                "tracked_service_ip": {"type": {"key": "string",
> +                                                "min": 0, "max": 1}},
> +                "tracked_service_port": {"type": {"key": {"type": "integer",
> +                                                          "minInteger": 0,
> +                                                          "maxInteger": 
> 65535},
> +                                                  "min": 0, "max": 1}},
> +                "tracked_service_protocol": {
> +                    "type": {"key": {"type": "string",
> +                             "enum": ["set", ["tcp", "udp", "icmp"]]},
> +                             "min": 0, "max": 1}},
>                  "external_ids": {
>                      "type": {"key": "string", "value": "string",
>                               "min": 0, "max": "unlimited"}}},
>              "indexes": [["datapath", "logical_port",
> -                         "ip_prefix", "tracked_port"]],
> +                         "ip_prefix", "tracked_port",
> +                         "tracked_service_ip", "tracked_service_port",
> +                         "tracked_service_protocol"]],
>              "isRoot": true},
>          "Learned_Route": {
>              "columns": {
> diff --git a/ovn-sb.xml b/ovn-sb.xml
> index e45b63d73..7c843a8fa 100644
> --- a/ovn-sb.xml
> +++ b/ovn-sb.xml
> @@ -5047,8 +5047,8 @@ tcp.flags = RST;
>        </p>
>  
>        <column name="type">
> -        The type of the service. Supported values are "load-balancer" and
> -        "network-function".
> +        The type of the service. Supported values are "load-balancer",
> +        "network-function", and "logical-switch-port".
>        </column>
>  
>        <column name="ip">
> @@ -5425,6 +5425,58 @@ tcp.flags = RST;
>        destination is local and adjust the route priorities based on that.
>      </column>
>  
> +    <column name="tracked_service_ip">
> +      Optional. Together with
> +      <ref column="tracked_service_port"/> and
> +      <ref column="tracked_service_protocol"/>, identifies the specific
> +      backend service (typically a Load_Balancer backend) whose health
> +      governs this route's advertisement.
> +
> +      <p>
> +        Set by <code>ovn-northd</code> for routes emitted from a
> +        Load_Balancer with <code>ip_port_mappings</code> configured.
> +        When all three <code>tracked_service_*</code> columns are
> +        present, <code>ovn-controller</code> joins them with
> +        <ref column="tracked_port"/> and the local chassis name to
> +        find the matching <ref table="Service_Monitor"/> row(s) on
> +        <ref table="Service_Monitor" 
> column="type"/>=<code>load-balancer</code>
> +        and gates kernel-route installation on their
> +        <code>status</code>: if matching rows exist on this chassis
> +        and none report <code>online</code>, the route is withdrawn
> +        locally.
> +      </p>
> +
> +      <p>
> +        The controller requires all three columns to be set before
> +        gating the route on Service_Monitor status. Rows with any
> +        column unset are installed unconditionally, since the
> +        controller cannot distinguish an LB row with a missing
> +        selector from a non-LB row (the route source is not stored
> +        in this table).
> +      </p>
> +
> +      <p>
> +        <b>Rolling upgrade:</b> all three columns are optional (min 0).
> +        During a mixed-version upgrade, an older northd that does not
> +        set these columns will leave them absent, and an older
> +        controller that does not read them will ignore them - so the
> +        behavior degrades gracefully to the pre-per-backend state (one
> +        Advertised_Route per VIP, no health-check gating). No special
> +        upgrade orchestration is required.
> +      </p>
> +    </column>
> +
> +    <column name="tracked_service_port">
> +      Optional. L4 port of the backend service that this route
> +      depends on. See <ref column="tracked_service_ip"/>.
> +    </column>
> +
> +    <column name="tracked_service_protocol">
> +      Optional. L4 protocol of the backend service that this route
> +      depends on. See <ref column="tracked_service_ip"/>. Mirrors the
> +      <ref table="Service_Monitor" column="protocol"/> enum.
> +    </column>
> +
>      <column name="external_ids">
>        See <em>External IDs</em> at the beginning of this document.
>      </column>
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index aa3e14410..f842d9634 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -18249,6 +18249,333 @@ 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])
> +ovn_start
> +
> +# When the LB row's ip_port_mappings populate per-backend LSPs, northd
> +# emits one Advertised_Route per (VIP, backend LSP) instead of a single
> +# row using the peer LR's gateway LRP. That moves the chassis-locality
> +# decision from a chassis-unbound patch port (the peer LRP) to the
> +# actual backend port - the controller can then per-chassis install
> +# the kernel route via dynamic-routing-redistribute-local-only=true,
> +# and gate it on Service_Monitor.status.
> +
> +check ovn-nbctl lr-add lr0
> +check ovn-nbctl set Logical_Router lr0 \
> +    options:dynamic-routing=true       \
> +    options:chassis=hv1
> +check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
> +check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
> +check ovn-nbctl ls-add up
> +check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
> +
> +check ovn-nbctl lr-add lr1
> +check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
> +check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
> +
> +# A backend logical switch hanging off lr1. The two backend LSPs are
> +# what we want as tracked_port in Advertised_Route.
> +check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
> +check ovn-nbctl ls-add be
> +check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
> +check ovn-nbctl lsp-add be be-vm1
> +check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
> +check ovn-nbctl lsp-add be be-vm2
> +check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 192.168.1.11"
> +
> +check ovn-nbctl \
> +    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 \
> +    -- set Load_Balancer lb0 options:distributed=true \
> +        ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
> +        ip_port_mappings:192.168.1.11="be-vm2:192.168.1.1" \
> +    -- lr-lb-add lr1 lb0
> +check ovn-nbctl --wait=sb sync
> +
> +datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
> +pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
> +pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
> +pb_be_vm2=$(fetch_column Port_Binding _uuid logical_port=be-vm2)
> +
> +# Expect two Advertised_Route rows - one per backend LSP.
> +check_row_count Advertised_Route 2
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="172.16.1.10"        \
> +    datapath=$datapath_lr0         \
> +    logical_port=$pb_lr0_up        \
> +    tracked_port=$pb_be_vm1
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="172.16.1.10"        \
> +    datapath=$datapath_lr0         \
> +    logical_port=$pb_lr0_up        \
> +    tracked_port=$pb_be_vm2
> +
> +# Forwarding parsed_route is still emitted once - the data-plane
> +# decision is independent of backend identity.
> +ovn-sbctl lflow-list lr0 > lr0_flows
> +AT_CHECK([grep -c 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows], [0], [1
> +])
> +
> +# Without ip_port_mappings, the fallback path emits one row with
> +# tracked_port = peer LRP (covered by the existing test). Re-derive
> +# that by clearing ip_port_mappings and re-syncing.
> +check ovn-nbctl --wait=sb clear Load_Balancer lb0 ip_port_mappings
> +pb_lr1_up=$(fetch_column Port_Binding _uuid logical_port=lr1-up)
> +check_row_count Advertised_Route 1
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="172.16.1.10"        \
> +    datapath=$datapath_lr0         \
> +    logical_port=$pb_lr0_up        \
> +    tracked_port=$pb_lr1_up
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([dynamic-routing - LB redistribute SCTP skips service selector])
> +AT_KEYWORDS([dynamic-routing])
> +ovn_start
> +
> +# SCTP is outside the Service_Monitor.protocol enum (tcp/udp/icmp), so
> +# the per-backend Advertised_Route rows must NOT carry service selector
> +# columns.
> +
> +check ovn-nbctl lr-add lr0
> +check ovn-nbctl set Logical_Router lr0 \
> +    options:dynamic-routing=true       \
> +    options:chassis=hv1
> +check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
> +check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
> +check ovn-nbctl ls-add up
> +check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
> +
> +check ovn-nbctl lr-add lr1
> +check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
> +check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
> +
> +check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
> +check ovn-nbctl ls-add be
> +check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
> +check ovn-nbctl lsp-add be be-vm1
> +check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
> +check ovn-nbctl lsp-add be be-vm2
> +check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 192.168.1.11"
> +
> +check ovn-nbctl \
> +    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 sctp \
> +    -- set Load_Balancer lb0 options:distributed=true \
> +        ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
> +        ip_port_mappings:192.168.1.11="be-vm2:192.168.1.1" \
> +    -- lr-lb-add lr1 lb0
> +check ovn-nbctl --wait=sb sync
> +
> +datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
> +pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
> +pb_be_vm2=$(fetch_column Port_Binding _uuid logical_port=be-vm2)
> +
> +# Expect two Advertised_Route rows - one per backend LSP.
> +check_row_count Advertised_Route 2
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="172.16.1.10"        \
> +    datapath=$datapath_lr0         \
> +    tracked_port=$pb_be_vm1
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="172.16.1.10"        \
> +    datapath=$datapath_lr0         \
> +    tracked_port=$pb_be_vm2
> +
> +# Selector columns must be absent for SCTP.
> +check_row_count Advertised_Route 0 tracked_service_ip="192.168.1.10"
> +check_row_count Advertised_Route 0 tracked_service_ip="192.168.1.11"
> +check_row_count Advertised_Route 0 tracked_service_port=80
> +check_row_count Advertised_Route 0 tracked_service_protocol="sctp"
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([dynamic-routing - LB redistribute no ip_port_mappings fallback])
> +AT_KEYWORDS([dynamic-routing])
> +ovn_start
> +
> +# When an LB has no ip_port_mappings at all, the fallback path emits
> +# one Advertised_Route per VIP with tracked_port set to the peer LRP.
> +
> +check ovn-nbctl lr-add lr0
> +check ovn-nbctl set Logical_Router lr0 \
> +    options:dynamic-routing=true       \
> +    options:chassis=hv1
> +check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
> +check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
> +check ovn-nbctl ls-add up
> +check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
> +
> +check ovn-nbctl lr-add lr1
> +check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
> +check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
> +check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
> +check ovn-nbctl ls-add be
> +check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
> +check ovn-nbctl lsp-add be be-vm1
> +check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
> +check ovn-nbctl lsp-add be be-vm2
> +check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 192.168.1.11"
> +
> +# LB with no ip_port_mappings at all.
> +check ovn-nbctl \
> +    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 \
> +    -- set Load_Balancer lb0 options:distributed=true \
> +    -- lr-lb-add lr1 lb0
> +check ovn-nbctl --wait=sb sync
> +
> +datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
> +pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
> +pb_lr1_up=$(fetch_column Port_Binding _uuid logical_port=lr1-up)
> +
> +# One Advertised_Route row with tracked_port = peer LRP (lr1-up),
> +# not a per-backend row.
> +check_row_count Advertised_Route 1
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="172.16.1.10"        \
> +    datapath=$datapath_lr0          \
> +    logical_port=$pb_lr0_up         \
> +    tracked_port=$pb_lr1_up
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([dynamic-routing - LB redistribute partial ip_port_mappings])
> +AT_KEYWORDS([dynamic-routing])
> +ovn_start
> +
> +# When only some backends have ip_port_mappings entries, only those
> +# backends should produce per-backend Advertised_Route rows.
> +
> +check ovn-nbctl lr-add lr0
> +check ovn-nbctl set Logical_Router lr0 \
> +    options:dynamic-routing=true       \
> +    options:chassis=hv1
> +check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
> +check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
> +check ovn-nbctl ls-add up
> +check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
> +
> +check ovn-nbctl lr-add lr1
> +check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
> +check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
> +
> +check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
> +check ovn-nbctl ls-add be
> +check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
> +check ovn-nbctl lsp-add be be-vm1
> +check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
> +check ovn-nbctl lsp-add be be-vm2
> +check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 192.168.1.11"
> +
> +# Only map one backend initially.
> +check ovn-nbctl \
> +    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 \
> +    -- set Load_Balancer lb0 options:distributed=true \
> +        ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
> +    -- lr-lb-add lr1 lb0
> +check ovn-nbctl --wait=sb sync
> +
> +datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
> +pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
> +pb_be_vm2=$(fetch_column Port_Binding _uuid logical_port=be-vm2)
> +
> +# Only one Advertised_Route row - for the mapped backend.
> +check_row_count Advertised_Route 1
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="172.16.1.10"        \
> +    datapath=$datapath_lr0         \
> +    tracked_port=$pb_be_vm1
> +
> +# Add the second mapping and re-sync.
> +check ovn-nbctl --wait=sb set Load_Balancer lb0 \
> +    ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
> +    ip_port_mappings:192.168.1.11="be-vm2:192.168.1.1"
> +
> +# Now expect two rows - both backends mapped.
> +check_row_count Advertised_Route 2
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="172.16.1.10"        \
> +    datapath=$datapath_lr0         \
> +    tracked_port=$pb_be_vm1
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="172.16.1.10"        \
> +    datapath=$datapath_lr0         \
> +    tracked_port=$pb_be_vm2
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([dynamic-routing - LB redistribute IPv6 per-backend selector])
> +AT_KEYWORDS([dynamic-routing])
> +ovn_start
> +
> +# IPv6 variant of the per-backend test: selector columns must be
> +# populated on the Advertised_Route rows.
> +
> +check ovn-nbctl lr-add lr0
> +check ovn-nbctl set Logical_Router lr0 \
> +    options:dynamic-routing=true       \
> +    options:chassis=hv1
> +check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
> +check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
> +check ovn-nbctl ls-add up
> +check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
> +
> +check ovn-nbctl lr-add lr1
> +check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 2001:db8::1/64
> +check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
> +
> +check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 2001:db8:1::1/64
> +check ovn-nbctl ls-add be
> +check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
> +check ovn-nbctl lsp-add be be-vm1
> +check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 2001:db8:1::10"
> +check ovn-nbctl lsp-add be be-vm2
> +check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 2001:db8:1::11"
> +
> +check ovn-nbctl \
> +    -- lb-add lb0 [[2001:db8:ffff::10]]:80 
> [[2001:db8:1::10]]:80,[[2001:db8:1::11]]:80 \
> +    -- set Load_Balancer lb0 options:distributed=true \
> +        ip_port_mappings:\"[[2001:db8:1::10]]\"=\"be-vm1:[[2001:db8:1::1]]\" 
> \
> +        ip_port_mappings:\"[[2001:db8:1::11]]\"=\"be-vm2:[[2001:db8:1::1]]\" 
> \
> +    -- lr-lb-add lr1 lb0
> +check ovn-nbctl --wait=sb sync
> +
> +datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
> +pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
> +pb_be_vm2=$(fetch_column Port_Binding _uuid logical_port=be-vm2)
> +
> +# Expect two Advertised_Route rows - one per backend LSP.
> +check_row_count Advertised_Route 2
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="2001\:db8\:ffff\:\:10" \
> +    datapath=$datapath_lr0           \
> +    tracked_port=$pb_be_vm1
> +check_row_count Advertised_Route 1 \
> +    ip_prefix="2001\:db8\:ffff\:\:10" \
> +    datapath=$datapath_lr0           \
> +    tracked_port=$pb_be_vm2
> +
> +# Selector columns must be set on both rows.
> +check_row_count Advertised_Route 1 tracked_service_ip="2001\:db8\:1\:\:10"
> +check_row_count Advertised_Route 1 tracked_service_ip="2001\:db8\:1\:\:11"
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
>  OVN_FOR_EACH_NORTHD_NO_HV([
>  AT_SETUP([dynamic-routing - LB forwarding route updates on nexthop change])
>  AT_KEYWORDS([dynamic-routing])
> -- 
> 2.53.0
> 
> 
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to