diff --git a/python/neutron-understack/neutron_understack/conf/policies/evpn.py b/python/neutron-understack/neutron_understack/conf/policies/evpn.py index 916f07caf..415ff1136 100644 --- a/python/neutron-understack/neutron_understack/conf/policies/evpn.py +++ b/python/neutron-understack/neutron_understack/conf/policies/evpn.py @@ -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}" @@ -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 diff --git a/python/neutron-understack/neutron_understack/evpn_compat.py b/python/neutron-understack/neutron_understack/evpn_compat.py new file mode 100644 index 000000000..dd9c38bee --- /dev/null +++ b/python/neutron-understack/neutron_understack/evpn_compat.py @@ -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 diff --git a/python/neutron-understack/neutron_understack/extensions/understack_vni.py b/python/neutron-understack/neutron_understack/extensions/understack_vni.py index 9e181c126..58cf6202d 100644 --- a/python/neutron-understack/neutron_understack/extensions/understack_vni.py +++ b/python/neutron-understack/neutron_understack/extensions/understack_vni.py @@ -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 diff --git a/python/neutron-understack/neutron_understack/l3_router/vrf.py b/python/neutron-understack/neutron_understack/l3_router/vrf.py index ff0ba6194..53759b925 100644 --- a/python/neutron-understack/neutron_understack/l3_router/vrf.py +++ b/python/neutron-understack/neutron_understack/l3_router/vrf.py @@ -6,11 +6,6 @@ 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 @@ -18,6 +13,7 @@ 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 @@ -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