Drivers, exposure methods and configuration — 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
ovn-bgp-agent exposes no REST API. Its "interface" is: the configuration file, the choice of a driver (which OVN database it watches and how it reacts), the choice of an exposing method (how a route reaches BGP and how traffic is steered into OVN), and the set of OVN objects it makes reachable from the BGP fabric. This file replaces the endpoint catalogue of a service with these three things; the triggers are detailed in 05-async-processing.
1. What the agent is for
In an OVN-based OpenStack where the provider networks are not stretched at L2 across racks, the leaf switches must learn where each public IP lives. The agent runs on every node that hosts VMs or router gateways (compute and network nodes). For every IP that is reachable through this node — a VM on a provider network bound here, a floating IP whose VM or router gateway is here, a router gateway hosted here and, optionally, the tenant networks behind it — it:
- wires the kernel/OVS so that traffic for that IP arriving from the fabric is handed to OVN (ingress), and traffic leaving OVN is routed by the kernel (egress);
- announces the IP through the local FRR, which already peers with the fabric (the agent never configures BGP neighbours). When the IP moves away or disappears, it does the reverse, announcement first.
2. Drivers
[DEFAULT] driver, loaded through stevedore namespace ovn_bgp_agent.drivers (ovn_bgp_agent/drivers/driver_api.py:25-32, setup.cfg).
| Driver | OVN DB watched | Supported exposing methods (code) | Declared status (doc/source/bgp_supportability_matrix.rst) | Class |
|---|---|---|---|---|
ovn_bgp_driver (default) | Southbound | underlay only in practice (wiring goes through wire_utils, which only has underlay/vrf/ovn branches; the SB driver builds no EVPN devices) [inferred] | Underlay: supported | ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:46 |
nb_ovn_bgp_driver | Northbound (+ node-local NB for ovn) | underlay (broken on this branch by defect D16: provider IPs are not wired, sync fails, see 02 § 4), vrf (EVPN type-5), ovn; l2vni and dynamic raise UnsupportedWiringConfig at every sync (ovn_bgp_agent/drivers/openstack/utils/wire.py:34-44) | Underlay, VRF, OVN supported; L2VNI, Dynamic not supported | ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:115 |
ovn_evpn_driver | Southbound | its own EVPN plumbing per router (VNI from Neutron annotations) | VRF: not supported | ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:46 |
ovn_stretched_l2_bgp_driver | Southbound | subnet routes via gateway in the BGP VRF table | Underlay: supported | ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:49 |
All drivers implement the abstract AgentDriverBase (expose_ip, withdraw_ip, expose_remote_ip, withdraw_remote_ip, expose_subnet, withdraw_subnet, ovn_bgp_agent/drivers/driver_api.py:19-56) plus start, sync and frr_sync, which BGPAgent calls (ovn_bgp_agent/agent.py:38-64). The signatures differ between drivers (for example expose_ip(ips, row, associated_port) in SB, expose_ip(ips, ips_info) in NB, expose_ip(row, cr_lrp) in EVPN): the base class is a naming convention, not a common contract.
3. Exposing methods ([DEFAULT] exposing_method)
| Method | How an IP is announced | Ingress (fabric → OVN) | Egress (OVN → fabric) | Code |
|---|---|---|---|---|
underlay (default) | /32 or /128 added to dummy bgp_nic in VRF bgp_vrf; FRR redistributes connected in that VRF and leaks it into the default BGP instance | ip rule to <ip> lookup <bridge table> + route <ip> dev <bridge>[.vlan] in the per-bridge table; proxy ARP/NDP on the bridge; static neighbour for router gateways | OVS flow cookie 999 rewriting the destination MAC to the bridge MAC → kernel routing via the per-bridge default route | ovn_bgp_agent/drivers/openstack/utils/bgp.py:29-67, ovn_bgp_agent/drivers/openstack/utils/wire.py:670-707 |
vrf (NB only) | route in the EVPN VRF vrf-<vni> (table = VNI) out of a veth into the provider bridge; FRR advertises type-5 routes | VXLAN vxlan-<vni> in bridge br-<vni> in the VRF; veth to OVS | MAC-tweak flow towards the veth's anycast MAC | ovn_bgp_agent/drivers/openstack/utils/wire.py:87-183, ovn_bgp_agent/drivers/openstack/utils/evpn.py:39 |
ovn (NB only) | as underlay (bgp_nic) | node-local OVN router bgp-router with ARP proxy for the provider prefixes and ingress flows from the physical NIC | node-local OVN policy rerouting to peer_ips (ECMP + BFD) | ovn_bgp_agent/drivers/openstack/utils/wire.py:186-278 |
l2vni, dynamic | — | — | — | accepted by ovn_bgp_agent/config.py:130-145, rejected at sync by ovn_bgp_agent/drivers/openstack/utils/wire.py:44 |
4. What is exposed (functional catalogue)
Each row is the "operation" a reader would look for in a service's API catalogue. "Exposed by" means: which node announces it.
| Object exposed | Condition | Exposed by | SB driver | NB driver | EVPN driver | Stretched driver |
|---|---|---|---|---|---|---|
| VM IP on a provider network | port bound and up on the node | node hosting the VM | yes | yes | no | no |
| Virtual-port IP (VRRP/Octavia amphora VIP) on a provider network | virtual port bound here; NB: neutron:host_id = this host | node hosting the port | yes (+ IPv6 NDP proxy for its CIDR) | yes | no | no |
| Floating IP of a VM | SB: patch port nat_addresses + cr-lrp here, or VM here with FIP; NB distributed: VM here with neutron:port_fip; NB centralized: NAT gateway_port hosted here | SB/NB-centralized: node hosting the router gateway; NB-distributed: node hosting the VM | yes (IPv4 only) | yes | no | no |
| Router gateway IP (cr-lrp) | gateway port hosted on the node | gateway node | yes | yes | yes (into the router's VRF) | routes on every node |
| Tenant subnet (CIDR) | expose_tenant_networks (or GUA mode for IPv6 GUA subnets), router gateway here, address scope allowed | gateway node | yes (rule + route) | yes (subnet method: prefix announced; host method: wiring only) | yes (VRF route + flow) | yes (route via gateway IP on every node) |
| Tenant VM IP | same + VM bound anywhere | gateway node | yes | yes with advertisement_method_tenant_networks = host | yes (IP on lo-<vni>) | no |
| OVN LB VIP on a provider network | LB members behind a router whose gateway is here | gateway node | yes | yes | no | no |
| OVN LB VIP on a tenant network | tenant exposure on | gateway node | yes | yes | no | no |
OVN LB VIP FIP (neutron:vip_fip) | router gateway here | gateway node | no | yes | no | no |
Port-forwarding FIP (pf-floatingip-* LB) | router gateway here | gateway node | no | yes | no | no |
4.1 Provider VM IP (reference flow, NB driver)
- Purpose: make a VM plugged directly on a provider network reachable without stretching the VLAN across racks.
- Actor: Neutron/Nova (port creation and binding), ovn-controller (sets
up). - Preconditions: the switch has a
localnetport whosenetwork_nameis in this node'sovn-bridge-mappings(ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:471-493); the port has an IP inaddresses. - Business rules: the host is
requested-chassis(orneutron:host_idfor virtual ports), comparing with the OVShostname; during live migration both hosts are listed and both may expose (ovn_bgp_agent/drivers/openstack/utils/driver_utils.py:43-68); IPs already exposed are skipped (ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py:84). - Flow:
- (event) LSP update with
up = trueon this host →LogicalSwitchPortProviderCreateEvent. - Wire: ip rule + route in the bridge table (VLAN device if the localnet has a tag), MAC-tweak flow for the localnet patch port if not yet present (
ovn_bgp_agent/drivers/openstack/utils/wire.py:660-707). - Announce: address on
bgp_nic→ FRR advertises<ip>/32. - Record
_exposed_ips[<switch>][<ip>].
- (event) LSP update with
- Outcome: the fabric routes
<ip>/32to this node; the kernel hands it tobr-ex[.vlan]; OVN delivers it. - Withdraw: port deleted, down without additional binding, or bound elsewhere → address removed first, then rule/route; the IPv6 NDP proxy for the CIDR only when the switch has no virtual port and no active local port remains (
ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:599-609). - Limitations: one localnet per switch assumed (
ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:670); a switch first seen as tenant is cached as tenant until the next sync (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:482-484).
4.2 Router gateway and tenant networks (NB driver)
- Purpose: let the fabric reach tenant subnets (no SNAT) or at least the gateway IP, through the node that hosts the router's gateway.
- Preconditions: OVN ≥ 23.09 (
Logical_Router_Port.statuswithhosting-chassis; without it, a WARNING and no gateway is exposed,ovn_bgp_agent/drivers/openstack/utils/ovn.py:369-374);expose_tenant_networks(orexpose_ipv6_gua_tenant_networksfor IPv6 GUA only); withrequire_snat_disabled_for_tenant_networks, subnets that are SNATed by the router are skipped (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:873-895); withaddress_scopes, the subnet's scope must be listed. - Flow: gateway moves here (
ChassisRedirectCreateEvent) → gateway IPs wired with a permanent neighbour (router MAC) and announced → each router-interface LSP of that router: rule + route<cidr> via <gateway ip>in the bridge table →subnetmethod: route<cidr> via <gateway ip> dev bgp-nicin tablebgp_vrf_table_id(FRR must redistributekernel, whichensure_base_bgp_configurationturns on,ovn_bgp_agent/drivers/openstack/utils/bgp.py:80-81);hostmethod: each up VM IP of the subnet is added tobgp_nic→ LBs of the router. - Outcome:
<cidr>(or each VM/32) announced by the gateway node only.
4.3 Floating IPs: distributed vs centralized (NB driver)
NB_Global external_ids:neutron:fip-distributed decides who announces a FIP: the node hosting the VM (distributed, the agent's default when the key is absent) or the node hosting the router gateway (centralized, Neutron's default). The two event sets are swapped at run time when the flag changes (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:127-187). A WARNING at start says that the agent's default differs from Neutron's (ovn_bgp_agent/drivers/openstack/utils/ovn.py:446-454). With distributed FIPs, the FIP is wired with the NAT external_mac; centralized, with the gateway port's MAC (ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:89-105).
4.4 EVPN per router (SB ovn_evpn_driver)
- Purpose: put each router's networks in an L3 EVPN (VNI) instead of the underlay.
- Preconditions: the router's patch port carries
neutron_bgpvpn:vniandneutron_bgpvpn:as; the router has at least one FIP (nat_addresses), because the provider datapath is found through it (ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:199-202) — a router without FIPs is never exposed [inferred from the code path]. - Flow and outcome: see 05 § 4.
4.5 Stretched L2 (ovn_stretched_l2_bgp_driver)
Every node installs, in table bgp_vrf_table_id, a route to the gateway's network and one route per attached subnet via the gateway IP, for every router of the SB; FRR redistributes kernel only. Tenant VM IPs and FIPs are not handled (ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:534-539).
5. Configuration reference
Registered by ovn_bgp_agent/config.py:236-240; sample generator etc/oslo-config-generator/bgp-agent.conf (namespaces oslo.concurrency, oslo.log, oslo.privsep, ovnbgpagent). The command line is parsed with project='bgp-agent' (ovn_bgp_agent/config.py:243-244), so default config files are searched as bgp-agent.conf.
[DEFAULT]
| Option | Type | Default | Effect | Defined at |
|---|---|---|---|---|
reconcile_interval | int | 300 | period of the full sync | ovn_bgp_agent/config.py:26 |
frr_reconcile_interval | int | 15 | period of frr_sync | ovn_bgp_agent/config.py:29 |
expose_tenant_networks | bool | false | expose tenant subnets/VM IPs through the gateway node; takes precedence over GUA mode | ovn_bgp_agent/config.py:33 |
advertisement_method_tenant_networks | host|subnet | host | NB: announce tenant VMs as /32 or the subnet prefix | ovn_bgp_agent/config.py:39 |
require_snat_disabled_for_tenant_networks | bool | false | NB: do not expose subnets SNATed by the router | ovn_bgp_agent/config.py:48 |
expose_ipv6_gua_tenant_networks | bool | false | expose only global-unicast IPv6 tenant addresses | ovn_bgp_agent/config.py:57 |
driver | choice | ovn_bgp_driver | see § 2 | ovn_bgp_agent/config.py:64 |
ovsdb_connection | str (tcp:/ssl:/unix:) | unix:/usr/local/var/run/openvswitch/db.sock | local OVS DB | ovn_bgp_agent/config.py:69 |
ovsdb_connection_timeout | int | 180 | OVN IDL transaction timeout (not used for the OVS IDL) | ovn_bgp_agent/config.py:75 |
bgp_AS | str | 64999 | AS of router bgp used in FRR fragments | ovn_bgp_agent/config.py:78 |
bgp_router_id | str | none | router-id of the VRF instance; read from FRR if empty | ovn_bgp_agent/config.py:82 |
evpn_local_ip | IP | none | VXLAN source; mandatory for NB vrf | ovn_bgp_agent/config.py:86 |
evpn_nic | str | none | SB EVPN: take the VXLAN source from this NIC | ovn_bgp_agent/config.py:92 |
evpn_udp_dstport | port | 4789 | VXLAN UDP port | ovn_bgp_agent/config.py:98 |
clear_vrf_routes_on_startup | bool | false | flush table bgp_vrf_table_id at start | ovn_bgp_agent/config.py:102 |
delete_vrf_on_disconnect | bool | true | NB vrf: delete the VRF and its FRR config when unused | ovn_bgp_agent/config.py:106 |
bgp_nic | str | bgp-nic | dummy device holding announced IPs | ovn_bgp_agent/config.py:112 |
bgp_vrf | str | bgp-vrf | VRF of bgp_nic | ovn_bgp_agent/config.py:116 |
bgp_vrf_table_id | int | 10 | table of bgp_vrf | ovn_bgp_agent/config.py:120 |
address_scopes | list | none | allowed Neutron address scopes | ovn_bgp_agent/config.py:125 |
exposing_method | underlay|l2vni|vrf|dynamic|ovn | underlay | see § 3 | ovn_bgp_agent/config.py:130 |
[agent]
| Option | Type | Default | Effect | Defined at |
|---|---|---|---|---|
root_helper | str | sudo | prefix of privsep-helper | ovn_bgp_agent/config.py:149 |
[ovn] (all also accepted in [DEFAULT], deprecated)
| Option | Default | Defined at |
|---|---|---|
ovn_sb_private_key, ovn_nb_private_key | /etc/pki/tls/private/ovn_bgp_agent.key | ovn_bgp_agent/config.py:157, ovn_bgp_agent/config.py:178 |
ovn_sb_certificate, ovn_nb_certificate | /etc/pki/tls/certs/ovn_bgp_agent.crt | ovn_bgp_agent/config.py:162, ovn_bgp_agent/config.py:183 |
ovn_sb_ca_cert, ovn_nb_ca_cert | /etc/ipa/ca.crt | ovn_bgp_agent/config.py:167, ovn_bgp_agent/config.py:188 |
ovn_sb_connection, ovn_nb_connection | none (taken from OVS external_ids) | ovn_bgp_agent/config.py:172, ovn_bgp_agent/config.py:193 |
[local_ovn_cluster] (exposing_method = ovn)
| Option | Default | Effect | Defined at |
|---|---|---|---|
ovn_nb_connection | unix:/var/run/ovn/ovnnb_db.sock | node-local NB | ovn_bgp_agent/config.py:202 |
external_nics | [] | NICs used for ingress flows | ovn_bgp_agent/config.py:208 |
peer_ips | [] | next hops (ECMP) | ovn_bgp_agent/config.py:212 |
provider_networks_pool_prefixes | ['192.168.0.0/16'] | prefixes routed into OpenStack | ovn_bgp_agent/config.py:216 |
bgp_chassis_id | bgp | chassis of the local ovn-controller; suffix of ovn-bridge-mappings-<id> | ovn_bgp_agent/config.py:219 |
Also: oslo.log options, [privsep], [privsep_ovs_vsctl], [privsep_vtysh] (oslo.privsep), [oslo_concurrency]. The full sample is _rewrite/config_stable-2026.2.conf (07).
6. Lifecycle of an exposed IP
stateDiagram-v2 [*] --> Unknown Unknown --> Wired: event match (bound here + up) or sync Wired --> Announced: address added to bgp_nic (FRR advertises) Wired --> Unknown: wiring failed (exception logged), retried by next sync Announced --> Withdrawn: event (deleted / down / moved) — address removed first Withdrawn --> Unknown: rule/route/neighbour removed Announced --> Announced: sync (idempotent re-apply) Announced --> Unknown: sync finds it no longer expected (cleanup of bgp_nic, rules, routes)
Transitions triggered by events are described per object in 05; the sync transitions in § 2.2 and § 3.2 there.
Source: reverse-spec-ovn-bgp-agent/01-drivers-and-exposure_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.