OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

ovn-bgp-agent — Async processing

Asynchronous processing — ovn-bgp-agent — branch stable/2026.2

Branch stable/2026.2 @ f770768b41dc (7.0.0-2-gf770768) — repository https://opendev.org/openstack/ovn-bgp-agent — analysed on 2026-10-28 External references: ovsdbapp: = ovsdbapp stable/2026.2 @ 0fb5a1b9a675; neutron-lib: = neutron-lib stable/2026.2 @ 1ee72052251a.

ovn-bgp-agent has no API. Everything it does is triggered by one of three things:

  1. OVSDB events — row create/update/delete notifications received from the OVN Southbound (SB) or Northbound (NB) database through an ovsdbapp IDL (§ 2 to § 5);
  2. Two periodic loops — a full resynchronisation (sync, every reconcile_interval s, default 300) and an FRR base-configuration check (frr_sync, every frr_reconcile_interval s, default 15) (§ 1);
  3. Reconnection — a re-created Chassis/Chassis_Private row after an OVSDB reconnection forces a full sync (§ 1.3).

The effects are always local to the node: kernel addresses, routes, rules and neighbours (netlink through privsep), sysctl flags, OVS flows and ports (ovs-vsctl/ovs-ofctl/ovs-appctl through privsep), FRR configuration (vtysh through privsep) and, with exposing_method = ovn, a node-local OVN NB database. The agent never writes to the OpenStack OVN databases; it only reads them. See 03-interactions.

0. Summary of triggers

Which events are registered depends on the driver ([DEFAULT] driver) and on configuration.

DriverTriggerTypePurposeReadsWrites / emitsDefined at
allBGPAgent.synctimer, reconcile_interval (300 s)full reconciliation of the node state with OVNOVS, OVN SB or NB, kernelkernel, OVS, FRRovn_bgp_agent/agent.py:49
allBGPAgent.frr_synctimer, frr_reconcile_interval (15 s)re-apply the FRR VRF-leak configuration (FRR restarts)FRR (show ip bgp summary json if no router-id)vtysh -f, VRF and dummy NICovn_bgp_agent/agent.py:57
SB driversChassisCreateEvent / ChassisPrivateCreateEventOVSDB SB Chassis / Chassis_Private CREATE, name == <own chassis>full sync after a reconnection (not on the first notification)—as syncovn_bgp_agent/drivers/openstack/watchers/base_watcher.py:78
ovn_bgp_driverPortBindingChassisCreatedEventSB Port_Binding UPDATEa VM/virtual/cr-lrp port gets bound here or goes up → expose its IPsrowIP on bgp_nic, ip rule/route, ARP/NDP proxyovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:29
ovn_bgp_driverPortBindingChassisDeletedEventSB Port_Binding UPDATE/DELETEport leaves this chassis, goes down or is deleted → withdrawrowremoves the aboveovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:62
ovn_bgp_driverFIPSetEvent / FIPUnsetEventSB Port_Binding UPDATE of nat_addresses on a patch portFIP associated/disassociated on a router whose gateway is hererow, oldexpose/withdraw FIPovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:99, ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:137
ovn_bgp_driverOVNLBMemberCreateEvent / OVNLBMemberDeleteEventSB Load_Balancer CREATE/DELETEOVN (Octavia) LB with VIP on a provider network behind a local gatewaySB Port_Bindingexpose/withdraw VIPovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:382, ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:450
ovn_bgp_driverLocalnetCreateDeleteEventSB Port_Binding CREATE/DELETE, type == localnetprovider network created/deleted → full sync—as syncovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:472
ovn_bgp_driver (+ tenant exposure)SubnetRouterAttachedEvent / SubnetRouterDetachedEventSB Port_Binding CREATE/DELETE of an lrp-* patch porttenant subnet attached/detached to a router whose gateway is hereSBip rule + route via cr-lrp; tenant VM IPsovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:176, ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:235
ovn_bgp_driver (+ tenant exposure)TenantPortCreatedEvent / TenantPortDeletedEventSB Port_Binding UPDATE/DELETE (chassis set/unset)tenant VM bound/unbound anywhere, on a subnet exposed hereSBIP on bgp_nicovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:260, ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:299
ovn_bgp_driver (+ tenant exposure)OVNLBVIPPortEventSB Port_Binding CREATE/DELETE of an unbound, MAC-less portOVN LB VIP on a tenant networkrowIP on bgp_nicovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:345
nb_ovn_bgp_driverLogicalSwitchPortProviderCreateEvent / …DeleteEventNB Logical_Switch_Port UPDATE/DELETEVM on a provider network bound/up here, or leavingrowexpose/withdraw IPovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:33, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:96
nb_ovn_bgp_driver (distributed FIPs)LogicalSwitchPortFIPCreateEvent / …DeleteEvent, NATMACAddedEventNB Logical_Switch_Port UPDATE/DELETE; NB NAT UPDATEFIP of a VM hosted here (distributed NAT)NB NATexpose/withdraw FIPovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:159, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:260, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:882
nb_ovn_bgp_driver (centralized FIPs)ExposeFIPOnCRLRP / WithdrawFIPOnCRLRP, CrLrpChassisChangeExposeEvent / …WithdrawEventNB NAT CREATE/DELETE; NB Logical_Router_Port UPDATE of status:hosting-chassisFIPs handled on the node hosting the router gatewayNB NAT, Logical_Router_Portexpose/withdraw FIPovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:950, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:995
nb_ovn_bgp_driverChassisRedirectCreateEvent / …DeleteEventNB Logical_Router_Port UPDATE/DELETE, status:hosting-chassisrouter gateway (cr-lrp) moves to/away from this noderowexpose/withdraw gateway IP, its subnets and LBsovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:401, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:435
nb_ovn_bgp_driver (+ tenant exposure)LogicalSwitchPortSubnetAttachEvent / …DetachEventNB Logical_Switch_Port UPDATE/DELETE of type=router, device_owner=network:router_interfacesubnet attached to/detached from a local gateway routerNBip rule/route, tenant IPsovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:469, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:521
nb_ovn_bgp_driver (+ tenant exposure, host method)LogicalSwitchPortTenantCreateEvent / …DeleteEventNB Logical_Switch_Port UPDATE/DELETEtenant VM up/down on an exposed subnet—IP on bgp_nic or VRF routeovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:610, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:663
nb_ovn_bgp_driverOVNLBCreateEvent / OVNLBDeleteEventNB Load_Balancer UPDATE/DELETELB VIP or VIP-FIP added/removed, router external gateway set/unsetNB Logical_Switch_Port, NATexpose/withdraw VIP/FIPovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:712, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:769
nb_ovn_bgp_driverOVNPFCreateEvent / OVNPFDeleteEventNB Load_Balancer CREATE/DELETE, name pf-floatingip-*port-forwarding FIP—expose/withdraw FIPovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:864, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:873
nb_ovn_bgp_driverLocalnetCreateDeleteEvent (non-vrf) / LogicalSwitchUpdateEvent (vrf)NB Logical_Switch_Port CREATE/DELETE type=localnet; NB Logical_Switch UPDATE/DELETE of neutron_bgpvpn:type/vniprovider network or EVPN settings changed → full sync—as syncovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:385, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:348
nb_ovn_bgp_driverDistributedFlagChangedEventNB NB_Global UPDATE of external_ids:neutron:fip-distributedswitch distributed ↔ centralized FIP handlingrowre-register events, sync + frr_syncovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:1049
ovn_evpn_driverPortBindingChassisCreatedEvent / …DeletedEvent (EVPN)SB Port_Binding UPDATE/DELETE of type=chassisredirectrouter gateway with EVPN annotations bound here → build the VRF/VXLAN plumbingSBVRF, bridge, VXLAN, veth/VLAN, FRR VRF, OVS flowsovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py:28, ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py:53
ovn_evpn_driverSubnetRouterAttachedEvent / …DetachedEvent (EVPN)SB Port_Binding CREATE/UPDATE/DELETE of a patch port carrying neutron_bgpvpn:vni/assubnet or gateway annotated for EVPNSBVRF routes, OVS flowsovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py:81, ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py:116
ovn_evpn_driverTenantPortCreatedEvent / …DeletedEvent, LocalnetCreateDeleteEvent (EVPN)SB Port_Bindingtenant VM IP on lo-<vni>; sync on localnet changesSBIP on dummyovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py:151, ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py:204
ovn_stretched_l2_bgp_driverSubnetRouterAttachedEvent / SubnetRouterUpdateEvent / SubnetRouterDetachedEvent, PortBindingChassisCreatedEvent / …DeletedEventSB Port_Binding (bgp_watcher classes)route tenant subnets via the gateway IP in the BGP VRF tableSBroutes in table bgp_vrf_table_idovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:103

