Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from neutron.conf.policies import base
from oslo_policy import policy

from neutron_understack import evpn_compat

COLLECTION_PATH = "/routers"
RESOURCE_PATH = "/routers/{id}"

Expand Down Expand Up @@ -31,4 +33,9 @@


def list_rules():
# When core owns EVPN it registers these same policies; registering them
# again makes neutron.policy.register_rules() raise DuplicatePolicyError
# and abort policy initialization for the whole neutron-server.
if evpn_compat.core_provides_evpn():
return []
return rules
56 changes: 56 additions & 0 deletions python/neutron-understack/neutron_understack/evpn_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Compatibility helpers for the EVPN router extension.

The EVPN router feature (the ``evpn_vni`` attribute, its API extension and its
policies) was upstreamed into neutron. On a neutron that carries it (e.g. the
understack/2026.1 branch) core provides all of:

* ``neutron_lib.api.definitions.evpn`` -- the router ``evpn_vni`` attribute
* ``neutron.extensions.evpn`` -- the API extension
* ``neutron.conf.policies.evpn`` -- the create/get ``router:evpn_vni`` policies

If ``neutron_understack`` also registers any of these, neutron-server fails at
startup -- most visibly ``DuplicatePolicyError`` raised from
``neutron.policy.register_rules()``, which aborts policy initialization for the
whole service. When core owns EVPN, Understack must register none of them and
contribute only its runtime VNI allocation (the service-plugin callbacks).

Every surface that would otherwise register an EVPN artifact keys off
``core_provides_evpn()`` so the decision is made in exactly one place.
"""

from neutron.conf import policies as core_policies

from neutron_understack.api.definitions import understack_vni

try:
from neutron_lib.api.definitions import evpn as core_evpn_apidef
except ImportError:
core_evpn_apidef = None

# The router EVPN policy name core registers when it owns the feature. This is
# also the exact rule neutron_understack would otherwise re-register, so its
# presence in core's policy list is what triggers the DuplicatePolicyError.
_CORE_EVPN_POLICY = "create_router:evpn_vni"


def core_provides_evpn():
"""Return True when neutron core registers the EVPN router feature itself.

Checks the actual policy list neutron.policy.register_rules() consumes
(``neutron.conf.policies.list_rules()``), rather than merely whether the
extension module is importable -- core registers these policies
unconditionally at startup, independent of which extensions are loaded.
"""
return any(rule.name == _CORE_EVPN_POLICY for rule in core_policies.list_rules())


def api_definition():
"""The api-definition to use for the router VNI extension.

Prefer core's ``evpn`` api-definition when core owns the feature so the
``evpn_vni`` attribute is defined exactly once; otherwise fall back to
Understack's own definition.
"""
if core_provides_evpn() and core_evpn_apidef is not None:
return core_evpn_apidef
return understack_vni
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
from importlib import util as importlib_util

from neutron_lib.api import extensions

try:
from neutron_lib.api.definitions import evpn as evpn_apidef
except ImportError:
evpn_apidef = None

from neutron_understack.api.definitions import understack_vni as apidef


def _api_definition():
if (
evpn_apidef is not None
and importlib_util.find_spec("neutron.extensions.evpn") is None
):
return evpn_apidef
return apidef


# This descriptor carries Understack's own ``understack_vni`` alias. It is only
# applied to routers when the UnderstackVniPlugin advertises that alias (see
# vrf._supported_extension_aliases); when core owns EVPN the plugin advertises
# core's ``evpn`` alias instead, so this descriptor is loaded but never applied.
class Understack_vni(extensions.APIExtensionDescriptor):
api_definition = _api_definition()
api_definition = apidef
12 changes: 4 additions & 8 deletions python/neutron-understack/neutron_understack/l3_router/vrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,14 @@
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib.db import resource_extend

try:
from neutron_lib.api.definitions import evpn as evpn_apidef
except ImportError:
evpn_apidef = None
from neutron_lib.plugins import constants as plugin_constants
from neutron_lib.plugins import directory
from neutron_lib.services import base as service_base
from oslo_config import cfg
from oslo_log import log as logging

from neutron_understack import config
from neutron_understack import evpn_compat
from neutron_understack.api.definitions import understack_vni as apidef
from neutron_understack.l3_router import understack_vni_db

Expand All @@ -44,9 +40,9 @@ def _is_vrf_router(context, router):


def _supported_extension_aliases():
if evpn_apidef is not None:
return [evpn_apidef.ALIAS]
return [apidef.ALIAS]
# Advertise whichever extension provides the router evpn_vni attribute:
# core's ``evpn`` when core owns it, otherwise Understack's own.
return [evpn_compat.api_definition().ALIAS]


@resource_extend.has_resource_extenders
Expand Down
Loading