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

Reverse-spec · Services

ovn-bgp-agent — Rewrite pack

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: = ovsdbapp stable/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/

FileContentUse 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, resultdecision-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 stategolden 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 codeproof 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 name bgp-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), the sync timer and the frr_sync timer (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)

#RuleEvidence
O1Expose: 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
O2Withdraw: remove the address on bgp_nic (BGP WITHDRAW) first, then unwireovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:408-409; scenarios sb-withdraw-provider-vm, nb-provider-vm
O3Gateway withdraw: gateway IPs, then subnets, then LBs, then forget the gatewayovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py:936-979, ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py:608-633
O4Sync: 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
O5A 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 touchedovn_bgp_agent/utils/linux_net.py:618-623

2.3 Identity and matching rules

  • Chassis identity: system-id for SB rows and LRP status:hosting-chassis; hostname (fallback system hostname) for LSP requested-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, then neutron:host_id (ovn_bgp_agent/drivers/openstack/utils/driver_utils.py:43-68).
  • Provider network: SB = datapath with a localnet port; NB = switch whose localnet port's network_name is in this node's ovn-bridge-mappings.
  • Address parsing: SB mac[0] / NB addresses[0] = "<mac> <ip> <ip>…"; ["unknown"] → use neutron: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_NOTE in the file): event_class, table, events, event (type or any), row/old as {"__row__": {column: value}} (references to other rows are nested __row__ objects; the same row referenced twice is the same object: comparisons such as row.chassis != old.chassis rely on it), agent (plain attributes such as chassis, ovn_local_cr_lrps, ovn_local_lrps, distributed; mocked queries with their return_value, e.g. is_ip_exposed, is_ls_provider, nb_idl.lsp_get().execute()), matches, and error when 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/…WithdrawEvent and OVNPFCreateEvent/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, old and an agent stub from each case, run the rewrite's filter, compare with matches. A rewrite that pre-filters with OVSDB monitor_cond conditions 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

ValueFormulaEvidence
bridge ARP/NDP addresses169.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 numberrandom 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 restartovn_bgp_agent/utils/linux_net.py:232-272
EVPN anycast MAC02: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 tablethe VNIovn_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

ExpectationWhere it comes from
Neutron writes the external_ids keys of 03 § 5 exactly; neutron-<uuid> switch and router namesovn_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 Neutronovn_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 boundused 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 methodscenarios 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 with hostNetwork, /run/frr, /etc/iproute2, the OVS socket and /run/openvswitch mounted [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 → vtysh exec (same texts) or frr-k8s; OVS → ovs-vsctl/ovs-ofctl exec or OpenFlow library.

7. Conformance strategy

  1. Event filters: run the 245 vectors (§ 3.1). Target 245/245.
  2. Side effects: for each scenario of side-effects_stable-2026.2.json, give the rewrite the same OVN answers and node starting state (bridge br-ex MAC aa:aa:aa:aa:aa:01, patch port ofport 5, empty rt_tables except 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 named defect-* record current defects: decide per defect whether to reproduce it (normally not).
  3. 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 (FipOnCRLRPBaseEvent subclasses), gateway-move FIP events (CrLrpChassisChange*) and port-forwarding events (OVNPF*): no vectors.
  • D16 — NB driver, underlay, regression since 5.0.0: ensure_base_wiring_config replaces the driver's empty ovn_routing_tables dict with a new one (routing_tables = routing_tables or {}, commit ccf4c29), so the driver's dict is never filled: wiring of provider IPs raises KeyError (swallowed: nothing announced) and every sync fails in cleanup_wiring with KeyError: '<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 scenario defect-d16-nb-underlay-routing-tables. The unit tests mock ensure_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, the add-vrf fragment renders rd None:<vni> (scenario frr-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 from lo or evpn_nic.
  • D1 (patch port not ready → IP not announced) and D2 (set_device_status TypeError) confirmed by scenarios; D14 (BFD table not registered) not run (no OVSDB server here).
  • exposing_method = ovn and vrf were 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_Binding per sync and per FIP lookup; vtysh -f every 15 s; ovs-ofctl dump-flows per 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_ids identity, 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: host and subnet methods, 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.