Rewrite pack — 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:= ovsdbappstable/2026.2@0fb5a1b9a675.
ovn-bgp-agent has no API, schema or policy, so the usual contracts (OpenAPI, DDL, policies) do not exist. What a re-implementation (for example a Go agent for OpenKeel) must reproduce is:
- what it reads: OVN rows and the Neutron conventions in them (03 § 3–5);
- when it reacts: the event decision tables (§ 3.1,
cases-event-match); - what it writes: kernel, OVS and FRR state, in a precise order (§ 3.2–3.4,
side-effects); - how it converges: periodic full reconciliation with cleanup (§ 3.5).
1. Contents of _rewrite/
| File | Content | Use in a rewrite |
|---|---|---|
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf) | oslo-config-generator sample (etc/oslo-config-generator/bgp-agent.conf: oslo.concurrency, oslo.log, oslo.privsep, ovnbgpagent) | option names, groups, defaults, deprecated [DEFAULT] aliases of [ovn] options; keep them to stay a drop-in for deployment tools. [privsep_ovs_vsctl] and [privsep_vtysh] are not in the sample (the oslo.privsep namespace only lists [privsep]) |
[cases-event-match_stable-2026.2.json](_rewrite/cases-event-match_stable-2026.2.json) | 245 recorded calls of match_fn of 36 event classes (from the 325 watcher unit tests): event type, row, old row, agent state, result | decision-table tests for the rewrite's event filters (§ 3.1) |
[side-effects_stable-2026.2.json](_rewrite/side-effects_stable-2026.2.json) | 11 driver scenarios run against a fake node: ordered privileged requests, OVS commands, FRR texts, final node state | golden data for the node-side effects (§ 3.2–3.4) |
[replay-report_stable-2026.2.json](_rewrite/replay-report_stable-2026.2.json) | replay of the 245 event cases against the original code | proof that the vectors are complete: 245/245 pass (3 runs) |
Extraction environment: Python 3.12, upper-constraints stable/2026.2: ovsdbapp 2.19.0, ovs 3.7.1, pyroute2 0.8.1, neutron-lib 5.0.1, oslo.privsep 3.12.0, oslo.service 4.8.0, Jinja2 3.1.6. The project's unit suite in that venv: 962 tests, 960 pass, 2 errors that need Linux netlink (test_ovn_evpn_driver.TestOVNEVPNDriver.test_withdraw_ip*, run on macOS).
Commands (venv outside the repository; adapter scripts/adapters/ovn_bgp_agent.py of the generate-reverse-spec skill):
rewrite_pack.py ovn-bgp-agent --branch stable/2026.2 --project ovn-bgp-agent \
--config-generator etc/oslo-config-generator/bgp-agent.conf --out <tmp>/rp
# one capture per event class defining match_fn (41 classes)
conformance_capture.py calls ovn-bgp-agent --branch stable/2026.2 --adapter adapters/ovn_bgp_agent.py \
--test-module ovn_bgp_agent.tests.unit.drivers.openstack.watchers.test_<module> \
--record ovn_bgp_agent.drivers.openstack.watchers.<module>:<Class>.match_fn --name event-match --out <tmp>/ev/<Class>
conformance_capture.py replay ovn-bgp-agent --branch stable/2026.2 --adapter adapters/ovn_bgp_agent.py \
--cases cases-event-match_stable-2026.2.json --annotate --workdir <tmp> --out <tmp>
conformance_capture.py scenarios ovn-bgp-agent --branch stable/2026.2 --adapter adapters/ovn_bgp_agent.py \
--workdir <tmp> --out <tmp> # writes side-effects_<slug>.json (the exchanges/response-schemas files are empty: no HTTP)
2. Runtime behaviour that a rewrite must keep
2.1 Process model
- One long-running process per node, started as
ovn-bgp-agent --config-file …; configuration project namebgp-agent(ovn_bgp_agent/config.py:243-244). - One driver per process, chosen by
[DEFAULT] driver(ovn_bgp_agent/agent.py:35-36). A rewrite can implement only the drivers OpenKeel needs; the NB driver is the one the project develops (§ 8). - Three concurrent activities: the event stream of the OVN IDL (one dispatcher thread, events in order,
ovsdbapp:ovsdbapp/event.py:186-208), thesynctimer and thefrr_synctimer (ovn_bgp_agent/agent.py:44-47). All node writes are serialised by one in-process lock per driver (ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:128,ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:286). A rewrite should keep a single writer (a Go mutex or a single reconcile goroutine fed by a work queue). - Filtering (
match_fn) runs on the IDL thread against driver caches without that lock (05 § 1.2); the rewrite may tighten this (take a read lock or evaluate in the worker), since nothing depends on the race.
2.2 Ordering rules (observable from the fabric)
| # | Rule | Evidence |
|---|---|---|
| O1 | Expose: wire first (rule, route, neighbour, flow), then add the address on bgp_nic (BGP UPDATE) | ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:341-346; scenario sb-sync-provider-vm |
| O2 | Withdraw: remove the address on bgp_nic (BGP WITHDRAW) first, then unwire | ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:408-409; scenarios sb-withdraw-provider-vm, nb-provider-vm |
| O3 | Gateway withdraw: gateway IPs, then subnets, then LBs, then forget the gateway | ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:936-979, ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:608-633 |
| O4 | Sync: add everything expected, then delete leftovers (addresses, rules, routes, VLAN devices, flows) | ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:236-249, ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:338-344 |
| O5 | A leftover is anything in bgp_nic /32–/128 addresses, in rules pointing to the per-bridge tables, in non-default routes of those tables, or in cookie-999 flows, that the sync did not (re)declare. Routes with proto 186 (FRR) are never touched | ovn_bgp_agent/utils/linux_net.py:618-623 |
2.3 Identity and matching rules
- Chassis identity:
system-idfor SB rows and LRPstatus:hosting-chassis;hostname(fallback system hostname) for LSPrequested-chassis/neutron:host_id(NB) (ovn_bgp_agent/drivers/openstack/utils/ovs.py:290-305). - Port chassis (NB):
requested-chassis(comma-separated, our host wins if listed, else the first) except for virtual ports, thenneutron:host_id(ovn_bgp_agent/drivers/openstack/utils/driver_utils.py:43-68). - Provider network: SB = datapath with a
localnetport; NB = switch whose localnet port'snetwork_nameis in this node'sovn-bridge-mappings. - Address parsing: SB
mac[0]/ NBaddresses[0]="<mac> <ip> <ip>…";["unknown"]→ useneutron:cidrs(ovn_bgp_agent/drivers/openstack/utils/port.py:20-59).
3. Core behaviour: formal specification
3.1 Event decision tables
Each watcher class is a predicate match(event_type, row, old, agent_state) → bool followed by an action. The tables are specified in prose in 05 and as data in cases-event-match_stable-2026.2.json.
- Case format (
CASES_NOTEin the file):event_class,table,events,event(type or any),row/oldas{"__row__": {column: value}}(references to other rows are nested__row__objects; the same row referenced twice is the same object: comparisons such asrow.chassis != old.chassisrely on it),agent(plain attributes such aschassis,ovn_local_cr_lrps,ovn_local_lrps,distributed; mocked queries with theirreturn_value, e.g.is_ip_exposed,is_ls_provider,nb_idl.lsp_get().execute()),matches, anderrorwhen the predicate raised. - Coverage: 245 cases (81 true, 164 false) over 36 classes: 74 SB BGP, 129 NB, 42 EVPN. No case exists for
FipOnCRLRPBaseEvent(ExposeFIPOnCRLRP/WithdrawFIPOnCRLRP),CrLrpChassisChangeExposeEvent/…WithdrawEventandOVNPFCreateEvent/OVNPFDeleteEvent: the project has no unit test for them (§ 8). Their predicates are short; specify them from 05 § 3.4 and § 3.6. - Replay: 245/245 pass (
replay-report_stable-2026.2.json). A first replay failed 2 cases until rows were memoised by UUID: the reference identity rule above is part of the contract. - How to use: rebuild
row,oldand an agent stub from each case, run the rewrite's filter, compare withmatches. A rewrite that pre-filters with OVSDBmonitor_condconditions must still return the same decision for the rows that reach it. - Known quirks to reproduce or consciously drop (documented in 02 § 4): D3 (
constants.constants), D12 (ovn_local_lrps != []always true). The vectors encode the current behaviour.
3.2 Exposure algorithm (underlay)
Pseudo-code of "expose IP set ips of a port", common to SB and NB (differences noted):
expose(ips, port):
if not provider(port.network) and not gateway_or_fip(port): return # 05 § 2.3 / § 3.3
bridge, vlan = bridge_mapping(network_name(localnet(port.network))), localnet.tag
table = rt_table(bridge) # /etc/iproute2/rt_tables, 1..252
for ip in ips:
ip rule add to ip/32|128 table <table> # ovn_bgp_agent/drivers/openstack/utils/wire.py:681-685
if lladdr: ip neigh replace ip lladdr <lladdr> dev bridge[.vlan] nud permanent # gateways; NB: also VM MACs
ip route replace ip/32|128 dev bridge[.vlan] table <table> proto boot scope link # ovn_bgp_agent/utils/linux_net.py:753-804
for cidr in proxy_cidrs if IPv6: ip -6 neigh add proxy <network address> dev bridge[.vlan]
ensure MAC-tweak flow for the localnet patch port (cookie 999, priority 900, ip+ipv6) # ovn_bgp_agent/drivers/openstack/utils/wire.py:660-667
ip addr add ip/32|128 dev bgp_nic # announce (O1)
Tenant subnet behind a local gateway (expose_tenant_networks):
ip rule add to <router-if ip>/<len> table <table> # rule keeps the interface IP (kernel normalises)
ip route replace <net>/<len> via <gateway ip of same family> dev bridge[.vlan] table <table>
if advertisement_method_tenant_networks == subnet:
ip route replace <net>/<len> via <gateway ip> dev bgp_nic table bgp_vrf_table_id # FRR 'redistribute kernel'
elif host method: for each VM IP up on the subnet: ip addr add <ip>/32|128 dev bgp_nic
Recorded instances: scenarios sb-sync-provider-vm, sb-gateway-and-tenant-subnet, sb-fip-on-gateway, nb-provider-vm, nb-gateway-subnet-advertisement in side-effects_stable-2026.2.json. Notable differences captured there: the NB driver installs a permanent neighbour for provider VM IPs too (it passes the VM MAC, ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:400-404), the SB driver only for gateways.
3.3 Derived values
| Value | Formula | Evidence |
|---|---|---|
| bridge ARP/NDP addresses | 169.254.<idx % 256>.<idx % 256> and fd53:d91e:400:7f17::<idx hex>:<idx hex> on the bridge (idx = 1-based position in ovn-bridge-mappings); for VLAN devices 169.254.<idx>.<vlan % 256> and …::<idx hex>:<vlan hex> | ovn_bgp_agent/utils/linux_net.py:139-170; scenario frr-and-evpn-renderings |
| VLAN device name | <bridge[:10]>.<vlan> (15-character limit) | ovn_bgp_agent/constants.py:97-98 |
| routing table number | random free number in 1–252 (absent from rt_tables and ≠ bgp_vrf_table_id), persisted in /etc/iproute2/rt_tables as <n> <bridge>; reused on restart | ovn_bgp_agent/utils/linux_net.py:232-272 |
| EVPN anycast MAC | 02:00:00:00:00:00 + int(hex(vni,6)+hex(vlan,4)) | ovn_bgp_agent/utils/linux_net.py:173-191, ovn_bgp_agent/drivers/openstack/utils/evpn.py:491-513 |
| EVPN VRF table | the VNI | ovn_bgp_agent/drivers/openstack/utils/evpn.py:92, ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py:584 |
| EVPN veth names (NB) | vrf<localnet uuid[:11]> / ovs<localnet uuid[:11]> | ovn_bgp_agent/drivers/openstack/utils/evpn.py:152-154 |
3.4 FRR texts
The four Jinja templates of ovn_bgp_agent/drivers/openstack/utils/frr.py:31-113 and their renderings are in side-effects_stable-2026.2.json (scenarios sb-sync-provider-vm, nb-gateway-subnet-advertisement, frr-and-evpn-renderings). The actual texts contain blank lines produced by the {% %} tags; vtysh -f ignores them, a rewrite need not reproduce them. The order of redistribute lines comes from a Python set and varies between runs: not significant. set_default_redistribute mutates a module-level set (ovn_bgp_agent/drivers/openstack/utils/frr.py:141-150): once kernel is added it stays until restart.
3.5 Reconciliation
sync is the source of truth; events are an optimisation. A rewrite can be a level-triggered reconciler (compute the desired set of (IP, wiring) from OVN, diff against the node, apply in the O1–O5 order) and use events only to schedule an earlier reconcile — this would also fix D11/D16 by construction. It must keep: the 300 s default period, the 15 s FRR re-apply (FRR restarts lose the VRF leak), and full resync after an OVN reconnection.
4. What the other components expect
| Expectation | Where it comes from |
|---|---|
Neutron writes the external_ids keys of 03 § 5 exactly; neutron-<uuid> switch and router names | ovn_bgp_agent/constants.py:33-125; neutron: prefix stripped at ovn_bgp_agent/drivers/openstack/utils/router.py:19-25 |
NB_Global external_ids:neutron:fip-distributed set by Neutron (enable_distributed_floating_ip); absent = distributed for the agent, centralized for Neutron | ovn_bgp_agent/drivers/openstack/utils/ovn.py:436-455 |
OVN ≥ 23.09 for NB gateways (Logical_Router_Port.status) and centralized FIPs (NAT.gateway_port) | ovn_bgp_agent/drivers/openstack/utils/ovn.py:369-374, ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:54-58 |
FRR already runs router bgp <bgp_AS> with its peers and accepts vtysh --vty_socket /run/frr/; the fabric accepts /32 and /128 (sample prefix-list only-host-prefixes) | etc/frr/frr.conf |
ovn-controller sets Port_Binding.up / LSP up and creates patch-<localnet>-to-br-int ports on the node when a port of that network is bound | used by the matches and by ovn_bgp_agent/drivers/openstack/utils/ovs.py:89-91 |
The fabric sees: one /32 or /128 per exposed IP from the node that hosts it (or its gateway), tenant prefixes from the gateway node with subnet method | scenarios in side-effects_stable-2026.2.json |
5. State strategy (no database)
All state is derived: OVN (read), the node (kernel/OVS/FRR) and in-memory caches rebuilt at every sync (ovn_local_cr_lrps, ovn_local_lrps, _exposed_ips, ovn_provider_ls…). Only one durable artefact: the rt_tables entries (table numbers survive restarts). A rewrite needs no storage; it must tolerate a node already configured by the Python agent (same device names, cookies 999/998, table names) to allow a rolling replacement.
6. Privileges, configuration, CLI
- Needs
CAP_NET_ADMIN,CAP_SYS_ADMIN,CAP_DAC_OVERRIDE,CAP_DAC_READ_SEARCH(03 § 1). In Kubernetes: a privileged DaemonSet withhostNetwork,/run/frr,/etc/iproute2, the OVS socket and/run/openvswitchmounted [inferred]. - Configuration: keep the option names of
config_stable-2026.2.conf; drop privsep sections if the rewrite does not use privsep. - Go equivalents [inferred]: OVSDB →
github.com/ovn-org/libovsdb(monitor_cond, cache, events); netlink →github.com/vishvananda/netlink; FRR →vtyshexec (same texts) or frr-k8s; OVS →ovs-vsctl/ovs-ofctlexec or OpenFlow library.
7. Conformance strategy
- Event filters: run the 245 vectors (§ 3.1). Target 245/245.
- Side effects: for each scenario of
side-effects_stable-2026.2.json, give the rewrite the same OVN answers and node starting state (bridgebr-exMACaa:aa:aa:aa:aa:01, patch port ofport 5, emptyrt_tablesexcept reserved tables), and compare the final_state (addresses, rules, routes, neighbours, sysctl, flows, rt_tables) — not the exact call sequence, which is implementation-specific — plus the O1–O5 ordering rules. Table numbers: the fake picks the smallest free; compare by bridge name. Scenarios nameddefect-*record current defects: decide per defect whether to reproduce it (normally not). - System test [not done here]: the project's functional tests need a real OVN (
ovn_bgp_agent/tests/functional/base.py); a Linux VM with ovsdb-server/ovn-northd and FRR would allow recording real netlink and BGP output for the same scenarios.
8. Gaps found while building the pack
- No unit test for centralized-FIP events (
FipOnCRLRPBaseEventsubclasses), gateway-move FIP events (CrLrpChassisChange*) and port-forwarding events (OVNPF*): no vectors. - D16 — NB driver, underlay, regression since 5.0.0:
ensure_base_wiring_configreplaces the driver's emptyovn_routing_tablesdict with a new one (routing_tables = routing_tables or {}, commitccf4c29), so the driver's dict is never filled: wiring of provider IPs raisesKeyError(swallowed: nothing announced) and everysyncfails incleanup_wiringwithKeyError: '<bridge>'(ovn_bgp_agent/drivers/openstack/utils/wire.py:34-38,ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:297-300). Confirmed by scenariodefect-d16-nb-underlay-routing-tables. The unit tests mockensure_base_wiring_config, so they do not see it. The NB scenarios that record the intended behaviour pass the dict by reference in the harness (pre-5.0.0 semantics) and say so. - D15 — EVPN RD: without
evpn_local_ip, theadd-vrffragment rendersrd None:<vni>(scenariofrr-and-evpn-renderings;ovn_bgp_agent/drivers/openstack/utils/frr.py:63-67,ovn_bgp_agent/drivers/openstack/utils/frr.py:210-214), although the SB EVPN driver can take the VXLAN source fromloorevpn_nic. - D1 (patch port not ready → IP not announced) and D2 (
set_device_statusTypeError) confirmed by scenarios; D14 (BFDtable not registered) not run (no OVSDB server here). exposing_method = ovnandvrfwere not run end to end (need a local NB and EVPN kernel features).- The stretched-L2 driver reads pyroute2 route objects through attributes (
route.dst,ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py:141-145) [not run].
9. Non-functional requirements
- Reaction time: one event → node change in milliseconds; announcement then depends on FRR/BGP timers.
- Cost: full scans of SB
Port_Bindingper sync and per FIP lookup;vtysh -fevery 15 s;ovs-ofctl dump-flowsper bridge per sync. A rewrite should index rows (by datapath, type, chassis, peer) and re-apply FRR only when it changed or FRR restarted. - Failure mode: never crash on a bad row; log and let the next sync repair. Exception: routing-table exhaustion exits.
10. Rewrite checklist
- [ ] Read OVS
external_idsidentity, remotes and bridge mappings; SB or NB monitors with the same tables and own-chassis condition. - [ ] Implement the decision tables of the chosen driver; pass
cases-event-match_stable-2026.2.json. - [ ] Underlay wiring: VRF + dummy NIC, per-bridge table in
rt_tables, default routes, ARP/NDP addresses, proxy ARP/NDP, MAC-tweak flows, rules/routes/neighbours per IP, NDP proxy for IPv6 CIDRs. - [ ] Announce/withdraw ordering O1–O3; reconciliation with leftover cleanup O4–O5.
- [ ] FRR VRF-leak fragment re-applied periodically; router-id from config or
show ip bgp summary json. - [ ] Tenant networks:
hostandsubnetmethods, SNAT filter, address scopes, GUA-only mode. - [ ] FIPs distributed and centralized, switch at run time on
neutron:fip-distributed. - [ ] LB VIPs, VIP-FIPs, port-forwarding FIPs.
- [ ] Do not reproduce D1, D2, D3, D11, D12, D15, D16 (decide explicitly).
- [ ] Final-state comparison against
side-effects_stable-2026.2.json.
Source: reverse-spec-ovn-bgp-agent/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.