Hi Jacob Thanks for the v3. LGTM Acked-by: Xavier Simonart <[email protected]>
Thanks Xavier On Mon, Oct 26, 2026 at 2:14 AM Jacob Tanenbaum <[email protected]> wrote: > The IGMP/MLD querier injects a single query through the > multicast flood group. The flood group's OpenFlow flow > contains one "load OUTPORT; resubmit(CHECK_LOOPBACK)" > action per local port, and OVS translates the entire thing > in a single packet_xlate(). The translation's resubmit > counter accumulates across all of those ports, so on a node > with many local ports (~330) the translation exceeds OVS's > MAX_RESUBMITS (4096) budget, packet_xlate() returns > OFPFMFC_UNKNOWN, and the query is dropped. > > Rather than relying on the flood group, have the querier > inject the query directly to the flood group's local ports. > consider_mc_group() already determines exactly which local > ports the flood group would deliver to (and each port's > conntrack zone); publish that list to pinctrl so the querier > can fan the query out per port. Publish an empty list when > the flood group has no local ports on this chassis (including > datapaths with no local ports at all), so the querier can > distinguish that from a list that has not been published > yet. > > The querier sends one OpenFlow PACKET_OUT per batch of up to > 100 ports. OVS executes every resubmit in an action list > (the flood group relies on this), and a batch of 100 ports > uses ~1900 of the 4096-resubmit budget because the per-port > egress path is a fixed ~19 resubmits. The single flood > query of ~330 ports used ~6300 resubmits. > > Each port's query still traverses the per-port egress > pipeline, so multicast ACLs still filter which ports > receive it. If the port list has not been published > yet (first transient cycles), the querier skips the query > and retries every second; once published, an empty list > means the flood group has no local ports, so the query is > skipped on the regular query interval. A flood-group query > would not be delivered locally in either state. > > Tested with the IGMP/MLD snoop/querier/relay, external > querier, and multicast relay unit tests (real > ovn-controller + vswitchd, pcap-validated query delivery). > > Reported-at: https://redhat.atlassian.net/browse/FDP-4445 > Assisted-by: Qwen3.8-27B-FP8, opencode > Signed-off-by: Jacob Tanenbaum <[email protected]> > > --- > > v3 > - changed the check on the delete to > if (!get_local_datapath(local_datapaths, port_list->dp_key) > - changed ip_mcast_querier_send_query() to send IPv6 mld regardless > of it IPv4 igmp succeeds or not. > - in the test "IGMP/MLD querier per-port query fan-out first query > at controller startup" I removed the racy OVS_WAIT_UNIT that > required n_packets=2 in favor of using pcap to count the number of > packets and make sure it increments by two. > > v2 > - changed the Reported-at: tag to be the upstream card > - Removal of querier port lists for datapaths that no longer exist > - changed several calls to HMAP_FOR_EACH -> HMAP_FOR_EACH_WITH_HASH > - consolidated ip_mcast_querier_ports_published() and > ip_mcast_qurier_get_ports() into ip_mcast_querier_fetch_ports() > - trimmed comments > the boundries of sending messages and that it can send out queries > to more ports than the original code based > - trimmed comments in controller/pinctrl.c > - WARN and drop the packet when queries are too big > - added test "IGMP/MLD querier per-port query fan-out" that tests > - added test "IGMP/MLD querier per-port query fan-out interval > cadence" that checks the query rounds arrive when they should in > the published state > - added test "IGMP/MLD querier per-port query fan-out first query > at controller startup" that shows the initial query arrives > immediatly and not at the end of an interval > - added test "IGMP/MLD querier per-port query fan-out empty flood > group" tests the published but empty list. > --- > controller/physical.c | 44 +++ > controller/pinctrl.c | 289 +++++++++++++++---- > controller/pinctrl.h | 17 ++ > tests/ovn.at | 643 ++++++++++++++++++++++++++++++++++++++++++ > 4 files changed, 936 insertions(+), 57 deletions(-) > > diff --git a/controller/physical.c b/controller/physical.c > index 8c5d9b940..554e611d6 100644 > --- a/controller/physical.c > +++ b/controller/physical.c > @@ -2922,6 +2922,22 @@ mc_ofctrl_add_flow(const struct > sbrec_multicast_group *mc, > ofpbuf_clear(&ctx->ofpacts); > } > > +/* Appends (port_key, ct_zone) to the array of flood_ports. */ > +static void > +flood_querier_port_add(struct mcast_querier_port *flood_ports, > + size_t *n_flood_ports, uint32_t port_key, > + int zone_id) > +{ > + if (!flood_ports) { > + return; > + } > + size_t i = *n_flood_ports; > + > + flood_ports[i].port_key = port_key; > + flood_ports[i].ct_zone = zone_id; > + (*n_flood_ports)++; > +} > + > static void > consider_mc_group(const struct physical_ctx *ctx, > const struct sbrec_multicast_group *mc, > @@ -2930,12 +2946,24 @@ consider_mc_group(const struct physical_ctx *ctx, > struct local_datapath *ldp = get_local_datapath(ctx->local_datapaths, > > mc->datapath->tunnel_key); > if (!ldp) { > + /* No local ports: publish an empty list so the querier can > + * distinguish this from a list that has not been published yet. > */ > + if (mc->tunnel_key == OVN_MCAST_FLOOD_TUNNEL_KEY) { > + pinctrl_mcast_querier_set_ports(mc->datapath->tunnel_key, > NULL, > + 0); > + } > return; > } > > struct sset remote_chassis = SSET_INITIALIZER(&remote_chassis); > struct sset vtep_chassis = SSET_INITIALIZER(&vtep_chassis); > > + struct mcast_querier_port *flood_ports = NULL; > + size_t n_flood_ports = 0; > + if (mc->tunnel_key == OVN_MCAST_FLOOD_TUNNEL_KEY) { > + flood_ports = xzalloc(mc->n_ports * sizeof *flood_ports); > + } > + > /* Go through all of the ports in the multicast group: > * > * - For remote ports, add the chassis to 'remote_chassis' or > @@ -3005,6 +3033,8 @@ consider_mc_group(const struct physical_ctx *ctx, > if (ldp->is_transit_switch) { > local_set_ct_zone_and_output_pb(port->tunnel_key, zone_id, > &local_ctx->ofpacts); > + flood_querier_port_add(flood_ports, &n_flood_ports, > + port->tunnel_key, zone_id); > } else { > local_output_pb(port->tunnel_key, &remote_ctx->ofpacts); > local_output_pb(port->tunnel_key, &ramp_ctx->ofpacts); > @@ -3026,9 +3056,13 @@ consider_mc_group(const struct physical_ctx *ctx, > || type == LP_L3GATEWAY)) { > local_set_ct_zone_and_output_pb(port->tunnel_key, zone_id, > &local_ctx->ofpacts); > + flood_querier_port_add(flood_ports, &n_flood_ports, > + port->tunnel_key, zone_id); > } else if (simap_contains(ctx->patch_ofports, > port->logical_port)) { > local_set_ct_zone_and_output_pb(port->tunnel_key, zone_id, > &local_ctx->ofpacts); > + flood_querier_port_add(flood_ports, &n_flood_ports, > + port->tunnel_key, zone_id); > } else if (type == LP_CHASSISREDIRECT > && port->chassis == ctx->chassis) { > const char *distributed_port = smap_get(&port->options, > @@ -3042,6 +3076,9 @@ consider_mc_group(const struct physical_ctx *ctx, > local_set_ct_zone_and_output_pb( > distributed_binding->tunnel_key, zone_id, > &local_ctx->ofpacts); > + flood_querier_port_add(flood_ports, &n_flood_ports, > + > distributed_binding->tunnel_key, > + zone_id); > } > } > } else if (!get_localnet_port(ctx->local_datapaths, > @@ -3068,6 +3105,13 @@ consider_mc_group(const struct physical_ctx *ctx, > } > } > > + /* Publish the local ports of the flood group to the querier. */ > + if (flood_ports) { > + pinctrl_mcast_querier_set_ports(mc->datapath->tunnel_key, > flood_ports, > + n_flood_ports); > + free(flood_ports); > + } > + > bool local_lports = local_ctx->ofpacts.size > 0; > bool remote_ports = remote_ctx->ofpacts.size > 0; > bool ramp_ports = ramp_ctx->ofpacts.size > 0; > diff --git a/controller/pinctrl.c b/controller/pinctrl.c > index 5c9634b24..f922d64b1 100644 > --- a/controller/pinctrl.c > +++ b/controller/pinctrl.c > @@ -5436,6 +5436,23 @@ static struct hmap mcast_snoop_map > OVS_GUARDED_BY(pinctrl_mutex); > */ > static struct ovs_list mcast_query_list; > > +/* > + * Local ports of each datapath's multicast flood group, published by > + * physical_run() (main thread) for the querier (pinctrl_handler thread). > + * > + * An entry exists once the datapath's flood group has been considered at > + * least once and may be empty when the flood group has no local ports on > + * this chassis. Both threads access the hmap under pinctrl_mutex. > + */ > +struct mcast_querier_port_list { > + struct hmap_node hmap_node; > + uint32_t dp_key; > + size_t n_ports; > + struct mcast_querier_port *ports; > +}; > + > +static struct hmap mcast_querier_ports OVS_GUARDED_BY(pinctrl_mutex); > + > /* Multicast config information stored independently by datapath key. > * Protected by pinctrl_mutex. pinctrl_handler has RO access and > pinctrl_main > * has RW access. Read accesses from pinctrl_ip_mcast_handle() can be > @@ -5754,6 +5771,7 @@ ip_mcast_snoop_init(void) > hmap_init(&mcast_snoop_map); > ovs_list_init(&mcast_query_list); > hmap_init(&mcast_cfg_map); > + hmap_init(&mcast_querier_ports); > } > > static void > @@ -5772,6 +5790,14 @@ ip_mcast_snoop_destroy(void) > HMAP_FOR_EACH_POP (ip_ms_state, hmap_node, &mcast_cfg_map) { > free(ip_ms_state); > } > + > + struct mcast_querier_port_list *port_list; > + > + HMAP_FOR_EACH_POP (port_list, hmap_node, &mcast_querier_ports) { > + free(port_list->ports); > + free(port_list); > + } > + hmap_destroy(&mcast_querier_ports); > } > > static void > @@ -5895,6 +5921,16 @@ ip_mcast_sync(struct ovsdb_idl_txn *ovnsb_idl_txn, > } > } > > + /* Remove the querier port lists of datapaths that no longer exist */ > + struct mcast_querier_port_list *port_list; > + HMAP_FOR_EACH_SAFE (port_list, hmap_node, &mcast_querier_ports) { > + if (!get_local_datapath(local_datapaths, port_list->dp_key)) { > + hmap_remove(&mcast_querier_ports, &port_list->hmap_node); > + free(port_list->ports); > + free(port_list); > + } > + } > + > const struct sbrec_igmp_group *sbrec_ip_mrouter; > const struct sbrec_igmp_group *sbrec_igmp; > > @@ -6204,10 +6240,12 @@ pinctrl_ip_mcast_handle(struct rconn *swconn, > } > } > > -static void > -ip_mcast_querier_send_igmp(struct rconn *swconn, struct ip_mcast_snoop > *ip_ms) > +/* Composes an IGMPv3 multicast query into 'buf' (up to 'buf_size' bytes) > and > + * returns its length in bytes. */ > +static size_t > +ip_mcast_querier_compose_igmp(struct ip_mcast_snoop *ip_ms, void *buf, > + size_t buf_size) > { > - /* Compose a multicast query. */ > uint64_t packet_stub[128 / 8]; > struct dp_packet packet; > > @@ -6223,35 +6261,25 @@ ip_mcast_querier_send_igmp(struct rconn *swconn, > struct ip_mcast_snoop *ip_ms) > uint8_t qqic = max_response; > packet_set_igmp3_query(&packet, max_response, 0, false, 0, qqic); > > - /* Inject multicast query. */ > - uint64_t ofpacts_stub[4096 / 8]; > - struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub); > - enum ofp_version version = rconn_get_version(swconn); > - put_load(ip_ms->dp_key, MFF_LOG_DATAPATH, 0, 64, &ofpacts); > - put_load(OVN_MCAST_FLOOD_TUNNEL_KEY, MFF_LOG_OUTPORT, 0, 32, > &ofpacts); > - put_load(1, MFF_LOG_FLAGS, MLF_LOCAL_ONLY, 1, &ofpacts); > - struct ofpact_resubmit *resubmit = ofpact_put_RESUBMIT(&ofpacts); > - resubmit->in_port = OFPP_CONTROLLER; > - resubmit->table_id = OFTABLE_LOCAL_OUTPUT; > - > - struct ofputil_packet_out po = { > - .packet = dp_packet_data(&packet), > - .packet_len = dp_packet_size(&packet), > - .buffer_id = UINT32_MAX, > - .ofpacts = ofpacts.data, > - .ofpacts_len = ofpacts.size, > - }; > - match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER); > - enum ofputil_protocol proto = > ofputil_protocol_from_ofp_version(version); > - queue_msg(swconn, ofputil_encode_packet_out(&po, proto)); > + size_t size = dp_packet_size(&packet); > + if (size > buf_size) { > + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5); > + VLOG_WARN_RL(&rl, "IGMP query (%"PRIuSIZE" bytes) too large for " > + "%"PRIuSIZE"-byte buffer", size, buf_size); > + dp_packet_uninit(&packet); > + return 0; > + } > + memcpy(buf, dp_packet_data(&packet), size); > dp_packet_uninit(&packet); > - ofpbuf_uninit(&ofpacts); > + return size; > } > > -static void > -ip_mcast_querier_send_mld(struct rconn *swconn, struct ip_mcast_snoop > *ip_ms) > +/* Composes an MLDv2 multicast query into 'buf' (up to 'buf_size' bytes) > and > + * returns its length in bytes. */ > +static size_t > +ip_mcast_querier_compose_mld(struct ip_mcast_snoop *ip_ms, void *buf, > + size_t buf_size) > { > - /* Compose a multicast query. */ > uint64_t packet_stub[128 / 8]; > struct dp_packet packet; > > @@ -6273,31 +6301,132 @@ ip_mcast_querier_send_mld(struct rconn *swconn, > struct ip_mcast_snoop *ip_ms) > struct in6_addr unspecified = { { { 0 } } }; > packet_set_mld_query(&packet, max_response, &unspecified, false, 0, > qqic); > > - /* Inject multicast query. */ > - uint64_t ofpacts_stub[4096 / 8]; > - struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub); > - enum ofp_version version = rconn_get_version(swconn); > - put_load(ip_ms->dp_key, MFF_LOG_DATAPATH, 0, 64, &ofpacts); > - put_load(OVN_MCAST_FLOOD_TUNNEL_KEY, MFF_LOG_OUTPORT, 0, 32, > &ofpacts); > - put_load(1, MFF_LOG_FLAGS, MLF_LOCAL_ONLY, 1, &ofpacts); > - struct ofpact_resubmit *resubmit = ofpact_put_RESUBMIT(&ofpacts); > - resubmit->in_port = OFPP_CONTROLLER; > - resubmit->table_id = OFTABLE_LOCAL_OUTPUT; > - > - struct ofputil_packet_out po = { > - .packet = dp_packet_data(&packet), > - .packet_len = dp_packet_size(&packet), > - .buffer_id = UINT32_MAX, > - .ofpacts = ofpacts.data, > - .ofpacts_len = ofpacts.size, > - }; > - match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER); > - enum ofputil_protocol proto = > ofputil_protocol_from_ofp_version(version); > - queue_msg(swconn, ofputil_encode_packet_out(&po, proto)); > + size_t size = dp_packet_size(&packet); > + if (size > buf_size) { > + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5); > + VLOG_WARN_RL(&rl, "MLD query (%"PRIuSIZE" bytes) too large for " > + "%"PRIuSIZE"-byte buffer", size, buf_size); > + dp_packet_uninit(&packet); > + return 0; > + } > + memcpy(buf, dp_packet_data(&packet), size); > dp_packet_uninit(&packet); > - ofpbuf_uninit(&ofpacts); > + return size; > +} > + > +/* Number of local ports per querier PACKET_OUT. > + * > + * (MCAST_QUERIER_PORT_BATCH) * (resubmits in egress pipeline) must be > + * less than MAX_RESUBMITS. */ > +#define MCAST_QUERIER_PORT_BATCH 100 > + > +/* Sends query to each port in ports in batches of at most > + * MCAST_QUERIER_PORT_BATCH ports per PACKET_OUT. */ > +static void > +ip_mcast_querier_send_port_fanout(struct rconn *swconn, > + struct ip_mcast_snoop *ip_ms, > + const void *query, size_t query_len, > + const struct mcast_querier_port *ports, > + size_t n_ports) > +{ > + enum ofputil_protocol proto = ofputil_protocol_from_ofp_version( > + rconn_get_version(swconn)); > + > + for (size_t start = 0; start < n_ports; > + start += MCAST_QUERIER_PORT_BATCH) { > + size_t end = MIN(start + MCAST_QUERIER_PORT_BATCH, n_ports); > + > + struct ofpbuf ofpacts; > + ofpbuf_init(&ofpacts, 0); > + put_load(ip_ms->dp_key, MFF_LOG_DATAPATH, 0, 64, &ofpacts); > + for (size_t i = start; i < end; i++) { > + const struct mcast_querier_port *port = &ports[i]; > + > + if (port->ct_zone) { > + put_load(port->ct_zone, MFF_LOG_CT_ZONE, 0, 16, &ofpacts); > + } > + put_load(port->port_key, MFF_LOG_OUTPORT, 0, 32, &ofpacts); > + struct ofpact_resubmit *resubmit = > ofpact_put_RESUBMIT(&ofpacts); > + resubmit->in_port = OFPP_CONTROLLER; > + resubmit->table_id = OFTABLE_CHECK_LOOPBACK; > + } > + > + struct ofputil_packet_out po = { > + .packet = query, > + .packet_len = query_len, > + .buffer_id = UINT32_MAX, > + .ofpacts = ofpacts.data, > + .ofpacts_len = ofpacts.size, > + }; > + match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER); > + queue_msg(swconn, ofputil_encode_packet_out(&po, proto)); > + ofpbuf_uninit(&ofpacts); > + } > +} > + > +/* Returns a copy of the local ports published for datapath dp_key or > NULL if > + * the list has not been published yet or is empty and if the list of > local > + * ports has been published. > + * > + * Takes pinctrl_mutex internally. */ > +static struct mcast_querier_port * > +ip_mcast_querier_fetch_ports(int64_t dp_key, size_t *n_ports, bool > *published) > +{ > + *published = false; > + ovs_mutex_lock(&pinctrl_mutex); > + > + size_t n = 0; > + struct mcast_querier_port *ports = NULL; > + struct mcast_querier_port_list *list; > + HMAP_FOR_EACH_WITH_HASH (list, hmap_node, ip_mcast_snoop_hash(dp_key), > + &mcast_querier_ports) { > + if (list->dp_key != dp_key) { > + continue; > + } > + *published = true; > + n = list->n_ports; > + if (n) { > + ports = xmalloc(n * sizeof *ports); > + memcpy(ports, list->ports, n * sizeof *ports); > + } > + break; > + } > + > + ovs_mutex_unlock(&pinctrl_mutex); > + > + *n_ports = n; > + return ports; > } > > +/* Composes and sends the IGMPv3 and MLDv2 queries (if enabled) to the > given > + * local ports. */ > +static void > +ip_mcast_querier_send_query(struct rconn *swconn, struct ip_mcast_snoop > *ip_ms, > + const struct mcast_querier_port *ports, > + size_t n_ports) > +{ > + char query_buf[128]; > + if (ip_ms->cfg.querier_v4_enabled) { > + size_t query_len = ip_mcast_querier_compose_igmp(ip_ms, query_buf, > + sizeof > query_buf); > + if (query_len) { > + ip_mcast_querier_send_port_fanout(swconn, ip_ms, query_buf, > + query_len, ports, n_ports); > + } > + } > + if (ip_ms->cfg.querier_v6_enabled) { > + size_t query_len = ip_mcast_querier_compose_mld(ip_ms, query_buf, > + sizeof query_buf); > + if (query_len) { > + ip_mcast_querier_send_port_fanout(swconn, ip_ms, query_buf, > + query_len, ports, n_ports); > + } > + } > +} > + > +/* Retry interval (ms) while the flood group's local ports are > unpublished. */ > +#define MCAST_QUERIER_INITIAL_RETRY_MS 1000 > + > static long long int > ip_mcast_querier_send(struct rconn *swconn, struct ip_mcast_snoop *ip_ms, > long long int current_time) > @@ -6306,16 +6435,26 @@ ip_mcast_querier_send(struct rconn *swconn, struct > ip_mcast_snoop *ip_ms, > return ip_ms->query_time_ms; > } > > - if (ip_ms->cfg.querier_v4_enabled) { > - ip_mcast_querier_send_igmp(swconn, ip_ms); > - } > + size_t n_ports; > + bool published; > + struct mcast_querier_port *ports = > + ip_mcast_querier_fetch_ports(ip_ms->dp_key, &n_ports, &published); > > - if (ip_ms->cfg.querier_v6_enabled) { > - ip_mcast_querier_send_mld(swconn, ip_ms); > + /* Retry at a short backoff until the port list is published. */ > + long long int interval_ms = > + published ? ip_ms->cfg.query_interval_s * 1000 > + : MCAST_QUERIER_INITIAL_RETRY_MS; > + > + ip_ms->query_time_ms = current_time + interval_ms; > + > + /* Skip if the flood group's local ports are unpublished or empty. */ > + if (!ports) { > + return ip_ms->query_time_ms; > } > > - /* Set the next query time. */ > - ip_ms->query_time_ms = current_time + ip_ms->cfg.query_interval_s * > 1000; > + ip_mcast_querier_send_query(swconn, ip_ms, ports, n_ports); > + free(ports); > + > return ip_ms->query_time_ms; > } > > @@ -6349,6 +6488,42 @@ ip_mcast_querier_wait(long long int query_time) > } > } > > +/* Sets the local ports of the flood group for datapath dp_key. > + * ports may be NULL to store and empty list which lets the querier > + * distinguish "no local ports" from "not yet published". */ > +void > +pinctrl_mcast_querier_set_ports(int64_t dp_key, > + const struct mcast_querier_port *ports, > + size_t n_ports) > +{ > + ovs_mutex_lock(&pinctrl_mutex); > + > + struct mcast_querier_port_list *list; > + HMAP_FOR_EACH_WITH_HASH (list, hmap_node, ip_mcast_snoop_hash(dp_key), > + &mcast_querier_ports) { > + if (list->dp_key == dp_key) { > + break; > + } > + } > + if (!list) { > + list = xzalloc(sizeof *list); > + list->dp_key = dp_key; > + hmap_insert(&mcast_querier_ports, &list->hmap_node, > + ip_mcast_snoop_hash(dp_key)); > + } > + > + free(list->ports); > + if (n_ports) { > + list->ports = xmalloc(n_ports * sizeof *list->ports); > + memcpy(list->ports, ports, n_ports * sizeof *list->ports); > + } else { > + list->ports = NULL; > + } > + list->n_ports = n_ports; > + > + ovs_mutex_unlock(&pinctrl_mutex); > +} > + > static void > send_garp_rarp_wait(long long int send_garp_rarp_time) > { > diff --git a/controller/pinctrl.h b/controller/pinctrl.h > index a638fe29f..3e38ebf51 100644 > --- a/controller/pinctrl.h > +++ b/controller/pinctrl.h > @@ -86,4 +86,21 @@ void send_self_originated_neigh_packet(struct rconn > *swconn, > struct in6_addr *local, > struct in6_addr *target, > uint8_t table_id); > + > +/* A local port that the multicast flood group would deliver a query to, > along > + * with the conntrack zone that the flood group's per-port action loads. > */ > +struct mcast_querier_port { > + uint32_t port_key; > + uint16_t ct_zone; > +}; > + > +/* Records the local ports of the flood group for datapath 'dp_key', so > the > + * querier can send its query to each port directly rather than through > the > + * flood group. 'ports' may be NULL to record an empty list. > + * > + * Called from the main thread; read by the handler thread. > + * Takes pinctrl_mutex internally. */ > +void pinctrl_mcast_querier_set_ports(int64_t dp_key, > + const struct mcast_querier_port > *ports, > + size_t n_ports); > #endif /* controller/pinctrl.h */ > diff --git a/tests/ovn.at b/tests/ovn.at > index 49674266f..780149467 100644 > --- a/tests/ovn.at > +++ b/tests/ovn.at > @@ -26150,6 +26150,649 @@ OVN_CLEANUP([hv1], [hv2]) > AT_CLEANUP > ]) > > +OVN_FOR_EACH_NORTHD([ > +AT_SETUP([IGMP/MLD querier per-port query fan-out]) > +AT_KEYWORDS([IP-multicast snoop querier]) > +AT_KEYWORDS([slowtest]) > + > +ovn_start > + > +dnl A single chassis with a large number of local VIFs on one logical > +dnl switch. Each port's querier query egress translation uses ~19 > +dnl resubmits, so delivering the queries to all 250 local ports in a > +dnl single flood-group PACKET_OUT would exceed OVS's MAX_RESUBMITS > +dnl (4096) budget. Instead, the querier sends one PACKET_OUT per > +dnl batch of at most 100 ports. Verify end to end that (1) every > +dnl local port receives both the IGMP and the MLD queries, with the > +dnl right contents, and (2) the number of OFPT_PACKET_OUT messages > +dnl matches the batch count: three per query type with 250 local > +dnl ports, two with 101, one with 100. > +dnl > +dnl sim_add() starts ovs-vswitchd with -vvconn, so every OpenFlow > +dnl message received from ovn-controller is dumped to > +dnl ovs-vswitchd.log. Counting the "received: OFPT_PACKET_OUT" log > +dnl lines therefore counts the messages the querier actually sent. > + > +n_ports=250 > + > +check ovn-nbctl ls-add ls1 > + > +# Create the VIF ports on ls1 in batches. > +batch="" > +for i in $(seq 1 $n_ports); do > + batch="$batch -- lsp-add ls1 ls1-p$i" > + if test $((i % 50)) -eq 0; then > + check ovn-nbctl $batch > + batch="" > + fi > +done > +if test -n "$batch"; then > + check ovn-nbctl $batch > +fi > + > +net_add n1 > +sim_add hv1 > +as hv1 > +check ovs-vsctl add-br br-phys > +ovn_attach n1 br-phys 192.168.0.1 > +check ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys > + > +# Create the VIFs in batches, keeping tx pcaps on a few of them. > +batch="" > +for i in $(seq 1 $n_ports); do > + if test $i -eq 1 -o $i -eq 125 -o $i -eq $n_ports; then > + batch="$batch -- add-port br-int hv1-vif$i -- \ > + set interface hv1-vif$i external-ids:iface-id=ls1-p$i \ > + options:tx_pcap=hv1/vif$i-tx.pcap \ > + options:rxq_pcap=hv1/vif$i-rx.pcap" > + else > + batch="$batch -- add-port br-int hv1-vif$i -- \ > + set interface hv1-vif$i external-ids:iface-id=ls1-p$i" > + fi > + if test $((i % 25)) -eq 0; then > + check ovs-vsctl $batch > + batch="" > + fi > +done > +if test -n "$batch"; then > + check ovs-vsctl $batch > +fi > + > +wait_for_ports_up > +check ovn-nbctl --wait=hv sync > + > +# Enable multicast snooping and querier on ls1. Use the maximum > +# query interval so that only the immediate query round fired when > +# the querier is enabled (and, later, when the interval changes) > +# happens during the test. > +check ovn-nbctl --wait=hv set Logical_Switch ls1 \ > + other_config:mcast_snoop="true" \ > + other_config:mcast_querier="true" \ > + other_config:mcast_query_interval=3600 \ > + other_config:mcast_eth_src="00:00:00:00:02:fe" \ > + other_config:mcast_ip4_src="20.0.0.254" \ > + other_config:mcast_ip6_src="fe80::fe" > + > +# Wait for pinctrl thread to be connected > +OVS_WAIT_UNTIL([grep pinctrl hv1/ovn-controller.log | grep -c connected]) > + > +dp_key=$(printf "%x" $(fetch_column Datapath_Binding tunnel_key > external_ids:name=ls1)) > + > +AS_BOX([Querier queries are fanned out to all local ports]) > +dnl Every local port has its own output flow in table 89, the last > +dnl logical egress table. The querier sends one IGMP query and one > +dnl MLD query per port per round, so once each per-port flow has > +dnl counted both packets, every local port has received both > +dnl queries. > +OVS_WAIT_UNTIL( > + [test $(as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY | > grep -c "metadata=0x$dp_key ") -eq $n_ports \ > + -a $(as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY | > grep "metadata=0x$dp_key " | grep -c "n_packets=2,") -eq $n_ports], > + [as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY]) > + > +AS_BOX([Querier query contents]) > +AT_CAPTURE_FILE([exp]) > +AT_CAPTURE_FILE([rcv]) > +check_packets() { > + > exp > + > rcv > + if test "$1" = --uniq; then > + sort="sort -u"; shift > + else > + sort=sort > + fi > + for tuple in "$@"; do > + set $tuple; pcap=$1 type=$2 > + echo "--- $pcap" | tee -a exp >> rcv > + $sort "$type" >> exp > + $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $pcap | $sort >> rcv > + echo | tee -a exp >> rcv > + done > + > + $at_diff exp rcv >/dev/null > +} > + > +> expected > +store_igmp_v3_query 0000000002fe $(ip_to_hex 20 0 0 254) 84dd expected > +store_mld_query 0000000002fe fe8000000000000000000000000000fe expected > + > +OVS_WAIT_UNTIL( > + [check_packets --uniq 'hv1/vif1-tx.pcap expected' \ > + 'hv1/vif125-tx.pcap expected' \ > + 'hv1/vif250-tx.pcap expected'], > + [$at_diff -F'^---' exp rcv]) > + > +AS_BOX([250 local ports require three messages per query type]) > +dnl One round over 250 local ports: 3 batches of IGMP + 3 batches > +dnl of MLD, i.e. 6 OFPT_PACKET_OUT messages in total. The round is > +dnl known to be fully delivered at this point (see above), so the > +dnl count is exact. > +OVS_WAIT_UNTIL( > + [test $(grep -c "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log) -eq > 6], > + [grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log]) > + > +AS_BOX([101 local ports require two messages per query type]) > +dnl Drop the ports above 101. Changing the query interval triggers > +dnl an immediate next query round (see ip_mcast_snoop_configure() in > +dnl ovn-controller), now over the 101 remaining ports, i.e. two > +dnl messages per query type, four in total. > +batch="" > +for i in $(seq 102 $n_ports); do > + batch="$batch -- lsp-del ls1-p$i" > + if test $((i % 50)) -eq 0; then > + check ovn-nbctl $batch > + batch="" > + fi > +done > +if test -n "$batch"; then > + check ovn-nbctl $batch > +fi > + > +batch="" > +for i in $(seq 102 $n_ports); do > + batch="$batch -- del-port br-int hv1-vif$i" > + if test $((i % 25)) -eq 0; then > + check ovs-vsctl $batch > + batch="" > + fi > +done > +if test -n "$batch"; then > + check ovs-vsctl $batch > +fi > + > +check ovn-nbctl --wait=hv sync > +check ovn-nbctl --wait=hv set Logical_Switch ls1 > other_config:mcast_query_interval=1800 > + > +OVS_WAIT_UNTIL( > + [test $(as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY | > grep -c "metadata=0x$dp_key ") -eq 101 \ > + -a $(as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY | > grep "metadata=0x$dp_key " | grep -c "n_packets=4,") -eq 101], > + [as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY]) > + > +dnl Two rounds in total: 6 messages from the 250-port round plus 2 > +dnl batches of IGMP + 2 batches of MLD over the 101 remaining ports. > +OVS_WAIT_UNTIL( > + [test $(grep -c "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log) -eq > 10], > + [grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log]) > + > +AS_BOX([100 local ports require one message per query type]) > +dnl Drop one more local port. Changing the query interval again > +dnl triggers another immediate query round, now over the 100 > +dnl remaining ports, i.e. one message per query type, two in total. > +check ovn-nbctl lsp-del ls1-p101 > +check ovs-vsctl --if-exists del-port br-int hv1-vif101 > +check ovn-nbctl --wait=hv sync > +check ovn-nbctl --wait=hv set Logical_Switch ls1 > other_config:mcast_query_interval=900 > + > +OVS_WAIT_UNTIL( > + [test $(as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY | > grep -c "metadata=0x$dp_key ") -eq 100 \ > + -a $(as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY | > grep "metadata=0x$dp_key " | grep -c "n_packets=6,") -eq 100], > + [as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY]) > + > +dnl Three rounds in total: the 6 + 4 messages above plus 1 batch of > +dnl IGMP + 1 batch of MLD over the 100 remaining ports. > +OVS_WAIT_UNTIL( > + [test $(grep -c "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log) -eq > 12], > + [grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log]) > + > +OVN_CLEANUP([hv1]) > +AT_CLEANUP > +]) > + > +OVN_FOR_EACH_NORTHD([ > +AT_SETUP([IGMP/MLD querier per-port query fan-out interval cadence]) > +AT_KEYWORDS([IP-multicast snoop querier]) > +AT_KEYWORDS([slowtest]) > + > +ovn_start > + > +dnl A single chassis with two local VIFs on one logical switch. > +dnl Each querier round sends one IGMP query and one MLD query (a > +dnl single batch for both ports), so every round shows up as two > +dnl "received: OFPT_PACKET_OUT" lines in ovs-vswitchd.log, which > +dnl sim_add() starts with -vvconn. > +dnl > +dnl Verify that (1) consecutive rounds are spaced by the configured > +dnl 3s query interval, (2) extending the interval does not reset the > +dnl pending round, and (3) shortening the interval triggers an > +dnl immediate round. > +dnl > +dnl Known limitation: the interval extension is applied by the > +dnl controller asynchronously. The assertions assume it is applied > +dnl within a couple of seconds; on a heavily loaded system an extra > +dnl round could fire and the exact count checks would fail. > + > +check ovn-nbctl ls-add ls1 > +check ovn-nbctl lsp-add ls1 ls1-p1 > +check ovn-nbctl lsp-add ls1 ls1-p2 > + > +net_add n1 > +sim_add hv1 > +as hv1 > +check ovs-vsctl add-br br-phys > +ovn_attach n1 br-phys 192.168.0.1 > +check ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys > +check ovs-vsctl add-port br-int hv1-vif1 -- \ > + set interface hv1-vif1 external-ids:iface-id=ls1-p1 \ > + options:tx_pcap=hv1/vif1-tx.pcap > +check ovs-vsctl add-port br-int hv1-vif2 -- \ > + set interface hv1-vif2 external-ids:iface-id=ls1-p2 \ > + options:tx_pcap=hv1/vif2-tx.pcap > + > +wait_for_ports_up > +check ovn-nbctl --wait=hv sync > + > +# Enable multicast snooping and querier on ls1 with a short query > +# interval so that several query rounds fire during the test. > +check ovn-nbctl --wait=hv set Logical_Switch ls1 \ > + other_config:mcast_snoop="true" \ > + other_config:mcast_querier="true" \ > + other_config:mcast_query_interval=3 \ > + other_config:mcast_eth_src="00:00:00:00:02:fe" \ > + other_config:mcast_ip4_src="20.0.0.254" \ > + other_config:mcast_ip6_src="fe80::fe" > + > +# Wait for pinctrl thread to be connected > +OVS_WAIT_UNTIL([grep pinctrl hv1/ovn-controller.log | grep -c connected]) > + > +dp_key=$(printf "%x" $(fetch_column Datapath_Binding tunnel_key > external_ids:name=ls1)) > + > +packet_out_count() { > + grep -c "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log > +} > +packet_out_ts() { > + dnl Prints the send time (in ms) of the $1-th OFPT_PACKET_OUT > + dnl message in hv1/ovs-vswitchd.log. The first field of a log > + dnl line is the UTC timestamp with millisecond precision. > + grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log \ > + | awk -F'|' -v n="$1" 'NR==n { print $1 }' \ > + | while read -r ts; do date -d "$ts" +%s%3N; done > +} > +round_time() { > + dnl The IGMP query of round $1 is the (2n-1)-th OFPT_PACKET_OUT. > + packet_out_ts $((2 * $1 - 1)) > +} > +check_interval_gap() { > + gap=$(( $2 - $1 )) > + check test "$gap" -ge $3 -a "$gap" -le $4 > +} > + > +AS_BOX([Query rounds follow the configured interval]) > +OVS_WAIT_UNTIL( > + [test $(packet_out_count) -ge 2], > + [grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log]) > +r1=$(round_time 1) > +OVS_WAIT_UNTIL( > + [test $(packet_out_count) -ge 8], > + [grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log]) > +r2=$(round_time 2) > +r3=$(round_time 3) > +r4=$(round_time 4) > + > +check_interval_gap $r1 $r2 2600 3600 > +check_interval_gap $r2 $r3 2600 3600 > +check_interval_gap $r3 $r4 2600 3600 > +total=$(( r4 - r1 )) > +check test "$total" -ge 8000 -a "$total" -le 12000 > + > +AS_BOX([Extending the interval keeps the pending round]) > +dnl The pending fifth round was scheduled with the old 3s interval, > +dnl so it must still fire on time even after the interval is > +dnl extended. > +check ovn-nbctl --wait=hv set Logical_Switch ls1 > other_config:mcast_query_interval=3600 > + > +OVS_WAIT_UNTIL( > + [test $(packet_out_count) -ge 10], > + [grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log]) > +r5=$(round_time 5) > +check_interval_gap $r4 $r5 2600 4500 > + > +dnl If the controller had not applied the extended interval before > +dnl the fifth round was rescheduled, a sixth round would have fired > +dnl right after it. > +sleep 4 > +check test $(packet_out_count) -eq 10 > + > +AS_BOX([Shortening the interval triggers an immediate round]) > +check ovn-nbctl --wait=hv set Logical_Switch ls1 > other_config:mcast_query_interval=1800 > + > +OVS_WAIT_UNTIL( > + [test $(packet_out_count) -eq 12], > + [grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log]) > +r6=$(round_time 6) > +dnl The immediate round fires well before the new 1800s interval. > +check test $(( r6 - r5 )) -le 10000 > + > +OVS_WAIT_UNTIL( > + [test $(as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY | > grep -c "metadata=0x$dp_key ") -eq 2 \ > + -a $(as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY | > grep "metadata=0x$dp_key " | grep -c "n_packets=12,") -eq 2], > + [as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY]) > + > +AS_BOX([Querier query contents]) > +AT_CAPTURE_FILE([exp]) > +AT_CAPTURE_FILE([rcv]) > +check_packets() { > + > exp > + > rcv > + if test "$1" = --uniq; then > + sort="sort -u"; shift > + else > + sort=sort > + fi > + for tuple in "$@"; do > + set $tuple; pcap=$1 type=$2 > + echo "--- $pcap" | tee -a exp >> rcv > + $sort "$type" >> exp > + $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $pcap | $sort >> rcv > + echo | tee -a exp >> rcv > + done > + > + $at_diff exp rcv >/dev/null > +} > + > +> expected > +store_igmp_v3_query 0000000002fe $(ip_to_hex 20 0 0 254) 84dd expected > +store_mld_query 0000000002fe fe8000000000000000000000000000fe expected > + > +OVS_WAIT_UNTIL( > + [check_packets --uniq 'hv1/vif1-tx.pcap expected' \ > + 'hv1/vif2-tx.pcap expected'], > + [$at_diff -F'^---' exp rcv]) > + > +OVN_CLEANUP([hv1]) > +AT_CLEANUP > +]) > + > +OVN_FOR_EACH_NORTHD([ > +AT_SETUP([IGMP/MLD querier per-port query fan-out first query at > controller startup]) > +AT_KEYWORDS([IP-multicast snoop querier]) > +AT_KEYWORDS([slowtest]) > + > +ovn_start > + > +dnl Multicast snooping and the querier are configured on ls1 before > +dnl the controller starts. When the VIF on ls1 is bound, the > +dnl querier state for the switch is created and must fire an > +dnl immediate first query round, without waiting for the (maximum) > +dnl query interval to elapse. > +dnl > +dnl Known limitations: (1) the immediate round is observed through > +dnl the OFPT_PACKET_OUT messages it generates; (2) the first round > +dnl may fire before the VIF's egress OpenFlow pipeline is installed, > +dnl in which case its packets are dropped on the way to the port. > +dnl The arrival and content checks therefore run against a second > +dnl round, triggered immediately by shortening the query interval, > +dnl whose packets are certain to reach the port. > + > +check ovn-nbctl ls-add ls1 > +check ovn-nbctl lsp-add ls1 ls1-p1 > + > +# Enable multicast snooping and querier on ls1 before the controller > +# starts. Use the maximum query interval so that, if any query round > +# fires during the test, it must be the immediate first one. > +check ovn-nbctl set Logical_Switch ls1 \ > + other_config:mcast_snoop="true" \ > + other_config:mcast_querier="true" \ > + other_config:mcast_query_interval=3600 \ > + other_config:mcast_eth_src="00:00:00:00:02:fe" \ > + other_config:mcast_ip4_src="20.0.0.254" \ > + other_config:mcast_ip6_src="fe80::fe" > + > +dnl Wait until northd has processed the snoop configuration. > +OVS_WAIT_UNTIL([test -n "$(fetch_column Datapath_Binding _uuid > external_ids:name=ls1)"]) > +dp_uuid=$(fetch_column Datapath_Binding _uuid external_ids:name=ls1) > +wait_column 3600 sb:IP_Multicast query_interval datapath=$dp_uuid > + > +net_add n1 > +sim_add hv1 > +as hv1 > +check ovs-vsctl add-br br-phys > +ovn_attach n1 br-phys 192.168.0.1 > +check ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys > +check ovs-vsctl add-port br-int hv1-vif1 -- \ > + set interface hv1-vif1 external-ids:iface-id=ls1-p1 \ > + options:tx_pcap=hv1/vif1-tx.pcap > + > +wait_for_ports_up > + > +# Wait for pinctrl thread to be connected > +OVS_WAIT_UNTIL([grep pinctrl hv1/ovn-controller.log | grep -c connected]) > + > +packet_out_count() { > + grep -c "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log > +} > + > +pcap_count() { > + $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap | wc -l > +} > + > +AS_BOX([The first query round fires immediately]) > +dnl One IGMP query and one MLD query are sent without waiting for > +dnl the (maximum) query interval to elapse. > +OVS_WAIT_UNTIL( > + [test $(packet_out_count) -eq 2], > + [grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log]) > + > +dnl The first round's packets may not have reached the port yet (or > +dnl may be dropped entirely if the egress pipeline is not installed); > +dnl record the captured count either way. > +first_round_count=$(pcap_count) > + > +dnl Let the VIF's egress pipeline settle, then trigger an immediate > +dnl second round by shortening the interval. Its packets must > +dnl reach the port. > +sleep 2 > +check ovn-nbctl --wait=hv set Logical_Switch ls1 > other_config:mcast_query_interval=1800 > + > +OVS_WAIT_UNTIL( > + [test $(packet_out_count) -eq 4], > + [grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log]) > + > +dnl The second round must add its two queries to the capture, so the > +dnl two sets of packets arrive separately. > +OVS_WAIT_UNTIL( > + [test $(pcap_count) -ge $((first_round_count + 2))], > + [echo "pcap has $(pcap_count) packets, expected at least > $((first_round_count + 2))"]) > + > +AS_BOX([Querier query contents]) > +AT_CAPTURE_FILE([exp]) > +AT_CAPTURE_FILE([rcv]) > +check_packets() { > + > exp > + > rcv > + if test "$1" = --uniq; then > + sort="sort -u"; shift > + else > + sort=sort > + fi > + for tuple in "$@"; do > + set $tuple; pcap=$1 type=$2 > + echo "--- $pcap" | tee -a exp >> rcv > + $sort "$type" >> exp > + $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $pcap | $sort >> rcv > + echo | tee -a exp >> rcv > + done > + > + $at_diff exp rcv >/dev/null > +} > + > +> expected > +store_igmp_v3_query 0000000002fe $(ip_to_hex 20 0 0 254) 84dd expected > +store_mld_query 0000000002fe fe8000000000000000000000000000fe expected > + > +OVS_WAIT_UNTIL( > + [check_packets --uniq 'hv1/vif1-tx.pcap expected'], > + [$at_diff -F'^---' exp rcv]) > + > +dnl The next round is a full query interval away. > +sleep 10 > +check test $(packet_out_count) -eq 4 > + > +OVN_CLEANUP([hv1]) > +AT_CLEANUP > +]) > + > +OVN_FOR_EACH_NORTHD([ > +AT_SETUP([IGMP/MLD querier per-port query fan-out empty flood group]) > +AT_KEYWORDS([IP-multicast snoop querier]) > +AT_KEYWORDS([slowtest]) > + > +ovn_start > + > +dnl A switch connected to a router through a static router port. > +dnl The router port belongs to the _MC_flood group, but its queries > +dnl always leave through the remote output and never to a local > +dnl port, so the switch's local flood port list stays empty. A VIF > +dnl with receive_multicast disabled is claimed by the chassis (which > +dnl makes the switch a local datapath) without joining the > +dnl _MC_flood group. > +dnl > +dnl With an empty (but published) flood port list the querier must > +dnl not send queries and must reschedule the next round at the full > +dnl query interval, not the 1s unpublished retry. Adding a port to > +dnl the flood group must not trigger an immediate round; shortening > +dnl the interval does. > +dnl > +dnl Known limitation: the "no queries sent" phases rely on the flood > +dnl port list being published before the querier state is created, > +dnl which holds in the current implementation because the port list > +dnl is published within the same controller loop iteration in which > +dnl the switch becomes local. > + > +check ovn-nbctl lr-add lr1 > +check ovn-nbctl lrp-add lr1 lrp1 f0:00:00:00:00:f1 192.168.1.1/24 > +check ovn-nbctl ls-add ls1 > +check ovn-nbctl lsp-add ls1 ls1-lrp1 > +check ovn-nbctl set Logical_Switch_Port ls1-lrp1 \ > + type=router \ > + options:router-port=lrp1 \ > + addresses=router > + > +# The VIF makes ls1 a local datapath on hv1 without joining the > +# _MC_flood group. > +check ovn-nbctl lsp-add ls1 ls1-p1 > +check ovn-nbctl set Logical_Switch_Port ls1-p1 > options:receive_multicast=false > + > +net_add n1 > +sim_add hv1 > +as hv1 > +check ovs-vsctl add-br br-phys > +ovn_attach n1 br-phys 192.168.0.1 > +check ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys > +check ovs-vsctl add-port br-int hv1-vif1 -- \ > + set interface hv1-vif1 external-ids:iface-id=ls1-p1 > + > +wait_for_ports_up > + > +# Enable multicast snooping and querier on ls1 with the maximum > +# query interval. > +check ovn-nbctl --wait=hv set Logical_Switch ls1 \ > + other_config:mcast_snoop="true" \ > + other_config:mcast_querier="true" \ > + other_config:mcast_query_interval=3600 \ > + other_config:mcast_eth_src="00:00:00:00:02:fe" \ > + other_config:mcast_ip4_src="20.0.0.254" \ > + other_config:mcast_ip6_src="fe80::fe" > + > +# Wait for pinctrl thread to be connected > +OVS_WAIT_UNTIL([grep pinctrl hv1/ovn-controller.log | grep -c connected]) > + > +dp_key=$(printf "%x" $(fetch_column Datapath_Binding tunnel_key > external_ids:name=ls1)) > + > +packet_out_count() { > + grep -c "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log > +} > + > +AS_BOX([No queries are sent while the flood group has no local ports]) > +dnl The first query round finds an empty (published) local flood > +dnl port list and reschedules at the full interval; well before the > +dnl next due time no queries may have been sent. > +sleep 8 > +check test $(packet_out_count) -eq 0 > + > +AS_BOX([Adding a port to the flood group does not trigger a round]) > +dnl Add a regular VIF. It joins the _MC_flood group, so the flood > +dnl port list is no longer empty, but the pending round is still a > +dnl full interval away. > +check ovn-nbctl lsp-add ls1 ls1-p2 > +check ovs-vsctl add-port br-int hv1-vif2 -- \ > + set interface hv1-vif2 external-ids:iface-id=ls1-p2 \ > + options:tx_pcap=hv1/vif2-tx.pcap > +wait_for_ports_up > + > +sleep 8 > +check test $(packet_out_count) -eq 0 > + > +AS_BOX([Shortening the interval triggers an immediate round]) > +check ovn-nbctl --wait=hv set Logical_Switch ls1 > other_config:mcast_query_interval=1800 > + > +OVS_WAIT_UNTIL( > + [test $(packet_out_count) -eq 2], > + [grep "received: OFPT_PACKET_OUT" hv1/ovs-vswitchd.log]) > + > +dnl Both queries went to the new port only: the VIF flow is the only > +dnl flow of the datapath that counts packets (the router port's > +dnl flows do not receive the queries). > +OVS_WAIT_UNTIL( > + [test $(as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY | > grep "metadata=0x$dp_key " | grep -c "n_packets=2,") -eq 1], > + [as hv1 ovs-ofctl dump-flows br-int table=OFTABLE_LOG_TO_PHY]) > + > +AS_BOX([Querier query contents]) > +AT_CAPTURE_FILE([exp]) > +AT_CAPTURE_FILE([rcv]) > +check_packets() { > + > exp > + > rcv > + if test "$1" = --uniq; then > + sort="sort -u"; shift > + else > + sort=sort > + fi > + for tuple in "$@"; do > + set $tuple; pcap=$1 type=$2 > + echo "--- $pcap" | tee -a exp >> rcv > + $sort "$type" >> exp > + $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $pcap | $sort >> rcv > + echo | tee -a exp >> rcv > + done > + > + $at_diff exp rcv >/dev/null > +} > + > +> expected > +store_igmp_v3_query 0000000002fe $(ip_to_hex 20 0 0 254) 84dd expected > +store_mld_query 0000000002fe fe8000000000000000000000000000fe expected > + > +OVS_WAIT_UNTIL( > + [check_packets --uniq 'hv1/vif2-tx.pcap expected'], > + [$at_diff -F'^---' exp rcv]) > + > +dnl The next round is a full query interval away. > +sleep 10 > +check test $(packet_out_count) -eq 2 > + > +OVN_CLEANUP([hv1]) > +AT_CLEANUP > +]) > + > OVN_FOR_EACH_NORTHD([ > AT_SETUP([unixctl socket]) > ovn_start > -- > 2.56.0 > > _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