Totals: 2 timers; 55 event classes in ovn_bgp_agent/drivers/openstack/watchers/ (11 base or abstract, 44 concrete), of which a given driver registers between 5 (ovn_stretched_l2_bgp_driver) and 18 (nb_ovn_bgp_driver).

1. Process-wide mechanisms

1.1 Start-up and the two loops (ovn-bgp-agent)

  • Entry point: console script ovn-bgp-agent = ovn_bgp_agent.cmd.agent:start (setup.cfg), which calls ovn_bgp_agent/agent.py:75: register options (ovn_bgp_agent/config.py:236), parse the command line with project='bgp-agent' (ovn_bgp_agent/config.py:243), set up logging, initialise privsep with [agent] root_helper (ovn_bgp_agent/config.py:258), then oslo_service.service.launch(CONF, BGPAgent()).
  • Driver choice: BGPAgent.__init__ loads [DEFAULT] driver from the stevedore namespace ovn_bgp_agent.drivers (ovn_bgp_agent/drivers/driver_api.py:25, ovn_bgp_agent/agent.py:35). Four drivers exist: ovn_bgp_driver (default), nb_ovn_bgp_driver, ovn_evpn_driver, ovn_stretched_l2_bgp_driver (ovn_bgp_agent/config.py:64).
  • Start: driver.start() (ovn_bgp_agent/agent.py:41) connects to the local OVS database, reads the chassis identity, applies the base FRR/VRF configuration and opens the OVN IDL with its event set (per driver, § 2 to § 5). **No initial sync is called explicitly**: the first full sync runs when the first timer tick fires, which FixedIntervalLoopingCall.start(interval=…) does immediately (no initial_delay) [inferred from oslo.service defaults].
  • Loops: two FixedIntervalLoopingCall (ovn_bgp_agent/agent.py:44-47):
    • sync every reconcile_interval seconds (default 300, ovn_bgp_agent/config.py:26);
    • frr_sync every frr_reconcile_interval seconds (default 15, ovn_bgp_agent/config.py:29). Both wrappers catch every exception, log Unexpected exception while running the sync: %s / …the frr sync: %s and keep the loop alive (ovn_bgp_agent/agent.py:52-64).

