Skip to content

Commit 7a95f87

Browse files
committed
northd: Mark BFD sessions down when no chassis can run them.
The Southbound BFD "status" column is only ever written by the ovn-controller that runs the session: bfd_monitor_run() picks a session up on the chassis where the port is a bound "l3gateway" port or where the port's "cr-" chassisredirect twin is chassis-resident. When that chassis goes away and no other chassis can take the gateway port over (e.g. it was the only chassis with a bridge mapping for the provider network), nothing is left to update the session status: the NB/SB status stays "up" forever even though no BFD packets are exchanged any more. BFD monitored (ECMP) static routes then keep selecting the dead next hop and traffic is blackholed. Fix this in ovn-northd, which can see that no chassis owns the port: teach bfd_table_sync() to determine the chassis that would run the session, mirroring the ownership rule of bfd_monitor_run(), and force both the NB and SB status to "down" when there is no such chassis, leaving "admin_down" sessions untouched. Route (and policy) processing already drops BFD monitored next hops whose session is "down", so this is enough to withdraw the routes. Once a chassis (re)claims the gateway port, its ovn-controller creates a fresh monitor entry and kicks the session back into negotiation, so a northd-written "down" does not prevent recovery. Use the same ownership rule to maintain the SB BFD "chassis_name" column. It was previously derived from the port's own Port_Binding only, but for a distributed gateway port that binding is a "patch" port whose chassis column is never set, so chassis_name stayed empty even for healthy sessions. It now tracks the chassis bound to the chassisredirect port and is cleared when the session has no owner. Finally, make the incremental processing engine reevaluate the session ownership when the Port_Binding of a port with a BFD session changes its bound chassis: the "northd" engine node deliberately ignores chassis-only binding updates, so a gateway port being released would not have re-triggered bfd_table_sync(). Add SB Port_Binding as an input of the "bfd_sync" engine node, with a change handler that falls back to recompute when the "chassis" column changed on the Port_Binding of a port (or of a chassisredirect twin of a port) that has a BFD session, and ignores all other Port_Binding changes. Reported-at: #320 Assisted-by: Claude Code Signed-off-by: Premysl Kouril <premysl.kouril@gmail.com>
1 parent eee8cb3 commit 7a95f87

9 files changed

Lines changed: 256 additions & 9 deletions

File tree

northd/en-northd.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,30 @@ bfd_sync_routes_change_handler(struct engine_node *node,
566566
return EN_HANDLED_UNCHANGED;
567567
}
568568

