On Tue, 2025-02-25 at 15:59 +0100, Dumitru Ceara wrote: > On 2/25/25 3:28 PM, martin.kal...@canonical.com wrote: > > On Tue, 2025-02-25 at 00:01 +0100, Dumitru Ceara wrote: > > > Change the en_dynamic_routes I-P node to compute a map of > > > 'advertised > > > routes' (struct ar_entry) instead of parsed routes. Like that we > > > can > > > avoid having to re-parse NAT/LB IPs when advertising them. > > > > > > Fixes: cd4ad2f56179 ("northd: Redistribution of NAT/LB routes.") > > > Signed-off-by: Dumitru Ceara <dce...@redhat.com> > > > > Hi Dumitru, > > Hi Martin, > > > Overall, this looks good to me. I have some nits/suggestions that > > I'll > > leave up to you to consider. Otherwise consider this acked by me. > > > > Thanks for the review! > > > Now that the dynamic routes don't use "struct parsed_route", we can > > probably remove the TODO item that calls for splitting > > parsed_routes > > [0]. > > > > Ah, I forgot about that, thanks for pointing it out! I'll remove it > in v2.
ack. > > > > --- > > > northd/en-advertised-route-sync.c | 376 +++++++++++++++++------- > > > ---- > > > -- > > > northd/northd.h | 3 +- > > > 2 files changed, 210 insertions(+), 169 deletions(-) > > > > > > diff --git a/northd/en-advertised-route-sync.c b/northd/en- > > > advertised-route-sync.c > > > index 486bcc2fd2..ebaed08b69 100644 > > > --- a/northd/en-advertised-route-sync.c > > > +++ b/northd/en-advertised-route-sync.c > > > @@ -27,6 +27,102 @@ > > > #include "openvswitch/hmap.h" > > > #include "ovn-util.h" > > > > > > +struct ar_entry { > > > + struct hmap_node hmap_node; > > > + > > > + const struct ovn_datapath *od; /* Datapath the route > > > is > > > + * advertised on. */ > > > + const struct ovn_port *op; /* Port the route is > > > advertised > > > + * on. */ > > > + char *ip_prefix; > > > + const struct ovn_port *tracked_port; /* If set, the port > > > whose > > > chassis > > > + * advertises this > > > route > > > with a > > > + * higher priority. */ > > > + enum route_source source; > > > +}; > > > + > > > +/* Add a new entries to the to-be-advertised routes. > > > + * Takes ownership of ip_prefix. */ > > > +static struct ar_entry * > > > +ar_entry_add_nocopy(struct hmap *routes, const struct > > > ovn_datapath > > > *od, > > > + const struct ovn_port *op, char *ip_prefix, > > > + const struct ovn_port *tracked_port, > > > + enum route_source source) > > > +{ > > > > With the "copy version" of this function being more popular, and > > the > > "no_copy" used only once. Would it make sense to keep only the > > "ar_entry_add" function that always copies the ip_prefix, and in > > the > > one case where we want "no_copy" just freeing the ip_prefix after > > calling regular "ar_entry_add"? > > > > We could but it seems straight forward enough to avoid that copy. > I'll > keep it as is if you don't mind. +1 > > > > > > + struct ar_entry *route_e = xzalloc(sizeof *route_e); > > > + > > > + route_e->od = od; > > > + route_e->op = op; > > > + route_e->ip_prefix = ip_prefix; > > > + route_e->tracked_port = tracked_port; > > > + route_e->source = source; > > > + uint32_t hash = uuid_hash(&od->sb->header_.uuid); > > > + hash = hash_string(op->sb->logical_port, hash); > > > + hash = hash_string(ip_prefix, hash); > > > + hmap_insert(routes, &route_e->hmap_node, hash); > > > + > > > + return route_e; > > > +} > > > + > > > +/* Add a new entries to the to-be-advertised routes. > > > + * Makes a copy of ip_prefix. */ > > > +static struct ar_entry * > > > +ar_entry_add(struct hmap *routes, const struct ovn_datapath *od, > > > + const struct ovn_port *op, const char *ip_prefix, > > > + const struct ovn_port *tracked_port, > > > + enum route_source source) > > > +{ > > > + return ar_entry_add_nocopy(routes, od, op, > > > xstrdup(ip_prefix), > > > + tracked_port, source); > > > +} > > > + > > > +static struct ar_entry * > > > +ar_entry_find(struct hmap *route_map, > > > + const struct sbrec_datapath_binding *sb_db, > > > + const struct sbrec_port_binding *logical_port, > > > + const char *ip_prefix, > > > + const struct sbrec_port_binding *tracked_port) > > > +{ > > > + struct ar_entry *route_e; > > > + uint32_t hash; > > > + > > > + hash = uuid_hash(&sb_db->header_.uuid); > > > + hash = hash_string(logical_port->logical_port, hash); > > > + hash = hash_string(ip_prefix, hash); > > > + > > > + HMAP_FOR_EACH_WITH_HASH (route_e, hmap_node, hash, > > > route_map) { > > > + if (!uuid_equals(&sb_db->header_.uuid, > > > + &route_e->od->sb->header_.uuid)) { > > > + continue; > > > + } > > > + if (!uuid_equals(&logical_port->header_.uuid, > > > + &route_e->op->sb->header_.uuid)) { > > > + continue; > > > + } > > > + if (strcmp(ip_prefix, route_e->ip_prefix)) { > > > + continue; > > > + } > > > + > > > + if (tracked_port) { > > > + if (!route_e->tracked_port || > > > + tracked_port != route_e->tracked_port->sb) { > > > + continue; > > > + } > > > + } > > > + > > > + return route_e; > > > + } > > > + > > > + return NULL; > > > +} > > > + > > > +static void > > > +ar_entry_free(struct ar_entry *route_e) > > > +{ > > > + free(route_e->ip_prefix); > > > + free(route_e); > > > +} > > > + > > > static void > > > advertised_route_table_sync( > > > struct ovsdb_idl_txn *ovnsb_txn, > > > @@ -158,7 +254,7 @@ en_advertised_route_sync_run(struct > > > engine_node > > > *node, void *data OVS_UNUSED) > > > sbrec_advertised_route_table, > > > &lr_stateful_data->table, > > > &routes_data->parsed_routes, > > > - &dynamic_routes_data- > > > >parsed_routes, > > > + &dynamic_routes_data->routes, > > > routes_sync_data); > > > > > > stopwatch_stop(ADVERTISED_ROUTE_SYNC_RUN_STOPWATCH_NAME, > > > time_msec()); > > > @@ -184,18 +280,18 @@ build_nat_parsed_route_for_port(const > > > struct > > > ovn_port *advertising_op, > > > continue; > > > } > > > > > > - int plen = nat_entry_is_v6(nat) ? 128 : 32; > > > - struct in6_addr prefix; > > > - ip46_parse(nat->nb->external_ip, &prefix); > > > - > > > const struct ovn_port *tracked_port = > > > nat->is_distributed > > > ? ovn_port_find(ls_ports, nat->nb->logical_port) > > > : nat->l3dgw_port; > > > - parsed_route_add(advertising_od, NULL, &prefix, plen, > > > false, > > > - nat->nb->external_ip, advertising_op, > > > 0, > > > false, > > > - false, NULL, ROUTE_SOURCE_NAT, &nat- > > > >nb- > > > > header_, > > > - tracked_port, routes); > > > + > > > + if (!ar_entry_find(routes, advertising_od->sb, > > > advertising_op->sb, > > > + nat->nb->external_ip, > > > + tracked_port ? tracked_port->sb : > > > NULL)) > > > { > > > > Is the if clause necessary here? With "routes" being hashmap of > > ar_routes that uses "advertising_od", "advertising_op" and > > "external_ip" as a key, isn't that enough to prevent duplicates? > > > > But ar_entry_add() doesn't check if the entry already exists, it will > insert a duplicate if we have multiple NAT entries with the same > external_ip. And AFAICT external_ip is not a index column in the > NB.NAT > schema so duplicates can happen. > > So I think we need to keep the lookup. I see, I misunderstood hmap then. I thought that the hash would be unique per map. > > Also, I just realized all these functions are still called > build_nat/lb_parsed_route_for_port(). I'll change that in v2 too. > > > > + ar_entry_add(routes, advertising_od, advertising_op, > > > + nat->nb->external_ip, tracked_port, > > > + ROUTE_SOURCE_NAT); > > > + } > > > } > > > } > > > > > > @@ -294,20 +390,12 @@ build_lb_parsed_route_for_port(const struct > > > ovn_port *advertising_op, > > > > > > const char *ip_address; > > > SSET_FOR_EACH (ip_address, &lb_ips->ips_v4) { > > > - struct in6_addr prefix; > > > - ip46_parse(ip_address, &prefix); > > > - parsed_route_add(advertising_od, NULL, &prefix, 32, > > > false, > > > - ip_address, advertising_op, 0, false, > > > false, > > > - NULL, ROUTE_SOURCE_LB, &advertising_op- > > > > nbrp->header_, > > > - tracked_port, routes); > > > + ar_entry_add(routes, advertising_od, advertising_op, > > > + ip_address, tracked_port, ROUTE_SOURCE_LB); > > > } > > > SSET_FOR_EACH (ip_address, &lb_ips->ips_v6) { > > > - struct in6_addr prefix; > > > - ip46_parse(ip_address, &prefix); > > > - parsed_route_add(advertising_od, NULL, &prefix, 128, > > > false, > > > - ip_address, advertising_op, 0, false, > > > false, > > > - NULL, ROUTE_SOURCE_LB, &advertising_op- > > > > nbrp->header_, > > > - tracked_port, routes); > > > + ar_entry_add(routes, advertising_od, advertising_op, > > > + ip_address, tracked_port, ROUTE_SOURCE_LB); > > > } > > > } > > > > > > @@ -402,7 +490,7 @@ en_dynamic_routes_init(struct engine_node > > > *node > > > OVS_UNUSED, > > > { > > > struct dynamic_routes_data *data = xmalloc(sizeof *data); > > > *data = (struct dynamic_routes_data) { > > > - .parsed_routes = HMAP_INITIALIZER(&data->parsed_routes), > > > + .routes = HMAP_INITIALIZER(&data->routes), > > > }; > > > > > > return data; > > > @@ -411,9 +499,9 @@ en_dynamic_routes_init(struct engine_node > > > *node > > > OVS_UNUSED, > > > static void > > > en_dynamic_routes_clear(struct dynamic_routes_data *data) > > > { > > > - struct parsed_route *r; > > > - HMAP_FOR_EACH_POP (r, key_node, &data->parsed_routes) { > > > - parsed_route_free(r); > > > + struct ar_entry *ar; > > > + HMAP_FOR_EACH_POP (ar, hmap_node, &data->routes) { > > > + ar_entry_free(ar); > > > } > > > } > > > void > > > @@ -422,7 +510,7 @@ en_dynamic_routes_cleanup(void *data_) > > > struct dynamic_routes_data *data = data_; > > > > > > en_dynamic_routes_clear(data); > > > - hmap_destroy(&data->parsed_routes); > > > + hmap_destroy(&data->routes); > > > } > > > > > > void > > > @@ -447,100 +535,20 @@ en_dynamic_routes_run(struct engine_node > > > *node, void *data) > > > } > > > build_nat_parsed_routes(od, lr_stateful_rec->lrnat_rec, > > > &northd_data->ls_ports, > > > - &dynamic_routes_data- > > > > parsed_routes); > > > + &dynamic_routes_data->routes); > > > build_nat_connected_parsed_routes(od, &lr_stateful_data- > > > > table, > > > &northd_data- > > > >ls_ports, > > > - &dynamic_routes_data- > > > > parsed_routes); > > > + &dynamic_routes_data- > > > > routes); > > > > > > build_lb_parsed_routes(od, lr_stateful_rec->lb_ips, > > > - &dynamic_routes_data- > > > >parsed_routes); > > > + &dynamic_routes_data->routes); > > > build_lb_connected_parsed_routes(od, &lr_stateful_data- > > > > table, > > > - &dynamic_routes_data- > > > > parsed_routes); > > > + &dynamic_routes_data- > > > > routes); > > > } > > > stopwatch_stop(DYNAMIC_ROUTES_RUN_STOPWATCH_NAME, > > > time_msec()); > > > engine_set_node_state(node, EN_UPDATED); > > > } > > > > > > -struct ar_entry { > > > - struct hmap_node hmap_node; > > > - > > > - const struct ovn_datapath *od; /* Datapath the route > > > is > > > - * advertised on. */ > > > - const struct ovn_port *op; /* Port the route is > > > advertised > > > - * on. */ > > > - char *ip_prefix; > > > - const struct ovn_port *tracked_port; /* If set, the port > > > whose > > > chassis > > > - * advertises this > > > route > > > with a > > > - * higher priority. */ > > > -}; > > > - > > > -/* Add a new entries to the to-be-advertised routes. > > > - * Takes ownership of ip_prefix. */ > > > -static struct ar_entry * > > > -ar_entry_add(struct hmap *routes, const struct ovn_datapath *od, > > > - const struct ovn_port *op, char *ip_prefix, > > > - const struct ovn_port *tracked_port) > > > -{ > > > - struct ar_entry *route_e = xzalloc(sizeof *route_e); > > > - > > > - route_e->od = od; > > > - route_e->op = op; > > > - route_e->ip_prefix = ip_prefix; > > > - route_e->tracked_port = tracked_port; > > > - uint32_t hash = uuid_hash(&od->sb->header_.uuid); > > > - hash = hash_string(op->sb->logical_port, hash); > > > - hash = hash_string(ip_prefix, hash); > > > - hmap_insert(routes, &route_e->hmap_node, hash); > > > - > > > - return route_e; > > > -} > > > - > > > -static struct ar_entry * > > > -ar_entry_find(struct hmap *route_map, > > > - const struct sbrec_datapath_binding *sb_db, > > > - const struct sbrec_port_binding *logical_port, > > > - const char *ip_prefix, > > > - const struct sbrec_port_binding *tracked_port) > > > -{ > > > - struct ar_entry *route_e; > > > - uint32_t hash; > > > - > > > - hash = uuid_hash(&sb_db->header_.uuid); > > > - hash = hash_string(logical_port->logical_port, hash); > > > - hash = hash_string(ip_prefix, hash); > > > - > > > - HMAP_FOR_EACH_WITH_HASH (route_e, hmap_node, hash, > > > route_map) { > > > - if (!uuid_equals(&sb_db->header_.uuid, > > > - &route_e->od->sb->header_.uuid)) { > > > - continue; > > > - } > > > - if (!uuid_equals(&logical_port->header_.uuid, > > > - &route_e->op->sb->header_.uuid)) { > > > - continue; > > > - } > > > - if (strcmp(ip_prefix, route_e->ip_prefix)) { > > > - continue; > > > - } > > > - > > > - if (tracked_port) { > > > - if (!route_e->tracked_port || > > > - tracked_port != route_e->tracked_port->sb) { > > > - continue; > > > - } > > > - } > > > - return route_e; > > > - } > > > - > > > - return NULL; > > > -} > > > - > > > -static void > > > -ar_entry_free(struct ar_entry *route_e) > > > -{ > > > - free(route_e->ip_prefix); > > > - free(route_e); > > > -} > > > - > > > static void > > > publish_lport_addresses(struct hmap *sync_routes, > > > const struct ovn_datapath *od, > > > @@ -550,16 +558,16 @@ publish_lport_addresses(struct hmap > > > *sync_routes, > > > { > > > for (size_t i = 0; i < addresses->n_ipv4_addrs; i++) { > > > const struct ipv4_netaddr *addr = &addresses- > > > >ipv4_addrs[i]; > > > - ar_entry_add(sync_routes, od, logical_port, > > > xstrdup(addr- > > > > addr_s), > > > - tracking_port); > > > + ar_entry_add(sync_routes, od, logical_port, addr- > > > >addr_s, > > > + tracking_port, ROUTE_SOURCE_CONNECTED); > > > } > > > for (size_t i = 0; i < addresses->n_ipv6_addrs; i++) { > > > if (in6_is_lla(&addresses->ipv6_addrs[i].network)) { > > > continue; > > > } > > > const struct ipv6_netaddr *addr = &addresses- > > > >ipv6_addrs[i]; > > > - ar_entry_add(sync_routes, od, logical_port, > > > xstrdup(addr- > > > > addr_s), > > > - tracking_port); > > > + ar_entry_add(sync_routes, od, logical_port, addr- > > > >addr_s, > > > + tracking_port, ROUTE_SOURCE_CONNECTED); > > > } > > > } > > > > > > @@ -640,78 +648,85 @@ publish_host_routes(struct hmap > > > *sync_routes, > > > } > > > } > > > > > > -static void > > > -advertised_route_table_sync_route_add( > > > - const struct lr_stateful_table *lr_stateful_table, > > > - struct advertised_route_sync_data *data, > > > - struct uuidset *host_route_lrps, > > > - struct hmap *sync_routes, > > > - const struct parsed_route *route) > > > +static bool > > > +should_advertise_route(const struct lr_stateful_table > > > *lr_stateful_table, > > > + struct advertised_route_sync_data *data, > > > + struct uuidset *host_route_lrps, > > > + struct hmap *sync_routes, > > > + const struct ovn_datapath > > > *advertising_od, > > > + const struct ovn_port *advertising_op, > > > + const struct ovn_port *tracked_op, > > > + enum route_source source) > > > { > > > - if (route->is_discard_route) { > > > - return; > > > - } > > > - if (prefix_is_link_local(&route->prefix, route->plen)) { > > > - return; > > > - } > > > - if (!route->od->dynamic_routing) { > > > - return; > > > + if (!advertising_od->dynamic_routing) { > > > + return false; > > > } > > > > > > enum dynamic_routing_redistribute_mode drr = > > > - route->out_port->dynamic_routing_redistribute; > > > - if (route->source == ROUTE_SOURCE_CONNECTED) { > > > + advertising_op->dynamic_routing_redistribute; > > > + > > > + switch (source) { > > > + case ROUTE_SOURCE_CONNECTED: > > > if (!drr_mode_CONNECTED_is_set(drr)) { > > > - return; > > > + return false; > > > } > > > + > > > /* If we advertise host routes, we only need to do so > > > once > > > per > > > * LRP. */ > > > - const struct uuid *lrp_uuid = &route->out_port->nbrp- > > > > header_.uuid; > > > - if (drr_mode_CONNECTED_AS_HOST_is_set(drr) && > > > - !uuidset_contains(host_route_lrps, lrp_uuid)) { > > > + const struct uuid *lrp_uuid = &advertising_op->nbrp- > > > > header_.uuid; > > > + if (drr_mode_CONNECTED_AS_HOST_is_set(drr)) { > > > + if (uuidset_contains(host_route_lrps, lrp_uuid)) { > > > + return false; > > > + } > > > uuidset_insert(host_route_lrps, lrp_uuid); > > > publish_host_routes(sync_routes, lr_stateful_table, > > > - route->od, route->out_port, > > > + advertising_od, advertising_op, > > > data); > > > - return; > > > + return false; > > > } > > > - } > > > - if (route->source == ROUTE_SOURCE_STATIC && > > > !drr_mode_STATIC_is_set(drr)) { > > > - return; > > > - } > > > - if (route->source == ROUTE_SOURCE_NAT) { > > > + return true; > > > + > > > + case ROUTE_SOURCE_STATIC: > > > + return drr_mode_STATIC_is_set(drr); > > > + > > > + case ROUTE_SOURCE_NAT: > > > if (!drr_mode_NAT_is_set(drr)) { > > > - return; > > > + return false; > > > } > > > + > > > /* If NAT route tracks port on a different DP than the > > > one > > > that > > > * advertises the route, we need to watch for changes on > > > that DP as > > > * well. */ > > > - if (route->tracked_port && route->tracked_port->od != > > > route- > > > > od) { > > > - if (route->tracked_port->od->nbr) { > > > + if (tracked_op && tracked_op->od != advertising_od) { > > > + if (tracked_op->od->nbr) { > > > uuidset_insert(&data->nb_lr, > > > - &route->tracked_port->od->nbr- > > > > header_.uuid); > > > - } else if (route->tracked_port->od->nbs) { > > > + &tracked_op->od->nbr- > > > >header_.uuid); > > > + } else if (tracked_op->od->nbs) { > > > uuidset_insert(&data->nb_ls, > > > - &route->tracked_port->od->nbs- > > > > header_.uuid); > > > + &tracked_op->od->nbs- > > > >header_.uuid); > > > } > > > } > > > - } > > > - if (route->source == ROUTE_SOURCE_LB) { > > > + return true; > > > + > > > + case ROUTE_SOURCE_LB: > > > if (!drr_mode_LB_is_set(drr)) { > > > - return; > > > + return false; > > > } > > > + > > > /* If LB route tracks port on a different DP than the > > > one > > > that > > > * advertises the route, we need to watch for changes on > > > that DP as > > > * well. */ > > > - if (route->tracked_port && route->tracked_port->od != > > > route- > > > > od) { > > > + if (tracked_op && tracked_op->od != advertising_od) { > > > uuidset_insert(&data->nb_lr, > > > - &route->tracked_port->od->nbr- > > > > header_.uuid); > > > + &tracked_op->od->nbr->header_.uuid); > > > } > > > + return true; > > > + > > > + case ROUTE_SOURCE_LEARNED: > > > + return true; > > > } > > > > > > - char *ip_prefix = normalize_v46_prefix(&route->prefix, > > > route- > > > > plen); > > > - ar_entry_add(sync_routes, route->od, route->out_port, > > > ip_prefix, > > > - route->tracked_port); > > > + OVS_NOT_REACHED(); > > > } > > > > > > static void > > > @@ -725,23 +740,48 @@ advertised_route_table_sync( > > > { > > > struct hmap sync_routes = HMAP_INITIALIZER(&sync_routes); > > > struct uuidset host_route_lrps = > > > UUIDSET_INITIALIZER(&host_route_lrps); > > > - const struct parsed_route *route; > > > - > > > - struct ar_entry *route_e; > > > > > > /* First build the set of non-dynamic routes that need sync- > > > ing. > > > */ > > > + const struct parsed_route *route; > > > HMAP_FOR_EACH (route, key_node, routes) { > > > - advertised_route_table_sync_route_add(lr_stateful_table, > > > - data, > > > &host_route_lrps, > > > - &sync_routes, > > > - route); > > > + if (route->is_discard_route) { > > > + continue; > > > + } > > > + > > > + if (prefix_is_link_local(&route->prefix, route->plen)) { > > > + continue; > > > + } > > > + > > > + if (!should_advertise_route(lr_stateful_table, > > > + data, &host_route_lrps, > > > + &sync_routes, > > > + route->od, route->out_port, > > > + route->tracked_port, > > > + route->source)) { > > > + continue; > > > + } > > > + > > > + ar_entry_add_nocopy(&sync_routes, route->od, route- > > > > out_port, > > > + normalize_v46_prefix(&route->prefix, > > > route->plen), > > > + route->tracked_port, > > > + route->source); > > > } > > > + > > > /* Then add the set of dynamic routes that need sync-ing. */ > > > - HMAP_FOR_EACH (route, key_node, dynamic_routes) { > > > - advertised_route_table_sync_route_add(lr_stateful_table, > > > - data, > > > &host_route_lrps, > > > - &sync_routes, > > > - route); > > > + struct ar_entry *route_e; > > > + HMAP_FOR_EACH (route_e, hmap_node, dynamic_routes) { > > > + if (!should_advertise_route(lr_stateful_table, > > > + data, &host_route_lrps, > > > + &sync_routes, > > > + route_e->od, route_e->op, > > > + route_e->tracked_port, > > > + route_e->source)) { > > > + continue; > > > + } > > > + > > > + ar_entry_add(&sync_routes, route_e->od, route_e->op, > > > + route_e->ip_prefix, route_e->tracked_port, > > > + route_e->source); > > > > With "dynamic_routes" and "sync_routes" both being hmaps of > > ar_entry > > structs, would it make sense to pop the "ar_entry" item from > > dynamic_routes and directly insert it into the "sync_routes"? > > Instead > > of sending individual route_e values through the ar_entry_add, to > > basically create a copy of "route_e". > > > > Unfortunately that would break the constraints the I-P engine > enforces > on us. A node should not change any data of other nodes that are its > inputs. In this case, imagine we ever need to add another I-P engine > node that depends on "en_dynamic_routes". If "en_advertise_routes" > changes the dynamic_routes map then the behavior of the incremental > processing graph is undefined, because the input data for the > "en_advertise_routes" and the new node looks different while it's > actually coming from the same input node. Makes sense, thanks for the explanation. Martin. > > > Thanks a lot, > > Martin. > > Regards, > Dumitru > > > > > [0] > > https://github.com/ovn-org/ovn/blob/fe26d5b53f5c1f939c8e03e0677e526 > > 8a1f7432d/TODO.rst?plain=1#L163-L168 > > > _______________________________________________ dev mailing list d...@openvswitch.org https://mail.openvswitch.org/mailman/listinfo/ovs-dev