1.2 Concurrency model

  • One notification thread per IDL: ovsdbapp's RowEventHandler evaluates match_fn in the IDL connection thread under a lock, queues the matches and runs them one by one in a single notify_loop thread (ovsdbapp:ovsdbapp/event.py:153, ovsdbapp:ovsdbapp/event.py:186). Events of one IDL are therefore processed sequentially, in arrival order.
  • Driver lock: every public driver method that touches the node (expose_*, withdraw_*, sync, frr_sync) is decorated @lockutils.synchronized('bgp') (SB and stretched drivers, ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:128), 'nbbgp' (NB driver, ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:286) or 'evpn' (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:100). These are in-process semaphores (no external=True): they serialise the timer threads with the event thread inside one agent, nothing more. Running two agents on one node is not protected.
  • **_SYNC_STATE_LOCK**: each watcher module declares a ReaderWriterLock and every _run takes its read lock (ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:26, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:30). No code ever takes the write lock, so this lock never excludes anything; the effective serialisation is the driver lock.
  • Events that re-enter the lock: LocalnetCreateDeleteEvent, LogicalSwitchUpdateEvent, ChassisCreateEventBase and DistributedFlagChangedEvent call agent.sync(), which takes the driver lock; a long sync therefore blocks all other events of that IDL.
  • Matching runs on cached state: many match_fn read driver caches (ovn_local_cr_lrps, ovn_local_lrps, _exposed_ips, provider_ovn_lbs) without the driver lock. A match can be evaluated against a cache that a running sync is rebuilding (sync starts by clearing all caches, ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:137-143, ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:293). Events missed this way are repaired by the next sync (300 s by default) [inferred].

1.3 Reconnection: ChassisCreateEvent / ChassisPrivateCreateEvent

  • Trigger: SB Chassis or Chassis_Private CREATE with condition name == <own chassis> (ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py:78-101). The SB IDL is also restricted server-side to this chassis's row (ovn_bgp_agent/drivers/openstack/utils/ovn.py:111-114).
  • Purpose: after an OVSDB reconnection the IDL replays all rows as CREATE; the agent uses its own chassis row as the signal and runs a full sync.
  • Flow: the first notification (initial dump) only clears first_time; any later one logs Connection to OVSDB established, doing a full sync and calls agent.sync() (ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py:89-94).
  • Registered by: ovn_bgp_driver and ovn_evpn_driver. Not by ovn_stretched_l2_bgp_driver nor nb_ovn_bgp_driver: the NB driver has no reconnection hook and relies on the 300 s timer.

1.4 Error handling common to all events

  • Event.run wraps _run and logs Unexpected exception while running the event action (ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py:31-36).
  • Exceptions raised in match_fn are caught by ovsdbapp, logged as Event not matched due to this exception: and treated as "no match" (ovsdbapp:ovsdbapp/event.py:153-158).
  • Five NB events override run instead of _run (NATMACAddedEvent, ExposeFIPOnCRLRP, WithdrawFIPOnCRLRP, CrLrpChassisChangeExposeEvent, CrLrpChassisChangeWithdrawEvent, DistributedFlagChangedEvent: ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:940, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:958, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:971, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:1014, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:1042, ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:1081). Their exceptions bypass the agent's handler and are logged by ovsdbapp as Unexpected exception in notify_loop (ovsdbapp:ovsdbapp/event.py:205-208); the event is lost, the loop survives.
  • No event is retried. Missed or failed work is recovered only by the next sync.

2. ovn_bgp_driver (SB driver, default)

2.1 Start (OVNBGPDriver.start)

ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:77:

  1. OVS IDL on ovsdb_connection; chassis = Open_vSwitch.external_ids:system-id; SB remote = [ovn] ovn_sb_connection or external_ids:ovn-remote (ovn_bgp_agent/drivers/openstack/utils/ovs.py:290, ovn_bgp_agent/drivers/openstack/utils/ovs.py:307).
  2. ensure_base_bgp_configuration() (§ 6.1).
  3. If clear_vrf_routes_on_startup, delete every non-local, non-FRR route of table bgp_vrf_table_id (ovn_bgp_agent/utils/linux_net.py:607).
  4. SB IDL on tables Port_Binding, Chassis, Datapath_Binding, Load_Balancer, Chassis_Private, Logical_DP_Group (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:42) with the event set of _get_events (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:109): 9 base events, plus 5 when expose_tenant_networks or expose_ipv6_gua_tenant_networks is set.

2.2 sync (SB)

ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:134. Purpose: make the node state match OVN SB, adding what is missing and removing leftovers.

  1. Reset all caches (routing tables, bridge mappings, local cr-lrps/lrps, LBs, flows).
  2. For each ovn-bridge-mappings entry network:bridge of the local OVS (ovn_bgp_agent/drivers/openstack/utils/ovs.py:317), with a 1-based index:
    • ensure the bridge exists as a kernel link and is up (ovn_bgp_agent/utils/linux_net.py:109);
    • ensure a routing table named after the bridge in /etc/iproute2/rt_tables (random free number in 1–252, ovn_bgp_agent/utils/linux_net.py:232) with IPv4 and IPv6 default routes out of the bridge (ovn_bgp_agent/utils/linux_net.py:281); collect the routes of that table that are not the expected defaults;
    • for each VLAN tag of the localnet ports of that network (SB Port_Binding type=localnet, options:network_name): ensure a VLAN device <bridge>.<tag> and its ARP/NDP addresses (ovn_bgp_agent/utils/linux_net.py:420, ovn_bgp_agent/utils/linux_net.py:139);
    • put 169.254.<idx>.<idx> and fd53:d91e:400:7f17::<idx>:<idx> on the bridge and enable proxy_arp / proxy_ndp (ovn_bgp_agent/utils/linux_net.py:139-170);
    • read the bridge MAC and the OpenFlow ports of its patch-provnet-* ports, ensure the cookie-999 "MAC tweak" flows and delete other cookie-999 flows (ovn_bgp_agent/drivers/openstack/utils/ovs.py:94, ovn_bgp_agent/drivers/openstack/utils/ovs.py:123).
  3. Read the /32 and /128 addresses on bgp_nic and the ip rules pointing to the bridge tables (ovn_bgp_agent/utils/linux_net.py:472, ovn_bgp_agent/utils/linux_net.py:539).
  4. Ports bound here: all SB Port_Binding rows whose chassis is this chassis (full table scan, ovn_bgp_agent/drivers/openstack/utils/ovn.py:542) → _ensure_port_exposed (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:270) → _expose_ip (§ 2.3).
  5. Gateways here: SB chassisredirect ports bound here → FIPs found in the nat_addresses of the matching patch port whose VM is on this chassis (ovn_bgp_agent/drivers/openstack/utils/ovn.py:559).
  6. For every local cr-lrp: process each lrp-* patch port of its router (tenant subnets, § 2.5), then the OVN LBs whose VIP is on the provider network and whose members sit behind this router (ovn_bgp_agent/drivers/openstack/utils/ovn.py:705).
  7. Cleanup: delete leftover IPs on bgp_nic, leftover ip rules, leftover routes of the bridge tables (ovn_bgp_agent/utils/linux_net.py:567) and VLAN devices whose tag no longer exists in OVN (ovn_bgp_agent/drivers/openstack/utils/wire.py:598).
  8. Cost: several full scans of SB Port_Binding per run (bound ports, cr-lrps, patch ports for every FIP lookup through get_fip_associated, ovn_bgp_agent/drivers/openstack/utils/ovn.py:517), one netlink dump per check, one ovs-ofctl dump-flows per bridge. On a large SB this is the main CPU cost of the agent [inferred].
  9. Idempotency: every write uses "replace"/"may-exist" semantics or ignores EEXIST (§ 6.3); a replayed sync converges.