569+
enum engine_input_handler_result
570+
bfd_sync_sb_port_binding_change_handler(struct engine_node *node, void *data)
571+
{
572+
struct bfd_sync_data *bfd_sync_data = data;
573+
struct northd_data *northd_data = engine_get_input_data("northd", node);
574+
const struct sbrec_port_binding_table *sbrec_port_binding_table =
575+
EN_OVSDB_GET(engine_get_input("SB_port_binding", node));
576+
577+
/* bfd_table_sync() derives the chassis that runs a BFD session (and
578+
* with it the NB/SB BFD "status" and the SB BFD "chassis_name") from
579+
* the Port_Binding of the session's logical port and of its
580+
* chassisredirect twin. If a chassis binding of such a port changed
581+
* (e.g. the chassis running the session went away and the binding
582+
* was released), fall back to recompute; all other Port_Binding
583+
* changes are irrelevant to BFD. */
584+
if (!bfd_sync_handle_sb_port_binding_changes(sbrec_port_binding_table,
585+
&northd_data->lr_ports,
586+
&bfd_sync_data->bfd_ports)) {
587+
return EN_UNHANDLED;
588+
}
589+
590+
return EN_HANDLED_UNCHANGED;
591+
}
592+
569593
enum engine_node_state
570594
en_bfd_sync_run(struct engine_node *node, void *data)
571595
{

northd/en-northd.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ bfd_sync_northd_change_handler(struct engine_node *node,
5858
enum engine_input_handler_result
5959
bfd_sync_routes_change_handler(struct engine_node *node,
6060
void *data OVS_UNUSED);
61+
enum engine_input_handler_result
62+
bfd_sync_sb_port_binding_change_handler(struct engine_node *node,
63+
void *data);
6164

6265
enum engine_node_state en_bfd_sync_run(struct engine_node *node, void *data);
6366
void en_bfd_sync_cleanup(void *data OVS_UNUSED);

northd/inc-proc-northd.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
345345
engine_add_input(&en_bfd_sync, &en_routes, bfd_sync_routes_change_handler);
346346
engine_add_input(&en_bfd_sync, &en_route_policies, NULL);
347347
engine_add_input(&en_bfd_sync, &en_northd, bfd_sync_northd_change_handler);
348+
engine_add_input(&en_bfd_sync, &en_sb_port_binding,
349+
bfd_sync_sb_port_binding_change_handler);
348350

349351
engine_add_input(&en_ecmp_nexthop, &en_global_config, NULL);
350352
engine_add_input(&en_ecmp_nexthop, &en_routes, NULL);

northd/northd.c

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11955,6 +11955,23 @@ bfd_get_connection_status(const struct nbrec_bfd *nb_bt,
1195511955
return bfd_rp ? bfd_rp->status : bfd_sr->status;
1195611956
}
1195711957

11958+
/* Returns the chassis that runs the BFD session for the logical router
11959+
* port 'op', mirroring the ownership rule implemented by bfd_monitor_run()
11960+
* in controller/pinctrl.c: ovn-controller runs the session if the
11961+
* chassisredirect twin of 'op' is bound to the chassis or if 'op' itself
11962+
* (an "l3gateway" port) is bound to the chassis. Returns NULL if there is
11963+
* no currently bound BFD owner. */
11964+
static const struct sbrec_chassis *
11965+
bfd_get_session_chassis(const struct ovn_port *op)
11966+
{
11967+
if (op->cr_port && op->cr_port->sb) {
11968+
return op->cr_port->sb->chassis;
11969+
}
11970+
11971+
return (op->sb && !strcmp(op->sb->type, "l3gateway"))
11972+
? op->sb->chassis : NULL;
11973+
}
11974+
1195811975
void
1195911976
bfd_table_sync(struct ovsdb_idl_txn *ovnsb_txn,
1196011977
const struct nbrec_bfd_table *nbrec_bfd_table,
@@ -11996,10 +12013,24 @@ bfd_table_sync(struct ovsdb_idl_txn *ovnsb_txn,
1199612013
continue;
1199712014
}
1199812015

12016+
const struct sbrec_chassis *chassis = bfd_get_session_chassis(op);
12017+
1199912018
nbrec_bfd_set_status(nb_bt,
1200012019
bfd_get_connection_status(nb_bt,
1200112020
rp_bfd_connections,
1200212021
sr_bfd_connections));
12022+
12023+
if (!chassis && strcmp(nb_bt->status, "admin_down") &&
12024+
strcmp(nb_bt->status, "down")) {
12025+
/* No chassis currently owns the BFD session for this port, e.g.
12026+
* the chassis that was running it disappeared and no other
12027+
* chassis took the port over. ovn-controller is the only writer of
12028+
* the SB BFD status, so mark the session down from here;
12029+
* otherwise a stale "up" status would keep BFD-monitored
12030+
* static routes pointing at a dead next hop. */
12031+
nbrec_bfd_set_status(nb_bt, "down");
12032+
}
12033+
1200312034
if (!bfd_e->sb_bt) {
1200412035
int udp_src = bfd_get_unused_port(bfd_src_ports);
1200512036
if (udp_src < 0) {
@@ -12013,8 +12044,8 @@ bfd_table_sync(struct ovsdb_idl_txn *ovnsb_txn,
1201312044
sbrec_bfd_set_disc(sb_bt, 1 + random_uint32());
1201412045
sbrec_bfd_set_src_port(sb_bt, udp_src);
1201512046
sbrec_bfd_set_status(sb_bt, nb_bt->status);
12016-
if (op->sb->chassis) {
12017-
sbrec_bfd_set_chassis_name(sb_bt, op->sb->chassis->name);
12047+
if (chassis) {
12048+
sbrec_bfd_set_chassis_name(sb_bt, chassis->name);
1201812049
}
1201912050

1202012051
int min_tx = nb_bt->n_min_tx ? nb_bt->min_tx[0] : BFD_DEF_MINTX;
@@ -12025,7 +12056,14 @@ bfd_table_sync(struct ovsdb_idl_txn *ovnsb_txn,
1202512056
: BFD_DEF_DETECT_MULT;
1202612057
sbrec_bfd_set_detect_mult(sb_bt, d_mult);
1202712058
} else {
12028-
if (strcmp(bfd_e->sb_bt->status, nb_bt->status)) {
12059+
if (!chassis && strcmp(nb_bt->status, "admin_down")) {
12060+
/* NB status has already been set to "down" above; make
12061+
* the SB status follow it instead of syncing the stale
12062+
* SB status back into the NB. */
12063+
if (strcmp(bfd_e->sb_bt->status, "down")) {
12064+
sbrec_bfd_set_status(bfd_e->sb_bt, "down");
12065+
}
12066+
} else if (strcmp(bfd_e->sb_bt->status, nb_bt->status)) {
1202912067
if (!strcmp(nb_bt->status, "admin_down") ||
1203012068
!strcmp(bfd_e->sb_bt->status, "admin_down")) {
1203112069
sbrec_bfd_set_status(bfd_e->sb_bt, nb_bt->status);
@@ -12035,10 +12073,10 @@ bfd_table_sync(struct ovsdb_idl_txn *ovnsb_txn,
1203512073
}
1203612074

1203712075
build_bfd_update_sb_conf(nb_bt, bfd_e->sb_bt);
12038-
if (op->sb->chassis && !strcmp(op->sb->chassis->name,
12039-
bfd_e->sb_bt->chassis_name)) {
12040-
sbrec_bfd_set_chassis_name(bfd_e->sb_bt,
12041-
op->sb->chassis->name);
12076+
12077+
const char *chassis_name = chassis ? chassis->name : "";
12078+
if (strcmp(chassis_name, bfd_e->sb_bt->chassis_name)) {
12079+
sbrec_bfd_set_chassis_name(bfd_e->sb_bt, chassis_name);
1204212080
}
1204312081
}
1204412082

@@ -12089,6 +12127,50 @@ build_bfd_map(const struct nbrec_bfd_table *nbrec_bfd_table,
1208912127
}
1209012128
}
1209112129

12130+
/* Returns false if a tracked SB Port_Binding change modified the chassis
12131+
* a BFD session runs on, i.e. the "chassis" column of a Port_Binding
12132+
* whose logical port (or, for a chassisredirect port, whose distributed
12133+
* port) has a BFD session changed. bfd_table_sync() then needs to run
12134+
* again to reevaluate the session ownership: e.g. the chassis running
12135+
* the session went away and the binding was released, in which case the
12136+
* NB/SB BFD status must be marked "down". Returns true if none of the
12137+
* tracked changes are relevant to BFD. */
12138+
bool
12139+
bfd_sync_handle_sb_port_binding_changes(
12140+
const struct sbrec_port_binding_table *sbrec_port_binding_table,
12141+
const struct hmap *lr_ports, const struct sset *bfd_ports)
12142+
{
12143+
const struct sbrec_port_binding *pb;
12144+
SBREC_PORT_BINDING_TABLE_FOR_EACH_TRACKED (pb,
12145+
sbrec_port_binding_table) {
12146+
if (sbrec_port_binding_is_new(pb) ||
12147+
sbrec_port_binding_is_deleted(pb)) {
12148+
continue;
12149+
}
12150+
12151+
if (!sbrec_port_binding_is_updated(pb,
12152+
SBREC_PORT_BINDING_COL_CHASSIS)) {
12153+
continue;
12154+
}
12155+
12156+
const struct ovn_port *op = ovn_port_find(lr_ports,
12157+
pb->logical_port);
12158+
if (!op) {
12159+
continue;
12160+
}
12161+
12162+
if (op->primary_port) {
12163+
op = op->primary_port;
12164+
}
12165+
12166+
if (bfd_is_port_running(bfd_ports, op->key)) {
12167+
return false;
12168+
}
12169+
}
12170+
12171+
return true;
12172+
}
12173+
1209212174
void
1209312175
build_ic_learned_svc_monitors_map(
1209412176
struct hmap *ic_learned_svc_monitors_map,

northd/northd.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,9 @@ void bfd_table_sync(struct ovsdb_idl_txn *, const struct nbrec_bfd_table *,
10301030
struct sset *);
10311031
void build_bfd_map(const struct nbrec_bfd_table *,
10321032
const struct sbrec_bfd_table *, struct hmap *);
1033+
bool bfd_sync_handle_sb_port_binding_changes(
1034+
const struct sbrec_port_binding_table *,
1035+
const struct hmap *lr_ports, const struct sset *bfd_ports);
10331036

10341037
void build_ic_learned_svc_monitors_map(
10351038
struct hmap *ic_learned_svc_monitors_map,

ovn-nb.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6704,6 +6704,13 @@ or
67046704
</li>
67056705
</ul>
67066706
</p>
6707+
<p>
6708+
The status is normally reported by the <code>ovn-controller</code>
6709+
running the BFD session. If no chassis currently owns the session
6710+
for the logical port (e.g. the gateway-port binding was released),
6711+
<code>ovn-northd</code> sets the status of a session that is not
6712+
<code>admin_down</code> to <code>down</code>.
6713+
</p>
67076714
</column>
67086715
</group>
67096716
</table>

ovn-sb.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5411,7 +5411,9 @@ tcp.flags = RST;
54115411
</column>
54125412

54135413
<column name="chassis_name">
5414-
The name of the chassis where the logical port is bound.
5414+
The name of the chassis running the BFD session. For a
5415+
distributed gateway port this is the chassis its chassisredirect
5416+
port is bound to. Empty when there is no currently bound owner.
54155417
</column>
54165418

54175419
<column name="options">

tests/ovn-inc-proc-graph-dump.at

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ digraph "Incremental-Processing-Engine" {
164164
routes -> bfd_sync [[label="bfd_sync_routes_change_handler"]];
165165
route_policies -> bfd_sync [[label=""]];
166166
northd -> bfd_sync [[label="bfd_sync_northd_change_handler"]];
167+
SB_port_binding -> bfd_sync [[label="bfd_sync_sb_port_binding_change_handler"]];
167168
SB_learned_route [[style=filled, shape=box, fillcolor=white, label="SB_learned_route"]];
168169
learned_route_sync [[style=filled, shape=box, fillcolor=white, label="learned_route_sync"]];
169170
SB_learned_route -> learned_route_sync [[label="learned_route_sync_sb_learned_route_change_handler"]];

tests/ovn-northd.at

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4419,8 +4419,15 @@ wait_row_count bfd 5
44194419

44204420
# Simulate BFD up in Southbound for an automatically created entry.
44214421
# This entry is referenced so the state in the Northbound should also
4422-
# become "up".
4422+
# become "up". A BFD session can only be up if some chassis runs it, so
4423+
# turn r0-sw2 into a gateway port and bind its chassisredirect port
4424+
# first, like ovn-controller would do.
44234425
wait_column down nb:bfd status logical_port=r0-sw2
4426+
check ovn-sbctl chassis-add hv1 geneve 127.0.0.1
4427+
check ovn-nbctl --wait=sb lrp-set-gateway-chassis r0-sw2 hv1 10
4428+
hv1_uuid=$(fetch_column chassis _uuid name=hv1)
4429+
check ovn-sbctl set port_binding cr-r0-sw2 chassis=$hv1_uuid
4430+
wait_column hv1 bfd chassis_name logical_port=r0-sw2
44244431
bfd2_uuid=$(fetch_column bfd _uuid logical_port=r0-sw2)
44254432
check ovn-sbctl set bfd $bfd2_uuid status=up
44264433
wait_column up nb:bfd status logical_port=r0-sw2
@@ -4468,6 +4475,122 @@ OVN_CLEANUP_NORTHD
44684475
AT_CLEANUP
44694476
])
44704477

4478+
OVN_FOR_EACH_NORTHD_NO_HV([
4479+
AT_SETUP([BFD sessions marked down when no chassis can run them])
4480+
AT_KEYWORDS([northd-bfd])
4481+
ovn_start
4482+
4483+
check ovn-sbctl chassis-add hv1 geneve 127.0.0.1
4484+
4485+
check ovn-nbctl lr-add r0
4486+
check ovn-nbctl lrp-add r0 r0-public 00:00:20:20:12:13 172.16.0.1/24
4487+
check ovn-nbctl ls-add public
4488+
check ovn-nbctl lsp-add-router-port public public-r0 r0-public
4489+
check ovn-nbctl --wait=sb lrp-set-gateway-chassis r0-public hv1 10
4490+
4491+
bfd_uuid=$(ovn-nbctl create bfd logical_port=r0-public dst_ip=172.16.0.50)
4492+
check test -n "$bfd_uuid"
4493+
check ovn-nbctl --bfd=$bfd_uuid lr-route-add r0 100.0.0.0/8 172.16.0.50
4494+
4495+
# The gateway port is not bound to any chassis yet, so no chassis runs
4496+
# the BFD session: both NB and SB status must be "down".
4497+
wait_column down nb:bfd status logical_port=r0-public
4498+
wait_column down bfd status logical_port=r0-public
4499+
check_column "" bfd chassis_name logical_port=r0-public
4500+
4501+
# Simulate hv1 claiming the chassisredirect port and the BFD session
4502+
# coming up, like ovn-controller would do.
4503+
hv1_uuid=$(fetch_column chassis _uuid name=hv1)
4504+
check ovn-sbctl set port_binding cr-r0-public chassis=$hv1_uuid
4505+
wait_column hv1 bfd chassis_name logical_port=r0-public
4506+
4507+
sb_bfd_uuid=$(fetch_column bfd _uuid logical_port=r0-public)
4508+
check ovn-sbctl set bfd $sb_bfd_uuid status=up
4509+
wait_column up nb:bfd status logical_port=r0-public
4510+
4511+
# The BFD monitored route is programmed while the session is up.
4512+
OVS_WAIT_UNTIL([ovn-sbctl dump-flows r0 > r0flows dnl
4513+
&& grep -q "ip4.dst == 100.0.0.0/8" r0flows])
4514+
4515+
# Release the chassisredirect binding while leaving the Chassis row intact.
4516+
# This specifically exercises the incremental Port_Binding.chassis input.
4517+
# The session must be marked "down" in both databases and the BFD monitored
4518+
# route must be withdrawn.
4519+
check ovn-sbctl clear port_binding cr-r0-public chassis
4520+
wait_column down nb:bfd status logical_port=r0-public
4521+
wait_column down bfd status logical_port=r0-public
4522+
check_column "" bfd chassis_name logical_port=r0-public
4523+
OVS_WAIT_UNTIL([ovn-sbctl dump-flows r0 > r0flows dnl
4524+
&& ! grep -q "ip4.dst == 100.0.0.0/8" r0flows])
4525+
4526+
# Give the session an owner again: northd must track the owning chassis
4527+
# but must not flip the status to "up" on its own; that is up to
4528+
# ovn-controller's BFD negotiation.
4529+
check ovn-sbctl set port_binding cr-r0-public chassis=$hv1_uuid
4530+
wait_column hv1 bfd chassis_name logical_port=r0-public
4531+
check ovn-nbctl --wait=sb sync
4532+
check_column down nb:bfd status logical_port=r0-public
4533+
check_column down bfd status logical_port=r0-public
4534+
4535+
# Once "ovn-controller" reports the session up again, the status must
4536+
# propagate to the Northbound as usual.
4537+
check ovn-sbctl set bfd $sb_bfd_uuid status=up
4538+
wait_column up nb:bfd status logical_port=r0-public
4539+
4540+
# An "admin_down" session must be left untouched when it loses its
4541+
# owner.
4542+
route_uuid=$(fetch_column nb:logical_router_static_route _uuid \
4543+
ip_prefix="100.0.0.0/8")
4544+
check ovn-nbctl clear logical_router_static_route $route_uuid bfd
4545+
wait_column admin_down nb:bfd status logical_port=r0-public
4546+
wait_column admin_down bfd status logical_port=r0-public
4547+
check ovn-sbctl clear port_binding cr-r0-public chassis
4548+
check ovn-nbctl --wait=sb sync
4549+
check_column admin_down nb:bfd status logical_port=r0-public
4550+
check_column admin_down bfd status logical_port=r0-public
4551+
check_column "" bfd chassis_name logical_port=r0-public
4552+
4553+
OVN_CLEANUP_NORTHD
4554+
AT_CLEANUP
4555+
])
4556+
4557+
OVN_FOR_EACH_NORTHD_NO_HV([
4558+
AT_SETUP([BFD ownership on an l3gateway port])
4559+
AT_KEYWORDS([northd-bfd])
4560+
ovn_start
4561+
4562+
check ovn-sbctl chassis-add hv1 geneve 127.0.0.1
4563+
hv1_uuid=$(fetch_column chassis _uuid name=hv1)
4564+
4565+
check ovn-nbctl lr-add r0
4566+
check ovn-nbctl set logical_router r0 options:chassis=hv1
4567+
check ovn-nbctl lrp-add r0 r0-public 00:00:20:20:12:13 172.16.0.1/24
4568+
check ovn-nbctl ls-add public
4569+
check ovn-nbctl lsp-add-router-port public public-r0 r0-public
4570+
4571+
bfd_uuid=$(ovn-nbctl create bfd logical_port=r0-public dst_ip=172.16.0.50)
4572+
check test -n "$bfd_uuid"
4573+
check ovn-nbctl --bfd=$bfd_uuid lr-route-add r0 100.0.0.0/8 172.16.0.50
4574+
4575+
wait_column l3gateway port_binding type logical_port=r0-public
4576+
check ovn-sbctl set port_binding r0-public chassis=$hv1_uuid
4577+
wait_column hv1 bfd chassis_name logical_port=r0-public
4578+
4579+
sb_bfd_uuid=$(fetch_column bfd _uuid logical_port=r0-public)
4580+
check ovn-sbctl set bfd $sb_bfd_uuid status=up
4581+
wait_column up nb:bfd status logical_port=r0-public
4582+
4583+
# The direct l3gateway binding is the owner. Releasing it must invalidate
4584+
# the stale status through the same incremental Port_Binding input.
4585+
check ovn-sbctl clear port_binding r0-public chassis
4586+
wait_column down nb:bfd status logical_port=r0-public
4587+
wait_column down bfd status logical_port=r0-public
4588+
check_column "" bfd chassis_name logical_port=r0-public
4589+
4590+
OVN_CLEANUP_NORTHD
4591+
AT_CLEANUP
4592+
])
4593+
44714594
OVN_FOR_EACH_NORTHD_NO_HV([
44724595
AT_SETUP([ovn -- check CoPP config])
44734596
AT_KEYWORDS([northd-CoPP])

0 commit comments

Comments
 (0)