From 49628a1cb8fa30e916d2262ed2f19c009ee87936 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 16 Jul 2026 12:37:33 -0500 Subject: [PATCH] fix(neutron-understack): wire up better compat for evpn alias If the evpn alias doesn't exist we need to wire up our compat layer but if it does then we need to use the one that exists. This provides an easier to use compat layer which avoids the DuplicatePolicyError that is seen when starting up the services. --- .../neutron_understack/conf/policies/evpn.py | 7 +++ .../neutron_understack/evpn_compat.py | 56 +++++++++++++++++++ .../extensions/understack_vni.py | 22 ++------ .../neutron_understack/l3_router/vrf.py | 12 ++-- 4 files changed, 72 insertions(+), 25 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/evpn_compat.py 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