2.3 Port bound here: PortBindingChassisCreatedEvent / PortBindingChassisDeletedEvent

  • Trigger: SB Port_Binding UPDATE (create event); UPDATE or DELETE (delete event) (ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:29-96).
  • Match (expose): mac[0] contains an IP ("<mac> <ip>…"), up[0] is true, chassis is this chassis, and either the chassis just changed/was empty or up just went from false to true.
  • Match (withdraw): mac[0] contains an IP and either the row is deleted while bound here, or the old chassis was this one and the new is different/empty, or up went true → false while still bound here.
  • Action: only for type in "", chassisredirect, virtual (ovn_bgp_agent/constants.py:20): expose_ip(ips, row) / withdraw_ip(ips, row) where ips = mac[0].split(' ')[1:].
  • **_expose_ip** (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:548), by port kind:
    • VM or virtual port on a provider network (the datapath has a localnet port, ovn_bgp_agent/drivers/openstack/utils/ovn.py:494): find the bridge/VLAN of the network, wire the IPs (ip rule to <ip> lookup <bridge table>, route <ip> dev <bridge>[.<vlan>] in that table; for virtual ports also an NDP proxy for the IPv6 neutron:cidrs), then add the IPs as /32 or /128 on bgp_nic (§ 6.2). Returns the exposed IPs (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:562-586).
    • VM on a tenant network with a FIP: find the FIP by scanning patch ports' nat_addresses for the port name (ovn_bgp_agent/drivers/openstack/utils/ovn.py:517); expose the FIP on the provider network of the patch port (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:588-606). IPv4 only (comment at ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:589).
    • cr-lrp (router gateway): provider datapath = datapath of the patch port named after the cr-lrp; record the gateway in ovn_local_cr_lrps; wire the gateway IPs with a permanent ARP/NDP neighbour entry for the router MAC and an NDP proxy; announce them; then expose the router's FIPs, attached tenant subnets and provider LBs (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:624-657, ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:898).
  • **withdraw_ip** (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:660) mirrors this. Notable rules:
    • if the datapath no longer exists, the port is assumed to be on a provider network to avoid leaking an announcement (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:681-693);
    • for a virtual port, the NDP proxy for the provider CIDR is removed only if no other virtual port of that datapath is bound here and no local cr-lrp uses that provider (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:700-714);
    • for a cr-lrp, the IPv6 NDP proxy is removed only if it is the last local cr-lrp on that provider, then its subnets and LBs are withdrawn and the cache entry dropped (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:936-979).
  • Ordering guarantee: on withdraw, the IP is removed from bgp_nic (BGP withdraw) before the rule/route is removed (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:408-409); on expose, the IP is added after the wiring succeeded (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:341-346). This avoids black-holing traffic.

2.4 FIP association on a gateway: FIPSetEvent / FIPUnsetEvent

  • Trigger: SB Port_Binding UPDATE where chassis is empty, nat_addresses changed and the port is not an lrp-* port; acts only on type=patch (ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:99-173).
  • Data: each nat_addresses entry has the form "<mac> <ip1> <ip2>… is_chassis_resident(\"cr-lrp-<id>\")"; the agent groups IPs by cr-lrp and computes the IPs added (set) or removed (unset).
  • Action: expose_ip(ips, row, associated_port=<cr-lrp>) → exposes the FIPs only if the cr-lrp is bound here and the patch port's datapath is a provider network (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:609-621). Withdraw also proceeds when the cr-lrp has no chassis or was deleted (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:749-765).

