Skip to content

Commit 92c7ab5

Browse files
ibot3dceara
authored andcommitted
controller: Allow multiple LRs to advertise routes to the same VRF.
When several Logical_Routers were configured with the same dynamic-routing-vrf-id, only one of them (or none) actually had its routes synced to the shared host routing table (VRF). Route exchange reconciles each VRF routing table as a single authoritative set: re_nl_sync_routes() dumps the table and removes every OVN owned route that is not part of the supplied set. Only one datapath's routes were kept per table, so when a second Logical_Router used the same dynamic-routing-vrf-id the collision was detected, logged and synchronisation of that table was skipped entirely. Pass the routes of all datapaths sharing a table to re_nl_sync_routes() and reconcile their union. Equivalent routes advertised by several datapaths are installed exactly once: while building the union only the occurrence from the first table is kept, which also lets handle_route_msg() stop at the first match. Add a system test with two Logical_Routers exporting their connected routes and a shared static prefix to the same VRF, including removal of both routers. CC: Jacob Tanenbaum <jtanenba@redhat.com> Assisted-by: Claude Opus 4.8, Claude Code Signed-off-by: Jakob Mueller <me@jakobm.de> Signed-off-by: Dumitru Ceara <dceara@redhat.com>
1 parent be7fb53 commit 92c7ab5

6 files changed

Lines changed: 185 additions & 67 deletions

File tree

NEWS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ OVN v26.09.0 - xxx xx xxxx
5151
"lb-add", "meter-add", "lr-policy-add", and "lr-policy-del", and
5252
fixed the "nfg-list" signature.
5353
- Dynamic Routing:
54-
* Allow multiple routers to read the same VRF table.
54+
* Allow multiple routers to read from and advertise their routes to
55+
the same VRF table.
5556
* Add support for hub-and-spoke propagation via the "hub-spoke" option
5657
in dynamic-routing-redistribute settings.
5758
* Add ECMP/multi-homing support for EVPN FDB entries. FDB entries

controller/route-exchange-netlink.c

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ struct route_msg_handle_data {
203203
struct hmapx *routes_to_advertise;
204204
struct vector *learned_routes;
205205
struct vector *stale_routes;
206-
const struct hmap *routes;
206+
/* Vector of "const struct hmap *", each holding advertise_route_entry
207+
* nodes for a datapath sharing this routing table. */
208+
const struct vector *route_tables;
207209
};
208210

