Recheck-request: github-robot-_Build_and_Test

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

> Build a transient hmap of type="load-balancer" Service_Monitor rows
> keyed by (logical_port, chassis_name, ip, port, protocol) in
> route_run() and consult it for Advertised_Route rows that carry
> the full backend selector (tracked_service_ip,
> tracked_service_port, tracked_service_protocol all set).
> 
> When matching SM rows exist but none report online, skip installing
> the kernel route. When no matching SM rows exist
> (e.g. the backend has no health check, or the LB protocol lacks
> Service_Monitor support), install unconditionally.
> 
> A partial selector is not enough to gate on: without route_source
> on the SB row, a tracked_port that coincides with an LB backend LSP
> could match an unrelated LB's Service_Monitor and be withdrawn
> incorrectly.
> 
> The gate applies only to routes whose tracked_port is local to this
> chassis (PRIORITY_LOCAL_BOUND). Routes at PRIORITY_DEFAULT are not
> affected.
> 
> Signed-off-by: Dmitrii Shcherbakov <[email protected]>
> ---
>  controller/ovn-controller.c      |  25 +-
>  controller/route.c               | 101 ++++++
>  controller/route.h               |   1 +
>  tests/ovn-inc-proc-graph-dump.at |   2 +
>  tests/system-ovn.at              | 521 +++++++++++++++++++++++++++++++
>  5 files changed, 649 insertions(+), 1 deletion(-)
> 
> diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
> index fd848c54c..c6d718133 100644
> --- a/controller/ovn-controller.c
> +++ b/controller/ovn-controller.c
> @@ -5301,6 +5301,8 @@ en_route_run(struct engine_node *node, void *data)
>  
>      const struct sbrec_advertised_route_table *advertised_route_table =
>          EN_OVSDB_GET(engine_get_input("SB_advertised_route", node));
> +    const struct sbrec_service_monitor_table *service_monitor_table =
> +        EN_OVSDB_GET(engine_get_input("SB_service_monitor", node));
>  
>      const struct ovsrec_open_vswitch *cfg
>          = ovsrec_open_vswitch_table_first(ovs_table);
> @@ -5309,6 +5311,7 @@ en_route_run(struct engine_node *node, void *data)
>  
>      struct route_ctx_in r_ctx_in = {
>          .advertised_route_table = advertised_route_table,
> +        .service_monitor_table = service_monitor_table,
>          .sbrec_port_binding_by_name = sbrec_port_binding_by_name,
>          .chassis = chassis,
>          .dynamic_routing_port_mapping = dynamic_routing_port_mapping,
> @@ -5621,6 +5624,23 @@ route_sb_datapath_binding_handler(struct engine_node 
> *node,
>      return EN_HANDLED_UNCHANGED;
>  }
>  
> +static enum engine_input_handler_result
> +route_sb_service_monitor_handler(struct engine_node *node,
> +                                 void *data OVS_UNUSED)
> +{
> +    const struct sbrec_service_monitor_table *sm_table =
> +        EN_OVSDB_GET(engine_get_input("SB_service_monitor", node));
> +
> +    const struct sbrec_service_monitor *sm;
> +    SBREC_SERVICE_MONITOR_TABLE_FOR_EACH_TRACKED (sm, sm_table) {
> +        if (sm->type && !strcmp(sm->type, "load-balancer")) {
> +            return EN_UNHANDLED;
> +        }
> +    }
> +
> +    return EN_HANDLED_UNCHANGED;
> +}
> +
>  static int
>  table_id_cmp(const void *a_, const void *b_)
>  {
> @@ -6869,7 +6889,8 @@ evpn_arp_vtep_binding_handler(struct engine_node *node, 
> void *data OVS_UNUSED)
>      SB_NODE(acl_id) \
>      SB_NODE(advertised_route) \
>      SB_NODE(learned_route) \
> -    SB_NODE(advertised_mac_binding)
> +    SB_NODE(advertised_mac_binding) \
> +    SB_NODE(service_monitor)
>  
>  enum sb_engine_node {
>  #define SB_NODE(NAME) SB_##NAME,
> @@ -7002,6 +7023,8 @@ inc_proc_ovn_controller_init(
>                       route_sb_advertised_route_data_handler);
>      engine_add_input(&en_route, &en_sb_datapath_binding,
>                       route_sb_datapath_binding_handler);
> +    engine_add_input(&en_route, &en_sb_service_monitor,
> +                     route_sb_service_monitor_handler);
>  
>      engine_add_input(&en_route_exchange, &en_route, NULL);
>      engine_add_input(&en_route_exchange, &en_sb_learned_route,
> diff --git a/controller/route.c b/controller/route.c
> index 13e6d3010..e77c90bcd 100644
> --- a/controller/route.c
> +++ b/controller/route.c
> @@ -38,6 +38,19 @@ VLOG_DEFINE_THIS_MODULE(exchange);
>  #define PRIORITY_DEFAULT 1000
>  #define PRIORITY_LOCAL_BOUND 100
>  
> +/* Key for the service-monitor LB index (sm_lb_index).  All string
> + * pointers alias SB rows and are valid for the duration of
> + * en_route_run(). */
> +struct sm_lb_key {
> +    struct hmap_node node;
> +    const char *logical_port;
> +    const char *chassis_name;
> +    const char *ip;
> +    int64_t port;
> +    const char *protocol;
> +    bool online;
> +};
> +
>  static bool
>  route_exchange_relevant_port(const struct sbrec_port_binding *pb)
>  {
> @@ -315,6 +328,36 @@ route_run(struct route_ctx_in *r_ctx_in,
>          }
>      }
>  
> +    struct hmap sm_lb_index = HMAP_INITIALIZER(&sm_lb_index);
> +    if (r_ctx_in->service_monitor_table) {
> +        const struct sbrec_service_monitor *sm;
> +        SBREC_SERVICE_MONITOR_TABLE_FOR_EACH (
> +            sm, r_ctx_in->service_monitor_table) {
> +            if (!sm->type || strcmp(sm->type, "load-balancer")) {
> +                continue;
> +            }
> +            if (!sm->logical_port || !sm->chassis_name ||
> +                !sm->ip || !sm->protocol) {
> +                continue;
> +            }
> +            struct sm_lb_key *k = xmalloc(sizeof *k);
> +            uint32_t hash = hash_string(sm->logical_port, 0);
> +            hash = hash_string(sm->chassis_name, hash);
> +            hash = hash_string(sm->ip, hash);
> +            hash = hash_int((uint32_t) sm->port, hash);
> +            hash = hash_string(sm->protocol, hash);
> +            *k = (struct sm_lb_key) {
> +                .logical_port = sm->logical_port,
> +                .chassis_name = sm->chassis_name,
> +                .ip = sm->ip,
> +                .port = sm->port,
> +                .protocol = sm->protocol,
> +                .online = sm->status && !strcmp(sm->status, "online"),
> +            };
> +            hmap_insert(&sm_lb_index, &k->node, hash);
> +        }
> +    }
> +
>      const struct sbrec_advertised_route *route;
>      SBREC_ADVERTISED_ROUTE_TABLE_FOR_EACH (route,
>                                             r_ctx_in->advertised_route_table) 
> {
> @@ -357,6 +400,58 @@ route_run(struct route_ctx_in *r_ctx_in,
>                  priority = PRIORITY_LOCAL_BOUND;
>                  sset_add(r_ctx_out->tracked_ports_local,
>                           route->tracked_port->logical_port);
> +
> +                /* If the route carries the full backend service selector,
> +                 * look up matching Service_Monitor rows on this chassis.
> +                 * Skip installing the route when matching rows exist but
> +                 * none report online.  Routes without the full selector
> +                 * are installed unconditionally.
> +                 *
> +                 * The full selector (ip + port + protocol) is required to
> +                 * identify the row as LB-derived, since route_source is
> +                 * not stored on the SB row.  A partial selector could
> +                 * match an unrelated SM on the same (ip, port) for a
> +                 * different protocol or LB. */
> +                bool has_full_selector =
> +                    route->tracked_service_ip &&
> +                    route->n_tracked_service_port > 0 &&
> +                    route->tracked_service_protocol;
> +                if (has_full_selector &&
> +                    !hmap_is_empty(&sm_lb_index)) {
> +                    const char *want_ip = route->tracked_service_ip;
> +                    int64_t want_port = route->tracked_service_port[0];
> +                    ovs_assert(want_port >= 0 && want_port <= UINT16_MAX);
> +                    const char *want_proto = route->tracked_service_protocol;
> +                    const char *want_lp = route->tracked_port->logical_port;
> +                    const char *want_chassis = r_ctx_in->chassis->name;
> +
> +                    uint32_t hash = hash_string(want_lp, 0);
> +                    hash = hash_string(want_chassis, hash);
> +                    hash = hash_string(want_ip, hash);
> +                    hash = hash_int((uint32_t) want_port, hash);
> +                    hash = hash_string(want_proto, hash);
> +
> +                    bool seen = false, any_online = false;
> +                    struct sm_lb_key *k;
> +                    HMAP_FOR_EACH_WITH_HASH (k, node, hash,
> +                                             &sm_lb_index) {
> +                        if (k->port != want_port ||
> +                            strcmp(k->logical_port, want_lp) ||
> +                            strcmp(k->chassis_name, want_chassis) ||
> +                            strcmp(k->ip, want_ip) ||
> +                            strcmp(k->protocol, want_proto)) {
> +                            continue;
> +                        }
> +                        seen = true;
> +                        if (k->online) {
> +                            any_online = true;
> +                            break;
> +                        }
> +                    }
> +                    if (seen && !any_online) {
> +                        continue;
> +                    }
> +                }
>              } else {
>                  sset_add(r_ctx_out->tracked_ports_remote,
>                           route->tracked_port->logical_port);
> @@ -386,6 +481,12 @@ route_run(struct route_ctx_in *r_ctx_in,
>                      advertise_route_hash(&ar->addr, &ar->nexthop, plen));
>      }
>  
> +    struct sm_lb_key *k;
> +    HMAP_FOR_EACH_POP (k, node, &sm_lb_index) {
> +        free(k);
> +    }
> +    hmap_destroy(&sm_lb_index);
> +
>      smap_destroy(&port_mapping);
>  }
>  
> diff --git a/controller/route.h b/controller/route.h
> index f1d03a9e5..5e13ef190 100644
> --- a/controller/route.h
> +++ b/controller/route.h
> @@ -34,6 +34,7 @@ struct sbrec_datapath_binding;
>  
>  struct route_ctx_in {
>      const struct sbrec_advertised_route_table *advertised_route_table;
> +    const struct sbrec_service_monitor_table *service_monitor_table;
>      struct ovsdb_idl_index *sbrec_port_binding_by_name;
>      const struct sbrec_chassis *chassis;
>      const char *dynamic_routing_port_mapping;
> diff --git a/tests/ovn-inc-proc-graph-dump.at 
> b/tests/ovn-inc-proc-graph-dump.at
> index b81352657..b8608d2f5 100644
> --- a/tests/ovn-inc-proc-graph-dump.at
> +++ b/tests/ovn-inc-proc-graph-dump.at
> @@ -451,6 +451,7 @@ digraph "Incremental-Processing-Engine" {
>       SB_chassis -> bfd_chassis [[label=""]];
>       SB_ha_chassis_group -> bfd_chassis [[label=""]];
>       SB_advertised_route [[style=filled, shape=box, fillcolor=white, 
> label="SB_advertised_route"]];
> +     SB_service_monitor [[style=filled, shape=box, fillcolor=white, 
> label="SB_service_monitor"]];
>       route [[style=filled, shape=box, fillcolor=white, label="route"]];
>       OVS_open_vswitch -> route [[label=""]];
>       SB_chassis -> route [[label=""]];
> @@ -458,6 +459,7 @@ digraph "Incremental-Processing-Engine" {
>       runtime_data -> route [[label="route_runtime_data_handler"]];
>       SB_advertised_route -> route 
> [[label="route_sb_advertised_route_data_handler"]];
>       SB_datapath_binding -> route 
> [[label="route_sb_datapath_binding_handler"]];
> +     SB_service_monitor -> route 
> [[label="route_sb_service_monitor_handler"]];
>       SB_learned_route [[style=filled, shape=box, fillcolor=white, 
> label="SB_learned_route"]];
>       route_table_notify [[style=filled, shape=box, fillcolor=white, 
> label="route_table_notify"]];
>       route_exchange_status [[style=filled, shape=box, fillcolor=white, 
> label="route_exchange_status"]];
> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> index 65781bed3..f7f833ee8 100644
> --- a/tests/system-ovn.at
> +++ b/tests/system-ovn.at
> @@ -21876,3 +21876,524 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port 
> patch-.*/d
>  
>  AT_CLEANUP
>  ])
> +])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([dynamic-routing - LB redistribute gated by Service_Monitor.status])
> +AT_KEYWORDS([dynamic-routing])
> +
> +VRF_RESERVE([1339])
> +
> +# Verifies the controller-side gate that skips installing the
> +# advertise-only kernel route for an LB-derived Advertised_Route
> +# that carries the full backend service selector (tracked_service_ip,
> +# tracked_service_port, tracked_service_protocol all set) and whose
> +# matching local Service_Monitor row, joined on (logical_port,
> +# chassis_name, type='load-balancer', ip, port, protocol), is not
> +# reporting online. The gate is not controlled by any LRP option: it
> +# acts automatically when the full selector is present and the matching
> +# SM row exists. Selector-less rows are installed unconditionally.
> +#
> +# Topology: two chassis-bound LRs (lr-origin, lr-target) share a
> +# common LS (ls-share). lr-origin has redistribute=lb on its LRP into
> +# ls-share, so northd emits per-backend Advertised_Route rows on
> +# lr-origin's datapath with tracked_port = the backend LSP and the
> +# full per-listener service selector populated. lr-target owns the
> +# LB. The backend LSP (be0) is on ls-share, bound to hv1.
> +
> +ovn_start
> +OVS_TRAFFIC_VSWITCHD_START()
> +
> +ADD_BR([br-int])
> +check ovs-vsctl \
> +    -- set Open_vSwitch . external-ids:system-id=hv1 \
> +    -- set Open_vSwitch . 
> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> +    -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
> +    -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
> +    -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
> +
> +start_daemon ovn-controller
> +
> +# Shared LS holding both LR LRPs and the backend LSP.
> +check ovn-nbctl ls-add ls-share
> +
> +# lr-origin: GW, vrf 1339, has the redistribute LRP into ls-share.
> +check ovn-nbctl lr-add lr-origin \
> +    -- set Logical_Router lr-origin options:chassis=hv1 \
> +                                    options:dynamic-routing=true \
> +                                    options:dynamic-routing-vrf-id=1339
> +check ovn-nbctl lrp-add lr-origin lr-origin-share 00:de:ad:00:00:01 \
> +        192.168.0.1/24 \
> +    -- set Logical_Router_Port lr-origin-share \
> +            options:dynamic-routing-redistribute="lb" \
> +            options:dynamic-routing-maintain-vrf=true
> +check ovn-nbctl lsp-add ls-share share-lr-origin \
> +    -- set Logical_Switch_Port share-lr-origin type=router \
> +                                               
> options:router-port=lr-origin-share \
> +    -- lsp-set-addresses share-lr-origin router
> +
> +# lr-target: GW, owns the LB. Also attached to ls-share so lr-origin
> +# can walk LR-LS-LR to discover the LB.
> +check ovn-nbctl lr-add lr-target \
> +    -- set Logical_Router lr-target options:chassis=hv1
> +check ovn-nbctl lrp-add lr-target lr-target-share 00:de:ad:00:00:02 \
> +        192.168.0.2/24
> +check ovn-nbctl lsp-add ls-share share-lr-target \
> +    -- set Logical_Switch_Port share-lr-target type=router \
> +                                               
> options:router-port=lr-target-share \
> +    -- lsp-set-addresses share-lr-target router
> +
> +# Backend LSP on the same LS. Veth-backed so it's claimed locally.
> +check ovn-nbctl lsp-add ls-share be0
> +check ovn-nbctl lsp-set-addresses be0 "00:de:ad:00:00:10 192.168.0.10"
> +ADD_NAMESPACES(be0_ns)
> +ADD_VETH(be0, be0_ns, br-int, "192.168.0.10/24", "00:de:ad:00:00:10")
> +
> +# LB with ip_port_mappings -> backend LSP "be0". options:distributed=true
> +# is required for northd to populate ovn_northd_lb_backend.logical_port
> +# (see ovn_lb_vip_backends_ip_port_mappings_init). The
> +# Load_Balancer_Health_Check is what makes ovn-northd populate the
> +# Service_Monitor SB table for the backend: a manually-created row
> +# would be garbage-collected as orphaned. ovn-controller then probes
> +# the backend and updates Service_Monitor.status, which is exactly
> +# what the controller-side gate observes.
> +check ovn-nbctl \
> +    -- lb-add lb0 172.16.1.10:80 192.168.0.10:80 \
> +    -- set Load_Balancer lb0 options:distributed=true \
> +        ip_port_mappings:192.168.0.10="be0:192.168.0.2" \
> +    -- lr-lb-add lr-target lb0
> +check_uuid ovn-nbctl --id=@hc create Load_Balancer_Health_Check \
> +        vip="172.16.1.10\:80" \
> +        options:interval=2 options:timeout=1 \
> +        options:success_count=1 options:failure_count=1 \
> +    -- add Load_Balancer lb0 health_check @hc
> +
> +check ovn-nbctl --wait=hv sync
> +wait_for_ports_up
> +OVS_CTL_TIMEOUT=30
> +
> +# Baseline: no listener on be0:80 -> ovn-controller's probe fails ->
> +# Service_Monitor.status=offline -> the gate withdraws the route from
> +# the VRF on this chassis. The blackhole route for 172.16.1.10 must
> +# NOT appear in ovnvrf1339.
> +OVS_WAIT_UNTIL([test -n "`ip vrf show ovnvrf1339 2>/dev/null`"])
> +wait_row_count Service_Monitor 1 logical_port=be0
> +wait_row_count Service_Monitor 1 logical_port=be0 status=offline
> +
> +# Confirm the gate is firing: no route for the LB VIP.
> +AT_CHECK([
> +    ip route list vrf ovnvrf1339 | grep -c "blackhole 172.16.1.10" || true
> +], [0], [0
> +])
> +
> +# Start a TCP listener on be0:80. ovn-controller's probe now succeeds
> +# and Service_Monitor.status flips to online, so the gate lets the route
> +# through and the route shows up in the VRF. OVS_START_L7 uses netstat
> +# for readiness which may not be available, so start test-l7.py directly
> +# and use Service_Monitor.status as the readiness signal instead.
> +be0_pid_file=$(mktemp be0_http.XXX.pid)
> +NETNS_DAEMONIZE([be0_ns],
> +    [[$PYTHON $srcdir/test-l7.py http]], [$be0_pid_file])
> +wait_row_count Service_Monitor 1 logical_port=be0 status=online
> +OVS_WAIT_UNTIL([
> +    ip route list vrf ovnvrf1339 | grep -q "blackhole 172.16.1.10"])
> +
> +# Kill the listener. The probe fails again, status=offline, and the
> +# route is withdrawn from the VRF so that any routing speaker reading
> +# it retracts the advertisement.
> +kill `cat $be0_pid_file`
> +wait_row_count Service_Monitor 1 logical_port=be0 status=offline
> +OVS_WAIT_UNTIL([
> +    ! ip route list vrf ovnvrf1339 | grep -q "blackhole 172.16.1.10"])
> +
> +# Remove the health_check from the LB -> northd deletes the SM row ->
> +# gate is inert again -> route reinstalled (the route is unconditional
> +# for unmonitored backends).
> +check ovn-nbctl clear Load_Balancer lb0 health_check
> +wait_row_count Service_Monitor 0 logical_port=be0
> +OVS_WAIT_UNTIL([
> +    ip route list vrf ovnvrf1339 | grep -q "blackhole 172.16.1.10"])
> +
> +OVS_APP_EXIT_AND_WAIT([ovn-controller])
> +
> +as ovn-sb
> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
> +
> +as ovn-nb
> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
> +
> +as northd
> +OVS_APP_EXIT_AND_WAIT([ovn-northd])
> +
> +as
> +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
> +/connection dropped.*/d"])
> +
> +AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([dynamic-routing - shared backend LSP gates per-VIP])
> +AT_KEYWORDS([dynamic-routing])
> +
> +VRF_RESERVE([1340])
> +
> +# Two distinct LBs (lb-a:80 and lb-b:443) share one backend LSP (be0).
> +# Each LB has its own Load_Balancer_Health_Check probing the backend
> +# port specific to that LB. This is the K8s-style multi-Service-per-pod
> +# shape: one backend LSP serves several VIPs whose health states are
> +# independent.
> +#
> +# With the full selector tuple match
> +# (logical_port + tracked_service_ip + tracked_service_port +
> +# tracked_service_protocol against Service_Monitor), each VIP's
> +# kernel-route presence depends only on its own backend port's SM
> +# row. Bringing one listener up and leaving the other down must
> +# install exactly the corresponding VIP's route, never both, never
> +# neither. This is a regression test for the previous looser
> +# (tracked_port, chassis) matching that would advertise both VIPs
> +# whenever any backend port was healthy.
> +
> +ovn_start
> +OVS_TRAFFIC_VSWITCHD_START()
> +
> +ADD_BR([br-int])
> +check ovs-vsctl \
> +    -- set Open_vSwitch . external-ids:system-id=hv1 \
> +    -- set Open_vSwitch . 
> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> +    -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
> +    -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
> +    -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
> +
> +start_daemon ovn-controller
> +
> +check ovn-nbctl ls-add ls-share
> +
> +check ovn-nbctl lr-add lr-origin \
> +    -- set Logical_Router lr-origin options:chassis=hv1 \
> +                                    options:dynamic-routing=true \
> +                                    options:dynamic-routing-vrf-id=1340
> +check ovn-nbctl lrp-add lr-origin lr-origin-share 00:de:ad:00:00:01 \
> +        192.168.0.1/24 \
> +    -- set Logical_Router_Port lr-origin-share \
> +            options:dynamic-routing-redistribute="lb" \
> +            options:dynamic-routing-maintain-vrf=true
> +check ovn-nbctl lsp-add ls-share share-lr-origin \
> +    -- set Logical_Switch_Port share-lr-origin type=router \
> +                                               
> options:router-port=lr-origin-share \
> +    -- lsp-set-addresses share-lr-origin router
> +
> +check ovn-nbctl lr-add lr-target \
> +    -- set Logical_Router lr-target options:chassis=hv1
> +check ovn-nbctl lrp-add lr-target lr-target-share 00:de:ad:00:00:02 \
> +        192.168.0.2/24
> +check ovn-nbctl lsp-add ls-share share-lr-target \
> +    -- set Logical_Switch_Port share-lr-target type=router \
> +                                               
> options:router-port=lr-target-share \
> +    -- lsp-set-addresses share-lr-target router
> +
> +# One backend LSP shared by both LBs, with two listeners on different
> +# ports (:80 and :443) inside the same netns.
> +check ovn-nbctl lsp-add ls-share be0
> +check ovn-nbctl lsp-set-addresses be0 "00:de:ad:00:00:10 192.168.0.10"
> +ADD_NAMESPACES(be0_ns)
> +ADD_VETH(be0, be0_ns, br-int, "192.168.0.10/24", "00:de:ad:00:00:10")
> +
> +# lb-a: VIP 172.16.1.10:80 -> 192.168.0.10:80
> +check ovn-nbctl \
> +    -- lb-add lb-a 172.16.1.10:80 192.168.0.10:80 \
> +    -- set Load_Balancer lb-a options:distributed=true \
> +        ip_port_mappings:192.168.0.10="be0:192.168.0.2" \
> +    -- lr-lb-add lr-target lb-a
> +check_uuid ovn-nbctl --id=@hc create Load_Balancer_Health_Check \
> +        vip="172.16.1.10\:80" \
> +        options:interval=2 options:timeout=1 \
> +        options:success_count=1 options:failure_count=1 \
> +    -- add Load_Balancer lb-a health_check @hc
> +
> +# lb-b: VIP 172.16.1.11:443 -> 192.168.0.10:443 (same backend LSP).
> +check ovn-nbctl \
> +    -- lb-add lb-b 172.16.1.11:443 192.168.0.10:443 \
> +    -- set Load_Balancer lb-b options:distributed=true \
> +        ip_port_mappings:192.168.0.10="be0:192.168.0.2" \
> +    -- lr-lb-add lr-target lb-b
> +check_uuid ovn-nbctl --id=@hc create Load_Balancer_Health_Check \
> +        vip="172.16.1.11\:443" \
> +        options:interval=2 options:timeout=1 \
> +        options:success_count=1 options:failure_count=1 \
> +    -- add Load_Balancer lb-b health_check @hc
> +
> +check ovn-nbctl --wait=hv sync
> +wait_for_ports_up
> +OVS_CTL_TIMEOUT=30
> +
> +# Baseline: nothing listening -> both SM rows offline -> neither VIP
> +# advertised. VRF exists (maintain-vrf=true) but carries no LB route.
> +OVS_WAIT_UNTIL([test -n "`ip vrf show ovnvrf1340 2>/dev/null`"])
> +wait_row_count Service_Monitor 1 logical_port=be0 port=80
> +wait_row_count Service_Monitor 1 logical_port=be0 port=80 status=offline
> +wait_row_count Service_Monitor 1 logical_port=be0 port=443 status=offline
> +AT_CHECK([
> +    ip route list vrf ovnvrf1340 | grep -c "blackhole 172.16.1." || true
> +], [0], [0
> +])
> +
> +# Bring up the :80 listener only. SM for :80 flips to online, SM for
> +# :443 stays offline. Tuple match must install ONLY the :80 route.
> +be80_pid_file=$(mktemp be0_http80.XXX.pid)
> +NETNS_DAEMONIZE([be0_ns],
> +    [[$PYTHON $srcdir/test-l7.py http]], [$be80_pid_file])
> +wait_row_count Service_Monitor 1 logical_port=be0 port=80 status=online
> +OVS_WAIT_UNTIL([
> +    ip route list vrf ovnvrf1340 | grep -q "blackhole 172.16.1.10"])
> +# The :443 VIP must NOT be advertised: this is the regression the
> +# tuple match buys. Under the loose (tracked_port, chassis) match the
> +# :80 SM going online would suffice to advertise 172.16.1.11 too.
> +AT_CHECK([
> +    ip route list vrf ovnvrf1340 | grep -c "blackhole 172.16.1.11" || true
> +], [0], [0
> +])
> +
> +# Bring up the :443 listener too. test-l7.py only binds :80, so spin
> +# up a separate Python TCP listener for :443.
> +be443_pid_file=$(mktemp be0_tcp443.XXX.pid)
> +NETNS_DAEMONIZE([be0_ns],
> +    [python3 -c "import socket; s=socket.socket(); s.bind(('0.0.0.0',443)); 
> s.listen(1)
> +while True:
> + c,_=s.accept(); c.close()"],
> +    [$be443_pid_file])
> +wait_row_count Service_Monitor 1 logical_port=be0 port=443 status=online
> +OVS_WAIT_UNTIL([
> +    ip route list vrf ovnvrf1340 | grep -q "blackhole 172.16.1.11"])
> +# :80 route should still be there.
> +AT_CHECK([
> +    ip route list vrf ovnvrf1340 | grep -c "blackhole 172.16.1.10"
> +], [0], [1
> +])
> +
> +# Kill ONLY the :80 listener. :443 still healthy. Only the 172.16.1.10
> +# VIP must withdraw. 172.16.1.11 stays.
> +kill `cat $be80_pid_file`
> +wait_row_count Service_Monitor 1 logical_port=be0 port=80 status=offline
> +OVS_WAIT_UNTIL([
> +    ! ip route list vrf ovnvrf1340 | grep -q "blackhole 172.16.1.10"])
> +AT_CHECK([
> +    ip route list vrf ovnvrf1340 | grep -c "blackhole 172.16.1.11"
> +], [0], [1
> +])
> +
> +OVS_APP_EXIT_AND_WAIT([ovn-controller])
> +
> +as ovn-sb
> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
> +
> +as ovn-nb
> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
> +
> +as northd
> +OVS_APP_EXIT_AND_WAIT([ovn-northd])
> +
> +as
> +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
> +/connection dropped.*/d"])
> +
> +AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([dynamic-routing - shared VIP IP and cross-VIP isolation])
> +AT_KEYWORDS([dynamic-routing])
> +
> +VRF_RESERVE([1341])
> +
> +# This test exercises the per-listener route emission and the
> +# per-listener controller gate over a single backend LSP that hosts
> +# services for two distinct VIP IPs:
> +#
> +#   - VIP A 172.16.1.20: two LB listeners (lb-c:80, lb-d:443) share
> +#     the same VIP IP and the same backend LSP (be0). The routing
> +#     speaker advertises 172.16.1.20 once but two Advertised_Route rows, one 
> per
> +#     listener, carry distinct service selectors and gate
> +#     independently. The chassis advertises the prefix while ANY
> +#     listener for that VIP IP is healthy locally.
> +#
> +#   - VIP B 172.16.1.30: a third LB (lb-e:8080) advertises a
> +#     different VIP IP off the same backend LSP. Its health must
> +#     not influence VIP A and vice versa.
> +#
> +# The cross-VIP isolation case (VIP A all offline, VIP B online) is
> +# the regression catcher: with the SB schema's unique index now
> +# spanning the per-listener selector columns, northd emits separate
> +# rows per listener and each row's gate matches only its own
> +# Service_Monitor on the full selector tuple. An earlier coarser
> +# match keyed on (logical_port, chassis) alone would let VIP B's
> +# healthy backend keep VIP A wrongly advertised. The full-selector
> +# gate keeps the two VIP IPs independent.
> +
> +ovn_start
> +OVS_TRAFFIC_VSWITCHD_START()
> +
> +ADD_BR([br-int])
> +check ovs-vsctl \
> +    -- set Open_vSwitch . external-ids:system-id=hv1 \
> +    -- set Open_vSwitch . 
> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> +    -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
> +    -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
> +    -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
> +
> +start_daemon ovn-controller
> +
> +check ovn-nbctl ls-add ls-share
> +
> +check ovn-nbctl lr-add lr-origin \
> +    -- set Logical_Router lr-origin options:chassis=hv1 \
> +                                    options:dynamic-routing=true \
> +                                    options:dynamic-routing-vrf-id=1341
> +check ovn-nbctl lrp-add lr-origin lr-origin-share 00:de:ad:00:00:01 \
> +        192.168.0.1/24 \
> +    -- set Logical_Router_Port lr-origin-share \
> +            options:dynamic-routing-redistribute="lb" \
> +            options:dynamic-routing-maintain-vrf=true
> +check ovn-nbctl lsp-add ls-share share-lr-origin \
> +    -- set Logical_Switch_Port share-lr-origin type=router \
> +                                               
> options:router-port=lr-origin-share \
> +    -- lsp-set-addresses share-lr-origin router
> +
> +check ovn-nbctl lr-add lr-target \
> +    -- set Logical_Router lr-target options:chassis=hv1
> +check ovn-nbctl lrp-add lr-target lr-target-share 00:de:ad:00:00:02 \
> +        192.168.0.2/24
> +check ovn-nbctl lsp-add ls-share share-lr-target \
> +    -- set Logical_Switch_Port share-lr-target type=router \
> +                                               
> options:router-port=lr-target-share \
> +    -- lsp-set-addresses share-lr-target router
> +
> +check ovn-nbctl lsp-add ls-share be0
> +check ovn-nbctl lsp-set-addresses be0 "00:de:ad:00:00:10 192.168.0.10"
> +ADD_NAMESPACES(be0_ns)
> +ADD_VETH(be0, be0_ns, br-int, "192.168.0.10/24", "00:de:ad:00:00:10")
> +
> +# VIP A, listener 1: lb-c carries 172.16.1.20:80 -> 192.168.0.10:80.
> +check ovn-nbctl \
> +    -- lb-add lb-c 172.16.1.20:80 192.168.0.10:80 \
> +    -- set Load_Balancer lb-c options:distributed=true \
> +        ip_port_mappings:192.168.0.10="be0:192.168.0.2" \
> +    -- lr-lb-add lr-target lb-c
> +check_uuid ovn-nbctl --id=@hc create Load_Balancer_Health_Check \
> +        vip="172.16.1.20\:80" \
> +        options:interval=2 options:timeout=1 \
> +        options:success_count=1 options:failure_count=1 \
> +    -- add Load_Balancer lb-c health_check @hc
> +
> +# VIP A, listener 2: lb-d carries 172.16.1.20:443 -> 192.168.0.10:443.
> +check ovn-nbctl \
> +    -- lb-add lb-d 172.16.1.20:443 192.168.0.10:443 \
> +    -- set Load_Balancer lb-d options:distributed=true \
> +        ip_port_mappings:192.168.0.10="be0:192.168.0.2" \
> +    -- lr-lb-add lr-target lb-d
> +check_uuid ovn-nbctl --id=@hc create Load_Balancer_Health_Check \
> +        vip="172.16.1.20\:443" \
> +        options:interval=2 options:timeout=1 \
> +        options:success_count=1 options:failure_count=1 \
> +    -- add Load_Balancer lb-d health_check @hc
> +
> +# VIP B: lb-e carries 172.16.1.30:8080 -> 192.168.0.10:8080.
> +check ovn-nbctl \
> +    -- lb-add lb-e 172.16.1.30:8080 192.168.0.10:8080 \
> +    -- set Load_Balancer lb-e options:distributed=true \
> +        ip_port_mappings:192.168.0.10="be0:192.168.0.2" \
> +    -- lr-lb-add lr-target lb-e
> +check_uuid ovn-nbctl --id=@hc create Load_Balancer_Health_Check \
> +        vip="172.16.1.30\:8080" \
> +        options:interval=2 options:timeout=1 \
> +        options:success_count=1 options:failure_count=1 \
> +    -- add Load_Balancer lb-e health_check @hc
> +
> +check ovn-nbctl --wait=hv sync
> +wait_for_ports_up
> +OVS_CTL_TIMEOUT=30
> +
> +# Northd emits one Advertised_Route per listener: two for VIP A and
> +# one for VIP B (three total on lr-origin's datapath), each row
> +# carrying its own per-listener selector. The SB index now spans the
> +# selector columns so the two VIP-A rows coexist.
> +OVS_WAIT_UNTIL([test -n "`ip vrf show ovnvrf1341 2>/dev/null`"])
> +wait_row_count Service_Monitor 1 logical_port=be0 port=80
> +wait_row_count Service_Monitor 1 logical_port=be0 port=80 status=offline
> +wait_row_count Service_Monitor 1 logical_port=be0 port=443 status=offline
> +wait_row_count Service_Monitor 1 logical_port=be0 port=8080 status=offline
> +wait_row_count sb:Advertised_Route 2 ip_prefix='"172.16.1.20"'
> +wait_row_count sb:Advertised_Route 1 ip_prefix='"172.16.1.30"'
> +wait_row_count sb:Advertised_Route 1 ip_prefix='"172.16.1.20"' 
> tracked_service_port=80
> +wait_row_count sb:Advertised_Route 1 ip_prefix='"172.16.1.20"' 
> tracked_service_port=443
> +wait_row_count sb:Advertised_Route 1 ip_prefix='"172.16.1.30"' 
> tracked_service_port=8080
> +
> +# Baseline: nothing listening -> neither VIP A nor VIP B advertised.
> +AT_CHECK([
> +    ip route list vrf ovnvrf1341 | grep -c "blackhole 172.16.1." || true
> +], [0], [0
> +])
> +
> +# Bring up VIP B's :8080 listener only. VIP B's row matches the
> +# online SM (be0, 192.168.0.10, 8080, tcp) and the route for
> +# 172.16.1.30 appears. Critically, VIP A's two rows match the
> +# (be0, 192.168.0.10, 80) and (be0, 192.168.0.10, 443) SMs and
> +# both stay offline, so 172.16.1.20 must NOT be advertised.
> +be8080_pid_file=$(mktemp be0_tcp8080.XXX.pid)
> +NETNS_DAEMONIZE([be0_ns],
> +    [python3 -c "import socket; s=socket.socket(); s.bind(('0.0.0.0',8080)); 
> s.listen(1)
> +while True:
> + c,_=s.accept(); c.close()"],
> +    [$be8080_pid_file])
> +wait_row_count Service_Monitor 1 logical_port=be0 port=8080 status=online
> +OVS_WAIT_UNTIL([
> +    ip route list vrf ovnvrf1341 | grep -q "blackhole 172.16.1.30"])
> +AT_CHECK([
> +    ip route list vrf ovnvrf1341 | grep -c "blackhole 172.16.1.20" || true
> +], [0], [0
> +])
> +
> +# Bring up VIP A's :80 listener. VIP A's :80 row passes its gate
> +# and the route for 172.16.1.20 appears, even though VIP A's :443
> +# row is still offline. This is the "advertise while any listener
> +# for this VIP IP is healthy" semantic.
> +be80_pid_file=$(mktemp be0_http80.XXX.pid)
> +NETNS_DAEMONIZE([be0_ns],
> +    [[$PYTHON $srcdir/test-l7.py http]], [$be80_pid_file])
> +wait_row_count Service_Monitor 1 logical_port=be0 port=80 status=online
> +OVS_WAIT_UNTIL([
> +    ip route list vrf ovnvrf1341 | grep -q "blackhole 172.16.1.20"])
> +
> +# Kill VIP A's :80 listener. VIP A is back to no healthy local
> +# listener -> route for 172.16.1.20 withdraws. VIP B's listener
> +# is still up, so 172.16.1.30 stays advertised. This is the
> +# cross-VIP isolation case: VIP B's online SM has a different
> +# (ip, port, protocol) selector than VIP A's offline SMs, so it
> +# cannot bleed into VIP A's per-listener gates and keep VIP A
> +# advertised.
> +kill `cat $be80_pid_file`
> +wait_row_count Service_Monitor 1 logical_port=be0 port=80 status=offline
> +OVS_WAIT_UNTIL([
> +    ! ip route list vrf ovnvrf1341 | grep -q "blackhole 172.16.1.20"])
> +AT_CHECK([
> +    ip route list vrf ovnvrf1341 | grep -c "blackhole 172.16.1.30"
> +], [0], [1
> +])
> +
> +OVS_APP_EXIT_AND_WAIT([ovn-controller])
> +
> +as ovn-sb
> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
> +
> +as ovn-nb
> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
> +
> +as northd
> +OVS_APP_EXIT_AND_WAIT([ovn-northd])
> +
> +as
> +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
> +/connection dropped.*/d"])
> +
> +AT_CLEANUP
> +])
> -- 
> 2.53.0
> 
> 
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to