From 4dd785bcb6caa4ca9819249c8d00a57bada0bfb6 Mon Sep 17 00:00:00 2001 From: Nidhi Rai Date: Tue, 14 Jul 2026 15:07:34 +0530 Subject: [PATCH] moved Palo Alto router provider on L3 base class --- .../neutron_understack/l3_router/palo_alto.py | 89 ++++++++++++++- .../tests/test_palo_alto_provider.py | 107 ++++++++++++++++++ 2 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/tests/test_palo_alto_provider.py diff --git a/python/neutron-understack/neutron_understack/l3_router/palo_alto.py b/python/neutron-understack/neutron_understack/l3_router/palo_alto.py index 34a4851b6..5aa473f1f 100644 --- a/python/neutron-understack/neutron_understack/l3_router/palo_alto.py +++ b/python/neutron-understack/neutron_understack/l3_router/palo_alto.py @@ -1,5 +1,88 @@ -from neutron.services.ovn_l3.service_providers.user_defined import UserDefined +import logging +from neutron.services.l3_router.service_providers import base +from neutron_lib import constants as const +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib.plugins import constants as plugin_constants +from neutron_lib.plugins import directory -class PaloAlto(UserDefined): - pass +LOG = logging.getLogger(__name__) + + +@registry.has_registry_receivers +class PaloAlto(base.L3ServiceProvider): + """Stub L3 service provider for the Palo Alto router flavor. + + Inherits from the base L3 service provider instead + of UserDefined. Routers of this flavor are detected + via their flavor's service profile driver. + """ + + ha_support = base.OPTIONAL + + def __init__(self, l3_plugin): + super().__init__(l3_plugin) + self._palo_alto_provider = f"{__name__}.{self.__class__.__name__}" + LOG.info( + "Palo Alto service provider initialized: driver=%r", + self._palo_alto_provider, + ) + + @property + def _flavor_plugin(self): + try: + return self._flavor_plugin_ref + except AttributeError: + self._flavor_plugin_ref = directory.get_plugin(plugin_constants.FLAVORS) + return self._flavor_plugin_ref + + def _is_palo_alto_provider(self, context, router): + flavor_id = router.get("flavor_id") + if flavor_id is None or flavor_id is const.ATTR_NOT_SPECIFIED: + LOG.debug( + "Palo Alto flavor check skipped: router=%s name=%s project=%s " + "flavor=%s request_id=%s", + router.get("id"), + router.get("name"), + router.get("project_id"), + flavor_id, + getattr(context, "request_id", None), + ) + return False + flavor = self._flavor_plugin.get_flavor(context, flavor_id) + provider = self._flavor_plugin.get_flavor_next_provider(context, flavor["id"])[ + 0 + ] + actual_driver = str(provider["driver"]) + matched = actual_driver == self._palo_alto_provider + LOG.debug( + "Palo Alto flavor check: router=%s name=%s project=%s flavor=%s " + "expected_driver=%s actual_driver=%s matched=%s request_id=%s", + router.get("id"), + router.get("name"), + router.get("project_id"), + flavor_id, + self._palo_alto_provider, + actual_driver, + matched, + getattr(context, "request_id", None), + ) + return matched + + @registry.receives(resources.ROUTER, [events.AFTER_CREATE]) + def _process_router_create(self, resource, event, trigger, payload=None): + router = payload.states[0] + context = payload.context + if not self._is_palo_alto_provider(context, router): + return + LOG.info( + "Palo Alto stub router create: no action taken for router=%s " + "name=%s project=%s flavor=%s request_id=%s", + router.get("id"), + router.get("name"), + router.get("project_id"), + router.get("flavor_id"), + getattr(context, "request_id", None), + ) diff --git a/python/neutron-understack/neutron_understack/tests/test_palo_alto_provider.py b/python/neutron-understack/neutron_understack/tests/test_palo_alto_provider.py new file mode 100644 index 000000000..af23e2999 --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/test_palo_alto_provider.py @@ -0,0 +1,107 @@ +from neutron_lib import constants as const + +from neutron_understack.l3_router import palo_alto + + +class FakeFlavorPlugin: + def __init__(self, driver): + self.driver = driver + + def get_flavor(self, _context, flavor_id): + return {"id": flavor_id} + + def get_flavor_next_provider(self, _context, _flavor_id): + return [{"driver": self.driver}] + + +class FakePayload: + def __init__(self, router, context="context"): + self.states = (router,) + self.context = context + self.resource_id = router.get("id") + + +def _palo_alto_driver(): + return f"{palo_alto.PaloAlto.__module__}.{palo_alto.PaloAlto.__name__}" + + +class TestPaloAltoProvider: + def test_flavor_plugin_is_cached(self, mocker): + plugin = FakeFlavorPlugin(_palo_alto_driver()) + get_plugin = mocker.patch.object( + palo_alto.directory, "get_plugin", return_value=plugin + ) + provider = palo_alto.PaloAlto(None) + + assert provider._flavor_plugin is plugin + assert provider._flavor_plugin is plugin + get_plugin.assert_called_once_with(palo_alto.plugin_constants.FLAVORS) + + def test_is_palo_alto_provider_returns_false_without_flavor(self, mocker): + get_plugin = mocker.patch.object(palo_alto.directory, "get_plugin") + provider = palo_alto.PaloAlto(None) + + assert provider._is_palo_alto_provider("context", {"id": "router-a"}) is False + assert ( + provider._is_palo_alto_provider( + "context", + {"id": "router-a", "flavor_id": const.ATTR_NOT_SPECIFIED}, + ) + is False + ) + get_plugin.assert_not_called() + + def test_is_palo_alto_provider_returns_true_for_palo_alto_driver(self, mocker): + plugin = FakeFlavorPlugin(_palo_alto_driver()) + mocker.patch.object(palo_alto.directory, "get_plugin", return_value=plugin) + provider = palo_alto.PaloAlto(None) + + assert ( + provider._is_palo_alto_provider( + "context", + {"id": "router-a", "flavor_id": "palo-alto-flavor-id"}, + ) + is True + ) + + def test_is_palo_alto_provider_returns_false_for_different_driver(self, mocker): + plugin = FakeFlavorPlugin("neutron_understack.l3_router.vrf.Vrf") + mocker.patch.object(palo_alto.directory, "get_plugin", return_value=plugin) + provider = palo_alto.PaloAlto(None) + + assert ( + provider._is_palo_alto_provider( + "context", + {"id": "router-a", "flavor_id": "vrf-flavor-id"}, + ) + is False + ) + + def test_router_create_is_noop_for_palo_alto_router(self, mocker): + plugin = FakeFlavorPlugin(_palo_alto_driver()) + mocker.patch.object(palo_alto.directory, "get_plugin", return_value=plugin) + l3_plugin = mocker.Mock() + provider = palo_alto.PaloAlto(l3_plugin) + payload = FakePayload({"id": "router-a", "flavor_id": "palo-alto-flavor-id"}) + + result = provider._process_router_create( + "router", "after_create", "trigger", payload + ) + + # Stub flavor: no action is taken on a matching router. + assert result is None + assert l3_plugin.mock_calls == [] + + def test_router_create_skips_non_palo_alto_router(self, mocker): + plugin = FakeFlavorPlugin("neutron_understack.l3_router.vrf.Vrf") + mocker.patch.object(palo_alto.directory, "get_plugin", return_value=plugin) + l3_plugin = mocker.Mock() + provider = palo_alto.PaloAlto(l3_plugin) + payload = FakePayload({"id": "router-b", "flavor_id": "vrf-flavor-id"}) + + result = provider._process_router_create( + "router", "after_create", "trigger", payload + ) + + assert result is None + assert l3_plugin.mock_calls == []