2.5 Tenant subnets (only with expose_tenant_networks or expose_ipv6_gua_tenant_networks)

  • **SubnetRouterAttachedEvent** (SB Port_Binding CREATE of an unbound lrp-* patch port with an IP, not a chassis-redirect-port) → expose_subnet(<first CIDR of mac>, row) (ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:176-198).
  • **expose_subnet** (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:1087): proceed only if the router's cr-lrp is bound here and known, the port has a peer, and the address scope is allowed (§ 2.7). Then _expose_lrp_port (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:981):
    1. skip if the CIDR is the gateway's own (gateway port) or if only GUA exposure is configured and the CIDR is not a global IPv6;
    2. record the subnet in the cr-lrp cache and ovn_local_lrps;
    3. wire it: ip rule to <cidr> lookup <bridge table> and route <cidr> via <cr-lrp ip> dev <bridge>[.<vlan>] in that table; with advertisement_method_tenant_networks = subnet, also a route <cidr> via <cr-lrp ip> dev <bgp_nic> in table bgp_vrf_table_id (ovn_bgp_agent/drivers/openstack/utils/wire.py:831-872);
    4. announce every VM IP of that subnet already bound to a chassis (adds /32 or /128 on bgp_nic) (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:1023-1028, ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:354).
  • **SubnetRouterDetachedEvent** (DELETE) → withdraw_subnet (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:1113): removes the VM IPs of that network from bgp_nic, then unwires the subnet (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:1030-1084). If the router datapath is already gone (router deleted with its gateway), it only checks whether the port belongs to a local cr-lrp and stops.
  • **TenantPortCreatedEvent / TenantPortDeletedEvent** (SB Port_Binding UPDATE chassis empty → set, or set → empty / DELETE, on VM/virtual ports; MAC unknown handled through neutron:cidrs) → expose_remote_ip / withdraw_remote_ip (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:785-867): if the port's network is not a provider network and one of its router ports is in ovn_local_lrps, add/remove the IPs on bgp_nic (filtered by GUA mode and address scopes). The VM may be on any chassis: the node hosting the gateway announces it.
  • **OVNLBVIPPortEvent** (SB Port_Binding CREATE/DELETE of a port without MAC, chassis and up, matched only if a cr-lrp is local) → expose_ovn_lb / withdraw_ovn_lb with the first IP of neutron:cidrs: same path as a tenant IP (ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:345-379, ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:445).
  • Note: the match functions compare self.agent.ovn_local_lrps != [] (ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:279), but ovn_local_lrps is a dict, so the comparison is always true; the real filter happens later in the driver.

2.6 OVN load balancers on provider networks: OVNLBMemberCreateEvent / OVNLBMemberDeleteEvent

  • Trigger: SB Load_Balancer CREATE (matched only if a cr-lrp is local) / DELETE (matched only if the LB was exposed here) (ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:382-469).
  • Create flow: read the LB datapaths (ls_datapath_group/datapath_group for switches, lr_datapath_group for routers, ovn_bgp_agent/utils/helpers.py:40); find the VIP port (SB port without MAC/chassis whose neutron:port_name is ovn-lb-vip-<lb name>, ovn_bgp_agent/drivers/openstack/utils/ovn.py:751); pick the local cr-lrp whose provider datapath is the VIP's and whose subnets (or router) contain the LB members; then expose the VIP like a provider IP via that gateway (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:472-503).
  • Delete flow: withdraw every VIP recorded for that LB name through its gateway (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:505-528).

2.7 Address-scope filter

When [DEFAULT] address_scopes is set, an IP is exposed only if the port's neutron:subnet_pool_addr_scope4/6 for its IP version is in that list; a port with no scope is never exposed (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:1148-1167, ovn_bgp_agent/drivers/openstack/utils/driver_utils.py:34).

2.8 LocalnetCreateDeleteEvent (SB)

SB Port_Binding CREATE/DELETE with type=localnet → full sync (ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py:472-483). Creating a provider network therefore costs one full resynchronisation on every node.

3. nb_ovn_bgp_driver (NB driver)

3.1 Start

ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:207:

  1. OVS IDL; chassis = external_ids:hostname (fallback: the system hostname) and chassis_id = external_ids:system-id (ovn_bgp_agent/drivers/openstack/utils/ovs.py:296). The hostname is compared with LSP options:requested-chassis / neutron:host_id; the system-id with LRP status:hosting-chassis.
  2. NB remote = [ovn] ovn_nb_connection or external_ids:ovn-nb-remote (ovn_bgp_agent/drivers/openstack/utils/ovs.py:311-313).
  3. Base FRR configuration and optional VRF table cleanup, as for SB.
  4. NB IDL on Logical_Switch_Port, NAT, Logical_Switch, Logical_Router, Logical_Router_Port, Load_Balancer, DHCP_Options, NB_Global (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:44) with the base events (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:253).
  5. With exposing_method = ovn: a second NB IDL on the node-local OVN cluster [local_ovn_cluster] ovn_nb_connection, leader only, no events (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:240-245).
  6. Reads NB_Global.external_ids:neutron:fip-distributed (missing → distributed, with a warning) and registers the distributed or centralized FIP events (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:127-138, ovn_bgp_agent/drivers/openstack/utils/ovn.py:436). Centralized mode requires the NAT.gateway_port column (OVN ≥ 23.09), otherwise RuntimeError at start (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:54-58).

3.2 sync (NB)

ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:292. Warning — defect D16 (02 § 4): on this branch, with exposing_method = underlay, the per-bridge routing tables are never recorded in the driver (ovn_bgp_agent/drivers/openstack/utils/wire.py:34-38), so the wiring of step 5 fails with KeyError and the whole sync fails at step 7. The steps below describe the intended behaviour, recorded with the defect corrected in the harness (07 § 8).

  1. Reset caches.
  2. Base wiring per exposing_method (ovn_bgp_agent/drivers/openstack/utils/wire.py:34): underlay (as SB step 2), vrf (EVPN type-5 devices, § 6.4), ovn (node-local OVN router, § 6.5). **l2vni and dynamic raise UnsupportedWiringConfig** (ovn_bgp_agent/drivers/openstack/utils/wire.py:44), so every sync fails with these values although the option accepts them (ovn_bgp_agent/config.py:130-145).
  3. Router gateways hosted here: every Logical_Router_Port whose status:hosting-chassis is this chassis (full list, ovn_bgp_agent/drivers/openstack/utils/ovn.py:364) → exposed if its neutron:network_name is a provider network wired on this node (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:375).
  4. Subnets of those routers: LSPs up=true, type=router, neutron:device_owner=network:router_interface whose neutron:device_id is a local router (ovn_bgp_agent/drivers/openstack/utils/ovn.py:379) → _expose_subnet (§ 3.5).
  5. VM ports up and bound here (up=true, requested-chassis or neutron:host_id = hostname, ovn_bgp_agent/drivers/openstack/utils/ovn.py:354): with a FIP (neutron:port_fip) and distributed mode → expose the FIP; otherwise, if on a provider network → expose the IPs. In centralized mode, all FIPs whose NAT.gateway_port is hosted here are exposed (ovn_bgp_agent/drivers/openstack/utils/ovn.py:256).
  6. LBs whose lr_ref or neutron:router_name is a local router (ovn_bgp_agent/drivers/openstack/utils/ovn.py:408): port-forwarding LBs expose their FIP; others their VIP, and their VIP-FIP if any.
  7. Cleanup per method (ovn_bgp_agent/drivers/openstack/utils/wire.py:526): IPs on bgp_nic that are not in _exposed_ips, rules, routes, VLAN devices, flows (underlay); excess routes of each EVPN VLAN device (vrf); only bgp_nic IPs (ovn — local NB policies/routes/ARP proxies/static MAC bindings are not cleaned, ovn_bgp_agent/drivers/openstack/utils/wire.py:558-560).

