Summary
Investigating a "no outbound connectivity" report on gateway (staging, PUBLICNET) turned up two separate OVN NB drift bugs that left a router's external gateway completely non-functional, with no automated tooling able to detect or repair either one.
What we found
1. Missing OVN localnet uplink port, with no reconciliation path
routers.create_uplink_port/delete_uplink_port (neutron_understack/routers.py) create/delete a raw type=localnet Logical_Switch_Port directly via ovn_client()._nb_idl.create_lswitch_port(...), bypassing Neutron's revision-number tracking entirely (empty external_ids, no ovn_revision_numbers row).
Neutron's core OVN maintenance worker (check_for_inconsistencies / get_deleted_resources) can only detect drift for resources that were ever inserted into ovn_revision_numbers — confirmed by reading neutron/db/ovn_revision_numbers_db.py:get_deleted_resources, which filters ovn_revision_numbers rows with standard_attr_id IS NULL. Since this LSP was never in that table, the maintenance worker has no way to know it exists, let alone repair it if it goes missing.
In this incident, the localnet port for PUBLICNET's VLAN segment (1806/f20-5-network) was simply gone. Nothing noticed. Outbound traffic through the router's SNAT had no physical uplink to leave on.
2. Router gateway's switch-side peer LSP had type/options blanked, and neutron-ovn-db-sync-util --repair couldn't fix it
The switch-side Logical_Switch_Port peer for the router's gateway LRP (name = the Neutron port ID, external_ids correctly showing device_owner=network:router_gateway) had type="" and options={} instead of type=router + options:router-port=.../nat-addresses/exclude-lb-vips-from-garp. This is set by SetLRouterPortInLSwitchPortCommand (core Neutron OVN driver, commands.py), called from create_router_port/_update_lrouter_port. We don't have a confirmed trigger for how it got blanked (candidate: ovn_client.update_port()'s upsert-style path skipping the type=router re-assertion — see ovn_client.py:709-719 — though the is_ovn_provider_router() gate should have passed for this non-flavored router, so this isn't fully proven).
More importantly: running neutron-ovn-db-sync-util's full repair did not fix it. ovn_db_sync.py:_is_router_port_changed (used by sync_routers_and_rports to decide whether a router port needs re-syncing) only compares networks and ipv6_ra_configs — the LRP-side data. It never inspects the switch-side peer LSP's type/options at all. Since this router's LRP-side data was never corrupted (only the switch-side peer was), the sync tool concluded "no change needed" and silently skipped the exact field that was broken. The repair run visibly refreshed external_ids/revision_number (via the generic ports-sync pass) but left type/options untouched.
Both were fixed manually via direct ovn-nbctl writes (not via any existing tool):
# 1. recreate the missing localnet uplink port
ovn-nbctl lsp-add <switch> uplink-<segment_id> -- \
lsp-set-type uplink-<segment_id> localnet -- \
lsp-set-addresses uplink-<segment_id> unknown -- \
set Logical_Switch_Port uplink-<segment_id> tag=<vlan> \
options:network_name=<physnet> options:mcast_flood_reports=true \
options:mcast_flood=false options:localnet_learn_fdb=false
# 2. restore the router-gateway peer LSP's type/options
ovn-nbctl set Logical_Switch_Port <router_port_id> \
type=router \
options:router-port=lrp-<router_port_id> \
options:nat-addresses=router \
options:exclude-lb-vips-from-garp=true
Ask
- For (1): either make
create_uplink_port/delete_uplink_port go through Neutron's revision-tracked port-creation path so the existing maintenance-worker machinery covers them for free, or add a dedicated periodic reconciliation task in neutron-understack that walks router-owned networks and recreates whichever half (Neutron trunk-subport vs. OVN localnet LSP) is missing.
- For (2): this is upstream/core Neutron behavior (
neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py), not neutron-understack — worth deciding whether we carry a local patch, or report/patch upstream, so _is_router_port_changed also compares the switch-side peer LSP's type/options, not just the LRP's networks.
Note: we weren't able to confirm the exact trigger event for (2) — it happened around the same time as an unrelated RabbitMQ queue-durability cleanup + neutron-server restart, but that's circumstantial, not confirmed from logs.
Environment
rax-staging-iad3-staging
- Router:
1533c5be-e717-45d3-a082-f84f18063016
- Network:
PUBLICNET (b420e863-7d26-4c87-8974-76f6c8750640), VLAN 1806 / f20-5-network
Summary
Investigating a "no outbound connectivity" report on
gateway(staging,PUBLICNET) turned up two separate OVN NB drift bugs that left a router's external gateway completely non-functional, with no automated tooling able to detect or repair either one.What we found
1. Missing OVN localnet uplink port, with no reconciliation path
routers.create_uplink_port/delete_uplink_port(neutron_understack/routers.py) create/delete a rawtype=localnetLogical_Switch_Portdirectly viaovn_client()._nb_idl.create_lswitch_port(...), bypassing Neutron's revision-number tracking entirely (emptyexternal_ids, noovn_revision_numbersrow).Neutron's core OVN maintenance worker (
check_for_inconsistencies/get_deleted_resources) can only detect drift for resources that were ever inserted intoovn_revision_numbers— confirmed by readingneutron/db/ovn_revision_numbers_db.py:get_deleted_resources, which filtersovn_revision_numbersrows withstandard_attr_id IS NULL. Since this LSP was never in that table, the maintenance worker has no way to know it exists, let alone repair it if it goes missing.In this incident, the localnet port for
PUBLICNET's VLAN segment (1806/f20-5-network) was simply gone. Nothing noticed. Outbound traffic through the router's SNAT had no physical uplink to leave on.2. Router gateway's switch-side peer LSP had
type/optionsblanked, andneutron-ovn-db-sync-util --repaircouldn't fix itThe switch-side
Logical_Switch_Portpeer for the router's gateway LRP (name = the Neutron port ID, external_ids correctly showingdevice_owner=network:router_gateway) hadtype=""andoptions={}instead oftype=router+options:router-port=.../nat-addresses/exclude-lb-vips-from-garp. This is set bySetLRouterPortInLSwitchPortCommand(core Neutron OVN driver,commands.py), called fromcreate_router_port/_update_lrouter_port. We don't have a confirmed trigger for how it got blanked (candidate:ovn_client.update_port()'s upsert-style path skipping thetype=routerre-assertion — seeovn_client.py:709-719— though theis_ovn_provider_router()gate should have passed for this non-flavored router, so this isn't fully proven).More importantly: running
neutron-ovn-db-sync-util's full repair did not fix it.ovn_db_sync.py:_is_router_port_changed(used bysync_routers_and_rportsto decide whether a router port needs re-syncing) only comparesnetworksandipv6_ra_configs— the LRP-side data. It never inspects the switch-side peer LSP'stype/optionsat all. Since this router's LRP-side data was never corrupted (only the switch-side peer was), the sync tool concluded "no change needed" and silently skipped the exact field that was broken. The repair run visibly refreshedexternal_ids/revision_number(via the generic ports-sync pass) but lefttype/optionsuntouched.Both were fixed manually via direct
ovn-nbctlwrites (not via any existing tool):Ask
create_uplink_port/delete_uplink_portgo through Neutron's revision-tracked port-creation path so the existing maintenance-worker machinery covers them for free, or add a dedicated periodic reconciliation task inneutron-understackthat walks router-owned networks and recreates whichever half (Neutron trunk-subport vs. OVN localnet LSP) is missing.neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py), notneutron-understack— worth deciding whether we carry a local patch, or report/patch upstream, so_is_router_port_changedalso compares the switch-side peer LSP'stype/options, not just the LRP'snetworks.Note: we weren't able to confirm the exact trigger event for (2) — it happened around the same time as an unrelated RabbitMQ queue-durability cleanup + neutron-server restart, but that's circumstantial, not confirmed from logs.
Environment
rax-staging-iad3-staging1533c5be-e717-45d3-a082-f84f18063016PUBLICNET(b420e863-7d26-4c87-8974-76f6c8750640), VLAN 1806 /f20-5-network