diff --git a/neutron/common/ovn/extensions.py b/neutron/common/ovn/extensions.py index 7ca4b98f576..0122e438e9d 100644 --- a/neutron/common/ovn/extensions.py +++ b/neutron/common/ovn/extensions.py @@ -44,6 +44,7 @@ from neutron_lib.api.definitions import l3_enable_default_route_bfd from neutron_lib.api.definitions import l3_enable_default_route_ecmp from neutron_lib.api.definitions import l3_ext_gw_mode from neutron_lib.api.definitions import l3_ext_gw_multihoming +from neutron_lib.api.definitions import l3_ext_ha_mode from neutron_lib.api.definitions import l3_flavors from neutron_lib.api.definitions import logging from neutron_lib.api.definitions import multiprovidernet @@ -125,6 +126,7 @@ ML2_SUPPORTED_API_EXTENSIONS_OVN_L3 = [ l3_ext_gw_multihoming.ALIAS, l3_enable_default_route_bfd.ALIAS, l3_enable_default_route_ecmp.ALIAS, + l3_ext_ha_mode.ALIAS, ] ML2_SUPPORTED_API_EXTENSIONS = [ address_group.ALIAS, diff --git a/neutron/db/ovn_l3_hamode_db.py b/neutron/db/ovn_l3_hamode_db.py new file mode 100644 index 00000000000..b3123cdde22 --- /dev/null +++ b/neutron/db/ovn_l3_hamode_db.py @@ -0,0 +1,35 @@ +# Copyright 2024 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron_lib.callbacks import events +from neutron_lib.callbacks import priority_group +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources + +from neutron.db import l3_attrs_db + + +@registry.has_registry_receivers +class OVN_L3_HA_db_mixin(l3_attrs_db.ExtraAttributesMixin): + """Mixin class to add high availability capability to OVN routers.""" + + @registry.receives(resources.ROUTER, [events.PRECOMMIT_CREATE], + priority_group.PRIORITY_ROUTER_EXTENDED_ATTRIBUTE) + def _precommit_router_create(self, resource, event, trigger, payload): + """Event handler to set ha flag creation.""" + # NOTE(ralonsoh): OVN L3 router HA flag is mandatory and True always, + # enforced by ``OvnDriver.ha_support`` set to ``MANDATORY``. This flag + # cannot be updated. + router_db = payload.metadata['router_db'] + self.set_extra_attr_value(router_db, 'ha', True) diff --git a/neutron/services/l3_router/service_providers/driver_controller.py b/neutron/services/l3_router/service_providers/driver_controller.py index 729ac7054b0..7dd59b18928 100644 --- a/neutron/services/l3_router/service_providers/driver_controller.py +++ b/neutron/services/l3_router/service_providers/driver_controller.py @@ -139,7 +139,7 @@ class DriverController(object): # attributes via the API. try: _ensure_driver_supports_request(drv, payload.request_body) - except lib_exc.InvalidInput: + except lib_exc.InvalidInput as exc: # the current driver does not support this request, we need to # migrate to a new provider. populate the distributed and ha # flags from the previous state if not in the update so we can @@ -162,7 +162,7 @@ class DriverController(object): {'ha_flag': payload.request_body['ha'], 'distributed_flag': payload.request_body['distributed']}) - new_drv = self._attrs_to_driver(payload.request_body) + new_drv = self._attrs_to_driver(payload.request_body, exc=exc) if new_drv: LOG.debug("Router %(id)s migrating from %(old)s provider to " "%(new)s provider.", {'id': payload.resource_id, @@ -224,7 +224,7 @@ class DriverController(object): driver = self.drivers[provider['provider']] return driver - def _attrs_to_driver(self, router): + def _attrs_to_driver(self, router, exc=None): """Get a provider driver handle based on the ha/distributed flags.""" distributed = _is_distributed( router.get('distributed', lib_const.ATTR_NOT_SPECIFIED)) @@ -236,6 +236,9 @@ class DriverController(object): for driver in drivers: if _is_driver_compatible(distributed, ha, driver): return driver + + if exc: + raise exc raise NotImplementedError( _("Could not find a service provider that supports " "distributed=%(d)s and ha=%(h)s") % {'d': distributed, 'h': ha} diff --git a/neutron/services/ovn_l3/service_providers/ovn.py b/neutron/services/ovn_l3/service_providers/ovn.py index d466f2e931f..00ea368748c 100644 --- a/neutron/services/ovn_l3/service_providers/ovn.py +++ b/neutron/services/ovn_l3/service_providers/ovn.py @@ -26,6 +26,7 @@ from oslo_utils import excutils from neutron.common.ovn import constants as ovn_const from neutron.common.ovn import utils from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf +from neutron.db import ovn_l3_hamode_db as ovn_l3_ha from neutron.db import ovn_revision_numbers_db as db_rev from neutron.extensions import revisions from neutron.objects import router as l3_obj @@ -37,7 +38,8 @@ LOG = logging.getLogger(__name__) @registry.has_registry_receivers -class OvnDriver(base.L3ServiceProvider): +class OvnDriver(base.L3ServiceProvider, + ovn_l3_ha.OVN_L3_HA_db_mixin): ha_support = base.MANDATORY distributed_support = base.MANDATORY diff --git a/neutron/tests/unit/db/test_ovn_l3_hamode_db.py b/neutron/tests/unit/db/test_ovn_l3_hamode_db.py new file mode 100644 index 00000000000..ab260d335e5 --- /dev/null +++ b/neutron/tests/unit/db/test_ovn_l3_hamode_db.py @@ -0,0 +1,50 @@ +# Copyright 2024 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron_lib import context +from neutron_lib.db import api as db_api +from neutron_lib.plugins import constants as plugin_constants +from neutron_lib.plugins import directory + +from neutron.db import ovn_l3_hamode_db +from neutron.objects import router as router_obj +from neutron.tests.unit.db import test_db_base_plugin_v2 as test_plugin +from neutron.tests.unit.db import test_l3_dvr_db + + +class FakeOVNL3Plugin(test_l3_dvr_db.FakeL3Plugin, + ovn_l3_hamode_db.OVN_L3_HA_db_mixin): + pass + + +class OVN_L3_HA_db_mixinTestCase(test_plugin.NeutronDbPluginV2TestCase): + + def setUp(self, **kwargs): + super().setUp(plugin='ml2', **kwargs) + self.core_plugin = directory.get_plugin() + self.ctx = context.get_admin_context() + self.mixin = FakeOVNL3Plugin() + directory.add_plugin(plugin_constants.L3, self.mixin) + + def _create_router(self, router): + with db_api.CONTEXT_WRITER.using(self.ctx): + return self.mixin._create_router_db(self.ctx, router, 'foo_tenant') + + def test_create_router(self): + router_dict = {'name': 'foo_router', 'admin_state_up': True, + 'distributed': False} + router_db = self._create_router(router_dict) + router = router_obj.Router.get_object(self.ctx, id=router_db.id) + self.assertTrue(router.extra_attributes.ha) diff --git a/neutron/tests/unit/services/ovn_l3/service_providers/test_driver_controller.py b/neutron/tests/unit/services/ovn_l3/service_providers/test_driver_controller.py new file mode 100644 index 00000000000..cb5e1ad648a --- /dev/null +++ b/neutron/tests/unit/services/ovn_l3/service_providers/test_driver_controller.py @@ -0,0 +1,59 @@ +# Copyright 2024 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib import context +from neutron_lib import exceptions as lib_exc + +from neutron.services.l3_router.service_providers import driver_controller \ + as l3_driver_controller +from neutron.services.ovn_l3.service_providers import driver_controller +from neutron.tests.unit import testlib_api + + +DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2' + + +class TestDriverController(testlib_api.SqlTestCase): + + def setUp(self): + super(TestDriverController, self).setUp() + self.setup_coreplugin(DB_PLUGIN_KLASS) + self.fake_l3 = mock.Mock() + self.dc = driver_controller.DriverController(self.fake_l3) + self.fake_l3.l3_driver_controller = self.dc + self.ctx = context.get_admin_context() + + def test__update_router_provider_ha_mandatory(self): + test_dc = driver_controller.DriverController(self.fake_l3) + with mock.patch.object(registry, "publish") as mock_cb: + with mock.patch.object(test_dc, "get_provider_for_router"): + with mock.patch.object( + l3_driver_controller, + "_ensure_driver_supports_request") as _ensure: + _ensure.side_effect = lib_exc.InvalidInput( + error_message='message') + self.assertRaises( + lib_exc.InvalidInput, + test_dc._update_router_provider, + None, None, None, + payload=events.DBEventPayload( + None, request_body={'ha': False, + 'distributed': True}, + states=({'flavor_id': None},)) + ) + mock_cb.assert_not_called() diff --git a/releasenotes/notes/ovn-l3-router-ha-enablement-24c5a5f9fc763db1.yaml b/releasenotes/notes/ovn-l3-router-ha-enablement-24c5a5f9fc763db1.yaml new file mode 100644 index 00000000000..9a09b40300e --- /dev/null +++ b/releasenotes/notes/ovn-l3-router-ha-enablement-24c5a5f9fc763db1.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Enabled the ``ha`` API extension for OVN routers. Now the high + availability flag ``ha`` can be set and read for OVN routers. NOTE: + currently OVN routers are always HA; the flag is mandatory + and cannot be unset (but now can be read).