3.3 Provider VM ports: LogicalSwitchPortProviderCreateEvent / …DeleteEvent

  • Create match (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:39-84): VM/virtual LSP with an IP in addresses[0]; its neutron:network_name is not a known tenant network; its chassis (requested-chassis, or neutron:host_id for virtual ports, ovn_bgp_agent/drivers/openstack/utils/driver_utils.py:43) is this host; it is up; the switch is a provider network wired here; and none of its IPs is exposed yet.
  • Delete match (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:102-150): the IPs are exposed here and the port is deleted, or went down without an additional binding (requested-chassis without a comma), or is no longer bound here.
  • Action: expose_ip / withdraw_ip (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:514, ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:578) → wiring + announcement as SB; the exposure is recorded in _exposed_ips[<switch>][<ip>], which drives later matches and the cleanup.
  • Live migration: requested-chassis holds src,dst during migration; the destination matches as soon as it is listed (ovn_bgp_agent/drivers/openstack/utils/driver_utils.py:57-64), the source keeps the announcement until the port goes down without an additional binding.

3.4 Floating IPs

  • Distributed mode (NB_Global external_ids:neutron:fip-distributed = "True" or absent):
    • LogicalSwitchPortFIPCreateEvent (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:159): VM/virtual LSP bound here, up, with neutron:port_fip; matches when it just came up, or the FIP changed and is not exposed yet, or the chassis changed. The class docstring documents the 9 NB updates of a live migration and why only the "port up" one is used (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:172-195). Action: read the port's NAT row for external_ip, external_mac and neutron:fip_network_id (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:673-694), then expose the FIP on provider switch neutron-<fip_network_id>.
    • LogicalSwitchPortFIPDeleteEvent (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:260): the exposed FIP's port is deleted, goes down, changes FIP or leaves this host → withdraw the old FIP (or the current one).
    • NATMACAddedEvent (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:882): dnat_and_snat NAT UPDATE whose external_mac just changed, whose LSP is a VM port requested on this host (or a virtual port whose neutron:host_id is this host) → expose the FIP. This covers the case where Neutron sets the external MAC after the port is up.
  • Centralized mode ("False"; requires OVN ≥ 23.09):
    • ExposeFIPOnCRLRP / WithdrawFIPOnCRLRP: dnat_and_snat NAT CREATE/DELETE whose LSP is a VM/virtual port and whose gateway_port is hosted on this chassis → expose the FIP with the gateway port's MAC (ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py:117-149, ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:89-112).
    • CrLrpChassisChangeExposeEvent / …WithdrawEvent: LRP status:hosting-chassis moved to / away from this chassis → expose/withdraw every dnat_and_snat NAT of that gateway port (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:975-1046, ovn_bgp_agent/drivers/openstack/utils/ovn.py:457).
  • Switching modes: DistributedFlagChangedEvent (NB_Global UPDATE where the flag changes and differs from the current mode) sets agent.distributed, which validates the OVN version, swaps the NAT exposer methods, unwatches the old FIP events and watches the new ones, then runs sync and frr_sync (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:1049-1091, ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:173-187).

3.5 Router gateways and tenant subnets

  • **ChassisRedirectCreateEvent / …DeleteEvent** (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:401-466): LRP UPDATE with networks, where status:hosting-chassis changed to this chassis (create) or from this chassis / the row is deleted while hosted here (delete). Action: expose_ip / withdraw_ip with an LRP description (mac, networks, neutron:network_name, neutron:router_name) (ovn_bgp_agent/drivers/openstack/utils/port.py:74). For a gateway, after wiring the IPs the driver records ovn_local_cr_lrps[<router>], exposes the router's subnets and LBs (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:549-572); withdraw does the reverse and drops the entry (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:611-633).
  • **LogicalSwitchPortSubnetAttachEvent / …DetachEvent** (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:469-607): router-interface LSP (type=router, neutron:device_owner=network:router_interface) that becomes up / changes router to a local one (attach), or goes down / is deleted / moves to another router while its router is local (detach). Action: expose_subnet / withdraw_subnet with neutron:cidrs, neutron:device_id, network name and address scopes.
  • **_expose_subnet** (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:861):
    1. needs a local gateway for the router;
    2. with require_snat_disabled_for_tenant_networks, drops CIDRs covered by an snat NAT entry of the router (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:873-895);
    3. _expose_router_lsp (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:981): subnet method → network prefixes; vrf method → host addresses; GUA filter; address-scope filter (all filtered out → ExposeDeniedForAddressScope); wire each prefix (WireFailure on failure), record it in _exposed_ips[<provider switch>] with via = <gateway IPs> and in ovn_local_lrps[<network>];
    4. with the host method, expose every up VM/virtual LSP of that network (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:909-934). ExposeDeniedForAddressScope and WireFailure are caught and logged at DEBUG (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:897-902).
  • **LogicalSwitchPortTenantCreateEvent / …DeleteEvent** (only with advertisement_method_tenant_networks = host): tenant LSP up/down on a network in ovn_local_lrps → _expose_remote_ip / _withdraw_remote_ip: underlay → /32 or /128 on bgp_nic; vrf → route in the EVPN VRF via the gateway (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:783-851, ovn_bgp_agent/drivers/openstack/utils/bgp.py:29).

