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