209211
static void
@@ -260,11 +262,14 @@ handle_route_msg(const struct route_table_msg *msg,
260262
const struct advertise_route_entry re =
261263
advertise_route_from_route_data(rd);
262264
if (handle_data->routes_to_advertise) {
263-
ar = advertise_route_find(re.priority, &re.addr, re.plen,
264-
&re.nexthop, handle_data->routes);
265-
if (ar) {
266-
hmapx_find_and_delete(handle_data->routes_to_advertise, ar);
267-
return;
265+
const struct hmap *routes;
266+
VECTOR_FOR_EACH (handle_data->route_tables, routes) {
267+
ar = advertise_route_find(re.priority, &re.addr, re.plen,
268+
&re.nexthop, routes);
269+
if (ar) {
270+
hmapx_find_and_delete(handle_data->routes_to_advertise, ar);
271+
return;
272+
}
268273
}
269274
}
270275

@@ -319,23 +324,44 @@ re_nl_encode_nexthop(struct ofpbuf *request, bool dst_is_ipv4,
319324
}
320325

321326
int
322-
re_nl_sync_routes(uint32_t table_id, const struct hmap *routes,
327+
re_nl_sync_routes(uint32_t table_id, const struct vector *route_tables,
323328
struct vector *learned_routes)
324329
{
325330
struct hmapx routes_to_advertise = HMAPX_INITIALIZER(&routes_to_advertise);
326331
struct vector stale_routes =
327332
VECTOR_EMPTY_INITIALIZER(struct advertise_route_entry);
328333
struct advertise_route_entry *ar;
329334

330-
HMAP_FOR_EACH (ar, node, routes) {
331-
hmapx_add(&routes_to_advertise, ar);
335+
/* Equivalent routes may be advertised by multiple datapaths sharing this
336+
* routing table. Only keep the occurrence from the first table that has
337+
* it, so each route is installed in the kernel exactly once and
338+
* handle_route_msg() can stop at the first match as well. */
339+
size_t n_prev = 0;
340+
const struct hmap *routes;
341+
VECTOR_FOR_EACH (route_tables, routes) {
342+
HMAP_FOR_EACH (ar, node, routes) {
343+
bool duplicate = false;
344+
for (size_t i = 0; i < n_prev; i++) {
345+
const struct hmap *prev = vector_get(route_tables, i,
346+
const struct hmap *);
347+
if (advertise_route_find(ar->priority, &ar->addr, ar->plen,
348+
&ar->nexthop, prev)) {
349+
duplicate = true;
350+
break;
351+
}
352+
}
353+
if (!duplicate) {
354+
hmapx_add(&routes_to_advertise, ar);
355+
}
356+
}
357+
n_prev++;
332358
}
333359

334-
/* Remove routes from the system that are not in the routes hmap and
335-
* remove entries from routes hmap that match routes already installed
336-
* in the system. */
360+
/* Remove routes from the system that are not in any of the route tables
361+
* and remove entries from routes_to_advertise that match routes already
362+
* installed in the system. */
337363
struct route_msg_handle_data data = {
338-
.routes = routes,
364+
.route_tables = route_tables,
339365
.routes_to_advertise = &routes_to_advertise,
340366
.learned_routes = learned_routes,
341367
.stale_routes = &stale_routes,

controller/route-exchange-netlink.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ void re_route_format(struct ds *, uint32_t table_id,
5757
const struct in6_addr *dst, unsigned int plen,
5858
const struct in6_addr *nexthop, int err);
5959

60-
int re_nl_sync_routes(uint32_t table_id, const struct hmap *routes,
60+
int re_nl_sync_routes(uint32_t table_id, const struct vector *route_tables,
6161
struct vector *learned_routes);
6262

6363
int re_nl_cleanup_routes(uint32_t table_id);

controller/route-exchange.c

Lines changed: 37 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -302,10 +302,11 @@ static int route_exchange_nl_status;
302302
struct advertised_routes_entry {
303303
struct hmap_node node;
304304

305+
/* Contains "struct advertise_datapath_entry *" for all datapaths that
306+
* advertise routes on this routing table. Multiple datapaths may share a
307+
* single table when they use the same dynamic-routing-vrf-id. */
305308
struct hmapx datapaths;
306-
const struct hmap *routes;
307309
uint32_t table_id;
308-
bool can_sync;
309310
};
310311

311312
void
@@ -356,17 +357,6 @@ route_exchange_run(const struct route_exchange_ctx_in *r_ctx_in,
356357
uint32_t hash = maintained_route_table_hash(table_id);
357358
HMAP_FOR_EACH_WITH_HASH (entry, node, hash, &advertised_routes) {
358359
if (entry->table_id == table_id) {
359-
if (!hmap_is_empty(&ad->routes)) {
360-
if (entry->routes && !hmap_is_empty(entry->routes)) {
361-
VLOG_WARN_RL(&rl,
362-
"Multiple datapaths are distributing "
363-
"routes on routing table %"PRIu32,
364-
table_id);
365-
entry->can_sync = false;
366-
} else {
367-
entry->routes = &ad->routes;
368-
}
369-
}
370360
break;
371361
}
372362
}
@@ -375,55 +365,51 @@ route_exchange_run(const struct route_exchange_ctx_in *r_ctx_in,
375365
entry = xmalloc(sizeof *entry);
376366
*entry = (struct advertised_routes_entry) {
377367
.datapaths = HMAPX_INITIALIZER(&entry->datapaths),
378-
.routes = &ad->routes,
379368
.table_id = table_id,
380-
.can_sync = true,
381369
};
382370
hmap_insert(&advertised_routes, &entry->node, hash);
383371
}
384372

385-
if (!entry->can_sync) {
386-
continue;
387-
}
388-
389-
hmapx_add(&entry->datapaths, CONST_CAST(void *, ad->db));
373+
hmapx_add(&entry->datapaths, CONST_CAST(void *, ad));
390374
}
391375

392376
struct advertised_routes_entry *arte;
393377
HMAP_FOR_EACH_POP (arte, node, &advertised_routes) {
394378
maintained_route_table_add(arte->table_id);
395-
if (arte->can_sync) {
396-
struct vector received_routes =
397-
VECTOR_EMPTY_INITIALIZER(struct re_nl_received_route_node);
398-
error = re_nl_sync_routes(arte->table_id, arte->routes,
399-
&received_routes);
400-
SET_ROUTE_EXCHANGE_NL_STATUS(error);
401-
402-
struct ovsdb_idl_index *sbrec_learned_route_by_datapath =
403-
r_ctx_in->sbrec_learned_route_by_datapath;
404-
struct hmapx_node *dp_node;
405-
HMAPX_FOR_EACH (dp_node, &arte->datapaths) {
406-
const struct sbrec_datapath_binding *db = dp_node->data;
407-
struct advertise_datapath_entry *adpe =
408-
advertise_datapath_find(r_ctx_in->announce_routes,
409-
db);
410-
if (!adpe) {
411-
VLOG_WARN_RL(&rl, "Cannot sync datapath binding "
412-
UUID_FMT", bound ports not found",
413-
UUID_ARGS(&db->header_.uuid));
414-
continue;
415-
}
416-
sb_sync_learned_routes(&received_routes, db,
417-
&adpe->bound_ports,
418-
r_ctx_in->ovnsb_idl_txn,
419-
r_ctx_in->sbrec_port_binding_by_name,
420-
sbrec_learned_route_by_datapath,
421-
&r_ctx_out->sb_changes_pending,
422-
r_ctx_in->chassis);
423-
}
424-
vector_push(r_ctx_out->route_table_watches, &arte->table_id);
425-
vector_destroy(&received_routes);
379+
380+
struct hmapx_node *dp_node;
381+
382+
/* Collect the route tables of all datapaths sharing this routing
383+
* table so they are synced together as a single authoritative set. */
384+
struct vector route_tables =
385+
VECTOR_EMPTY_INITIALIZER(const struct hmap *);
386+
HMAPX_FOR_EACH (dp_node, &arte->datapaths) {
387+
const struct advertise_datapath_entry *adpe = dp_node->data;
388+
const struct hmap *routes = &adpe->routes;
389+
vector_push(&route_tables, &routes);
390+
}
391+
392+
struct vector received_routes =
393+
VECTOR_EMPTY_INITIALIZER(struct re_nl_received_route_node);
394+
error = re_nl_sync_routes(arte->table_id, &route_tables,
395+
&received_routes);
396+
SET_ROUTE_EXCHANGE_NL_STATUS(error);
397+
vector_destroy(&route_tables);
398+
399+
struct ovsdb_idl_index *sbrec_learned_route_by_datapath =
400+
r_ctx_in->sbrec_learned_route_by_datapath;
401+
HMAPX_FOR_EACH (dp_node, &arte->datapaths) {
402+
const struct advertise_datapath_entry *adpe = dp_node->data;
403+
sb_sync_learned_routes(&received_routes, adpe->db,
404+
&adpe->bound_ports,
405+
r_ctx_in->ovnsb_idl_txn,
406+
r_ctx_in->sbrec_port_binding_by_name,
407+
sbrec_learned_route_by_datapath,
408+
&r_ctx_out->sb_changes_pending,
409+
r_ctx_in->chassis);
426410
}
411+
vector_push(r_ctx_out->route_table_watches, &arte->table_id);
412+
vector_destroy(&received_routes);
427413

428414
hmapx_destroy(&arte->datapaths);
429415
free(arte);

tests/system-ovn.at

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17340,6 +17340,105 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
1734017340
AT_CLEANUP
1734117341
])
1734217342

17343+
OVN_FOR_EACH_NORTHD([
17344+
AT_SETUP([dynamic-routing - multiple LRs sharing a VRF])
17345+
AT_KEYWORDS([dynamic-routing])
17346+
17347+
VRF_RESERVE([1337])
17348+
17349+
ovn_start
17350+
OVS_TRAFFIC_VSWITCHD_START()
17351+
17352+
ADD_BR([br-int])
17353+
check ovs-vsctl \
17354+
-- set Open_vSwitch . external-ids:system-id=hv1 \
17355+
-- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
17356+
-- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
17357+
-- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
17358+
-- set bridge br-int fail-mode=secure other-config:disable-in-band=true
17359+
17360+
start_daemon ovn-controller
17361+
17362+
dnl Two independent logical routers configured with the same
17363+
dnl dynamic-routing-vrf-id. Each redistributes a distinct connected subnet
17364+
dnl plus a static route for the same shared prefix. Only lr1 maintains the
17365+
dnl VRF device, but both must export their routes to it.
17366+
check ovn-nbctl \
17367+
-- lr-add lr1 \
17368+
-- set logical_router lr1 options:dynamic-routing=true \
17369+
options:dynamic-routing-vrf-id=1337 \
17370+
-- lrp-add lr1 lr1-gw 00:00:00:01:00:10 42.10.10.12/24 \
17371+
-- lrp-set-gateway-chassis lr1-gw hv1 10 \
17372+
-- lrp-set-options lr1-gw dynamic-routing-maintain-vrf=true \
17373+
-- lrp-add lr1 lr1-int 00:00:00:00:01:02 30.0.1.1/24 \
17374+
-- lrp-set-options lr1-int \
17375+
dynamic-routing-redistribute=connected,static \
17376+
-- lr-add lr2 \
17377+
-- set logical_router lr2 options:dynamic-routing=true \
17378+
options:dynamic-routing-vrf-id=1337 \
17379+
-- lrp-add lr2 lr2-gw 00:00:00:02:00:10 42.20.10.22/24 \
17380+
-- lrp-set-gateway-chassis lr2-gw hv1 10 \
17381+
-- lrp-add lr2 lr2-int 00:00:00:00:02:02 30.0.2.1/24 \
17382+
-- lrp-set-options lr2-int \
17383+
dynamic-routing-redistribute=connected,static \
17384+
-- ls-add ls1 \
17385+
-- lsp-add-router-port ls1 ls1-lr1-gw lr1-gw \
17386+
-- ls-add ls2 \
17387+
-- lsp-add-router-port ls2 ls2-lr2-gw lr2-gw \
17388+
-- ls-add ls-int1 \
17389+
-- lsp-add-router-port ls-int1 ls-int1-lr lr1-int \
17390+
-- lsp-add ls-int1 w1 \
17391+
-- lsp-set-addresses w1 "00:00:00:00:00:01 30.0.1.11" \
17392+
-- ls-add ls-int2 \
17393+
-- lsp-add-router-port ls-int2 ls-int2-lr lr2-int \
17394+
-- lsp-add ls-int2 w2 \
17395+
-- lsp-set-addresses w2 "00:00:00:00:00:02 30.0.2.11"
17396+
17397+
dnl Both routers advertise a static route for the same prefix.
17398+
check ovn-nbctl lr-route-add lr1 203.0.113.0/24 30.0.1.11
17399+
check ovn-nbctl lr-route-add lr2 203.0.113.0/24 30.0.2.11
17400+
17401+
check ovs-vsctl add-port br-int w1 \
17402+
-- set interface w1 type=internal external_ids:iface-id=w1
17403+
check ovs-vsctl add-port br-int w2 \
17404+
-- set interface w2 type=internal external_ids:iface-id=w2
17405+
check ovn-nbctl --wait=hv sync
17406+
wait_for_ports_up w1 w2
17407+
17408+
AT_CHECK([ip vrf show ovnvrf1337], [0], [dnl
17409+
ovnvrf1337 1337
17410+
])
17411+
17412+
dnl Both routers must have their connected routes installed in the shared VRF.
17413+
dnl The shared static prefix is advertised by both routers but must be
17414+
dnl installed only once.
17415+
OVN_ROUTE_EQUAL([ovnvrf1337], [dnl
17416+
blackhole 30.0.1.0/24 proto ovn metric 1000
17417+
blackhole 30.0.2.0/24 proto ovn metric 1000
17418+
blackhole 203.0.113.0/24 proto ovn metric 1000])
17419+
17420+
dnl Removing one router must not disturb the remaining router's routes,
17421+
dnl including the shared prefix it still advertises.
17422+
check ovn-nbctl --wait=hv lr-del lr2
17423+
OVN_ROUTE_EQUAL([ovnvrf1337], [dnl
17424+
blackhole 30.0.1.0/24 proto ovn metric 1000
17425+
blackhole 203.0.113.0/24 proto ovn metric 1000])
17426+
17427+
dnl Removing the last router must clean up the routes and the VRF device.
17428+
check ovn-nbctl --wait=hv lr-del lr1
17429+
OVS_WAIT_WHILE([ip link show dev ovnvrf1337])
17430+
17431+
OVN_CLEANUP_CONTROLLER([hv1])
17432+
17433+
OVN_CLEANUP_NORTHD
17434+
17435+
as
17436+
OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
17437+
/connection dropped.*/d"])
17438+
17439+
AT_CLEANUP
17440+
])
17441+
1734317442
OVN_FOR_EACH_NORTHD([
1734417443
AT_SETUP([dynamic-routing - multiple DGP with same priority])
1734517444

tests/test-ovn-netlink.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,14 @@ test_route_sync(struct ovs_cmdl_context *ctx)
231231
advertise_route_hash(&ar->addr, &ar->nexthop, ar->plen));
232232
}
233233

234-
ovs_assert(re_nl_sync_routes(table_id, &routes_to_advertise,
234+
struct vector route_tables =
235+
VECTOR_EMPTY_INITIALIZER(const struct hmap *);
236+
const struct hmap *routes = &routes_to_advertise;
237+
vector_push(&route_tables, &routes);
238+
239+
ovs_assert(re_nl_sync_routes(table_id, &route_tables,
235240
&received_routes) == 0);
241+
vector_destroy(&route_tables);
236242

237243
struct ds msg = DS_EMPTY_INITIALIZER;
238244

0 commit comments

Comments
 (0)