3.6 Load balancers and port forwarding

  • **OVNLBCreateEvent** (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:712): LB UPDATE with vips, linked (lr_ref) to a local router, where vips grew (old keys ⊂ new keys) or the router reference appeared. For each new IP (vips keys without the port): equal to neutron:vip → expose_ovn_lb_vip; equal to neutron:vip_fip → expose_ovn_lb_fip. A VIP on a tenant network is announced as a tenant IP; on a provider network it is wired and announced (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:1138-1159).
  • **OVNLBDeleteEvent** (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:769): LB DELETE linked to a local router, or UPDATE where the router reference changed or vips shrank → withdraw the corresponding VIP/FIP.
  • **OVNPFCreateEvent / OVNPFDeleteEvent** (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:841-879): LB named pf-floatingip-* with vips, whose neutron:router_name is local → expose/withdraw the FIP (first vips key) on the gateway's provider network.

3.7 LocalnetCreateDeleteEvent (NB) and LogicalSwitchUpdateEvent

  • Non-vrf methods: LSP CREATE/DELETE with type=localnet → full sync (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:385-398).
  • vrf method: Logical_Switch UPDATE where neutron_bgpvpn:type::neutron_bgpvpn:vni changed, or DELETE of a switch that had them → full sync (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:348-382, ovn_bgp_agent/drivers/openstack/utils/driver_utils.py:138).

4. ovn_evpn_driver (SB, EVPN type-5 per router)

Declared "not supported" in the project's support matrix (doc/source/bgp_supportability_matrix.rst). Depends on neutron_bgpvpn:vni and neutron_bgpvpn:as annotations on SB Port_Binding.external_ids (written by networking-bgpvpn or by hand).

  • Start (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:69): OVS and SB IDLs on Port_Binding, Chassis, Datapath_Binding, Chassis_Private; no FRR base configuration; frr_sync is a no-op (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:100).
  • **sync** (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:107): ARP/NDP addresses on each mapped bridge; expose every chassisredirect port bound here; then remove extra IPs on lo-<vni>, extra routes in the VNI tables, extra cookie-998 flows and extra EVPN devices (vrf-*, lo-*, br-*, vxlan-*, veth-vrf-*, vlan-*) (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:705-828). Beware: the br- prefix also matches ordinary OVS bridges; only br-int and the mapped bridges are spared (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:727-731).
  • Gateway bound here (EVPN PortBindingChassisCreatedEvent, cr-lrp UPDATE with chassis going from empty to this chassis, ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py:28-50) → _expose_ip (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:187):
    1. provider datapath found through the FIP nat_addresses that mention the cr-lrp; no FIP/NAT → nothing is exposed;
    2. VNI and AS read from the patch port's annotations; missing → DEBUG log, nothing exposed;
    3. create vrf-<vni> (routing table = VNI), br-<vni> (in the VRF), vxlan-<vni> (local IP = evpn_local_ip, else first /32 of lo, else first IP of evpn_nic; UDP port evpn_udp_dstport), lo-<vni> dummy, and either a vlan-<vni> OVS internal port (VLAN provider) or a veth-vrf-<vni>/veth-ovs-<vni> pair (flat) (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:562-642);
    4. FRR add-vrf configuration (§ 6.1);
    5. route the gateway IPs in the VNI table out of the VLAN/veth device, NDP proxy for IPv6, unreachable default routes in the VRF (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:656-686); permanent neighbour entries for the router MAC;
    6. expose the router's attached subnets (_ensure_network_exposed).
  • Subnet annotated (EVPN SubnetRouterAttachedEvent, patch port CREATE, or UPDATE where neutron_bgpvpn:vni/as appear) → expose_subnet (no NAT) or expose_ip (with NAT) (ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py:81-113). _expose_subnet adds route <cidr> via <gateway ip> in the VNI table, a cookie-998 flow that sends traffic from the router MAC and that source network to the VRF device, and puts the bound VM IPs on lo-<vni> (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:420-492). A VNI/AS mismatch between subnet and gateway is logged at ERROR and ignored (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:404-410).
  • Tenant VM (EVPN TenantPortCreatedEvent/…DeletedEvent): IP added/removed on lo-<vni>, with the kernel's automatic local route deleted from the VNI table (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:338-372, ovn_bgp_agent/utils/linux_net.py:663-681).
  • Gateway leaves (…DeletedEvent) → withdraw_ip deletes the devices, routes, neighbours and flows and sends FRR del-vrf (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:269-335).

5. ovn_stretched_l2_bgp_driver (SB)

Purpose: when the provider network is stretched at L2 between racks, every node announces the tenant subnets with the router gateway IP as next hop, instead of host routes.

  • Start (ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:70): FRR redistribution set to kernel only (ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:76), base configuration, SB IDL on 4 tables, 5 events (ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:103-110). No reconnection event.
  • **sync** (ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:120): reads the routes of table bgp_vrf_table_id; for every cr-lrp of the SB (not only local ones, ovn_bgp_agent/drivers/openstack/utils/ovn.py:546) adds route <gateway network> dev <bgp_nic> and route <subnet> via <gateway ip> dev <bgp_nic> in that table for each attached subnet of the same IP version (address scopes must match both sides when filtering is on); deletes the other routes of the table.
  • Events: PortBindingChassisCreatedEvent/…DeletedEvent act only on cr-lrps (ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:294-328) and only fire for cr-lrps bound here, while sync installs routes for gateways on all chassis: between two syncs, a gateway that appears on another node is not routed [inferred]. SubnetRouterAttachedEvent/SubnetRouterUpdateEvent/SubnetRouterDetachedEvent add/update/remove the subnet routes (ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:210-291).
  • expose_remote_ip / withdraw_remote_ip raise NotImplementedError (ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:534-539); no event calls them.

6. Side effects shared by the drivers

6.1 FRR base configuration (frr_sync, start)

ensure_base_bgp_configuration (ovn_bgp_agent/drivers/openstack/utils/bgp.py:70), only for exposing_method in underlay, dynamic, ovn:

  1. ensure VRF device bgp_vrf (default bgp-vrf) with table bgp_vrf_table_id (default 10), up;
  2. with advertisement_method_tenant_networks = subnet, redistribute connected and kernel (otherwise only connected; the stretched driver forces kernel);
  3. render LEAK_VRF_TEMPLATE and apply it with vtysh --vty_socket /run/frr/ -f <tmpfile> (ovn_bgp_agent/drivers/openstack/utils/frr.py:89-113, ovn_bgp_agent/drivers/openstack/utils/frr.py:180-192); if bgp_router_id is not set, it is read from show ip bgp summary json → ipv4Unicast.routerId; if that is empty, ERROR Unknown router-id, needed for route leaking and no configuration;
  4. ensure dummy device bgp_nic (default bgp-nic), up, enslaved to the VRF (ovn_bgp_agent/utils/linux_net.py:130).
  5. Cost: one vtysh -f (and possibly one vtysh -c) every 15 s, forever, even if nothing changed.

6.2 Exposing = adding an address

In underlay/ovn/dynamic modes, BGP announcement is a side effect of Zebra: adding /32 or /128 addresses to the dummy bgp_nic inside the VRF makes them "connected" routes in the VRF, redistributed into BGP and leaked into the default BGP instance (import vrf) (ovn_bgp_agent/drivers/openstack/utils/bgp.py:29-67). Withdrawing = deleting the address. In vrf mode, a route is added in the EVPN VRF instead (ovn_bgp_agent/drivers/openstack/utils/evpn.py:357).

6.3 Idempotency of kernel and OVS writes

  • Netlink: routes use replace; EEXIST/ENOENT/ESRCH on routes and rules, EEXIST on addresses (raised as IpAddressAlreadyExists, then ignored by callers) and EADDRNOTAVAIL on deletes are tolerated (ovn_bgp_agent/privileged/linux_net.py:363-390).
  • Devices: ensure_* set the link up and create it only on NetworkInterfaceNotFound (ovn_bgp_agent/privileged/linux_net.py:59-119).
  • OVS: --may-exist add-port, --if-exists del-port; flows are compared with dump-flows before add-flow (ovn_bgp_agent/drivers/openstack/utils/ovs.py:94-147).
  • Netlink dumps interrupted by concurrent changes are retried with exponential backoff for up to 8 s (ovn_bgp_agent/utils/linux_net.py:49-53).

6.4 EVPN type-5 wiring for the NB driver (exposing_method = vrf)

_ensure_base_wiring_config_evpn (ovn_bgp_agent/drivers/openstack/utils/wire.py:87): for every bridge mapping, ovs-vsctl add-br through the IDL; for every provider Logical_Switch with neutron_bgpvpn:type = l3 and neutron_bgpvpn:vni, build an EvpnBridge (br-<vni>, vxlan-<vni>, vrf-<vni> with table = VNI, FRR add-vrf with RT/RD lists parsed with ast.literal_eval from neutron_bgpvpn:rt/rd/it/et) and a VlanDev per localnet port (veth vrf<uuid[:11]>/ovs<uuid[:11]>, anycast MAC derived from VNI and VLAN, 169.254.x.x addresses, forwarding sysctls, cookie-999 MAC-tweak flow, gateway IPs from DHCP_Options.router, IPv6 RA through FRR ipv6 nd) (ovn_bgp_agent/drivers/openstack/utils/evpn.py:39-355). evpn_local_ip is mandatory here (ConfOptionRequired, ovn_bgp_agent/drivers/openstack/utils/evpn.py:43-46). Devices are torn down when their last route goes (ovn_bgp_agent/drivers/openstack/utils/evpn.py:286-310, ovn_bgp_agent/drivers/openstack/utils/evpn.py:100-131).

6.5 Node-local OVN cluster (exposing_method = ovn, NB driver only)

_ensure_base_wiring_config_ovn (ovn_bgp_agent/drivers/openstack/utils/wire.py:186), at each sync, against the local NB:

  1. egress flows on each provider bridge: IP/IPv6 from each patch-provnet-* port → set destination MAC 40:44:00:00:00:06 (ovn_bgp_agent/drivers/openstack/utils/wire.py:505-523);
  2. router bgp-router; policy priority 10 inport=="bgp-router-openstack" → reroute to peer_ips (ECMP when several) (ovn_bgp_agent/drivers/openstack/utils/wire.py:428-439);
  3. one BFD session per peer IP on port bgp-router-<network> and an ECMP default route per peer with that BFD (ovn_bgp_agent/drivers/openstack/utils/wire.py:281-293, ovn_bgp_agent/drivers/openstack/utils/wire.py:442-449);
  4. for each ovn-bridge-mappings-<bgp_chassis_id> mapping: switch + localnet port; internal side (bgp-router-openstack LRP with provider_networks_pool_prefixes, ARP proxy on the switch port) or external side (LRP with the NIC IP/MAC, ingress flow from the external NIC to the patch port for each provider prefix, static FDB entry); gateway chassis bgp_chassis_id priority 1 (ovn_bgp_agent/drivers/openstack/utils/wire.py:317-414, ovn_bgp_agent/drivers/openstack/utils/wire.py:452-502). Per exposed IP: a Static_MAC_Binding on bgp-router-openstack (ovn_bgp_agent/drivers/openstack/utils/wire.py:731-740). Tenant subnets are not wired in this mode (ovn_bgp_agent/drivers/openstack/utils/wire.py:820-828 returns None), so _expose_router_lsp raises WireFailure and tenant networks are never exposed — consistent with "Expose Tenants: Yes (Not implemented)" in the support matrix.

Source: reverse-spec-ovn-bgp-agent/05-async-processing_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.