diff --git a/neutron/common/ovn/extensions.py b/neutron/common/ovn/extensions.py index 1d25ef6ed68..67c67c3d49a 100644 --- a/neutron/common/ovn/extensions.py +++ b/neutron/common/ovn/extensions.py @@ -40,6 +40,7 @@ from neutron_lib.api.definitions import l3_ext_gw_mode from neutron_lib.api.definitions import logging from neutron_lib.api.definitions import multiprovidernet from neutron_lib.api.definitions import network_availability_zone +from neutron_lib.api.definitions import network_ha from neutron_lib.api.definitions import network_ip_availability from neutron_lib.api.definitions import network_mtu from neutron_lib.api.definitions import network_mtu_writable @@ -122,6 +123,7 @@ ML2_SUPPORTED_API_EXTENSIONS = [ extra_dhcp_opt.ALIAS, filter_validation.ALIAS, multiprovidernet.ALIAS, + network_ha.ALIAS, network_mtu.ALIAS, network_mtu_writable.ALIAS, network_availability_zone.ALIAS, diff --git a/neutron/db/l3_hamode_db.py b/neutron/db/l3_hamode_db.py index 5e65371e4aa..1b6412a3d27 100644 --- a/neutron/db/l3_hamode_db.py +++ b/neutron/db/l3_hamode_db.py @@ -19,6 +19,7 @@ import random import netaddr from neutron_lib.api.definitions import l3 as l3_apidef from neutron_lib.api.definitions import l3_ext_ha_mode as l3_ext_ha_apidef +from neutron_lib.api.definitions import network_ha as network_ha_apidef from neutron_lib.api.definitions import port as port_def from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import provider_net as providernet @@ -63,6 +64,11 @@ LOG = logging.getLogger(__name__) l3_hamode_db.register_db_l3_hamode_opts() +# TODO(ralonsoh): move to neutron-lib +class DuplicatedHANetwork(n_exc.Conflict): + message = _('Project %(project_id)s already has a HA network.') + + @registry.has_registry_receivers class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin, router_az_db.RouterAvailabilityZoneMixin): @@ -192,21 +198,21 @@ class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin, return p_utils.create_subnet(self._core_plugin, context, {'subnet': args}) - def _create_ha_network_tenant_binding(self, context, tenant_id, - network_id): - ha_network = l3_hamode.L3HARouterNetwork( - context, project_id=tenant_id, network_id=network_id) - ha_network.create() - # we need to check if someone else just inserted at exactly the - # same time as us because there is no constrain in L3HARouterNetwork - # that prevents multiple networks per tenant - if l3_hamode.L3HARouterNetwork.count( - context, project_id=tenant_id) > 1: - # we need to throw an error so our network is deleted - # and the process is started over where the existing - # network will be selected. - raise db_exc.DBDuplicateEntry(columns=['tenant_id']) - return None, ha_network + @registry.receives(resources.NETWORK, [events.PRECOMMIT_CREATE]) + def _create_ha_network_tenant_binding(self, resource, event, trigger, + payload=None): + if not payload.request_body.get(network_ha_apidef.HA): + return + + network = payload.latest_state + context = payload.context + ha_network = l3_hamode.L3HARouterNetwork(payload.context, + project_id=context.project_id, + network_id=network['id']) + try: + ha_network.create() + except obj_base.NeutronDbObjectDuplicateEntry: + raise DuplicatedHANetwork(project_id=context.project_id) def _add_ha_network_settings(self, network): if cfg.CONF.l3_ha_network_type: @@ -218,29 +224,27 @@ class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin, def _create_ha_network(self, context, tenant_id): admin_ctx = context.elevated() - + # The project ID is needed to create the ``L3HARouterNetwork`` + # resource; the project ID cannot be retrieved from the network because + # is explicitly created without it. + admin_ctx.project_id = tenant_id args = {'network': {'name': constants.HA_NETWORK_NAME % tenant_id, 'tenant_id': '', 'shared': False, - 'admin_state_up': True}} + 'admin_state_up': True, + network_ha_apidef.HA: True, + }} self._add_ha_network_settings(args['network']) - creation = functools.partial(p_utils.create_network, - self._core_plugin, admin_ctx, args) - content = functools.partial(self._create_ha_network_tenant_binding, - admin_ctx, tenant_id) - deletion = functools.partial(self._core_plugin.delete_network, - admin_ctx) - - network, ha_network = db_utils.safe_creation( - context, creation, deletion, content, transaction=False) + network = p_utils.create_network(self._core_plugin, admin_ctx, args) try: self._create_ha_subnet(admin_ctx, network['id'], tenant_id) except Exception: with excutils.save_and_reraise_exception(): self._core_plugin.delete_network(admin_ctx, network['id']) - return ha_network + return l3_hamode.L3HARouterNetwork.get_object( + admin_ctx, network_id=network['id'], project_id=tenant_id) def get_number_of_agents_for_scheduling(self, context): """Return number of agents on which the router will be scheduled.""" @@ -370,8 +374,10 @@ class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin, # ensure the HA network exists before we start router creation so # we can provide meaningful errors back to the user if no network # can be allocated - if not self.get_ha_network(context, router['tenant_id']): - self._create_ha_network(context, router['tenant_id']) + # TODO(ralonsoh): remove once bp/keystone-v3 migration finishes. + project_id = router.get('project_id') or router['tenant_id'] + if not self.get_ha_network(context, project_id): + self._create_ha_network(context, project_id) @registry.receives(resources.ROUTER, [events.PRECOMMIT_CREATE], priority_group.PRIORITY_ROUTER_EXTENDED_ATTRIBUTE) diff --git a/neutron/extensions/network_ha.py b/neutron/extensions/network_ha.py new file mode 100644 index 00000000000..a32f3f6e1d5 --- /dev/null +++ b/neutron/extensions/network_ha.py @@ -0,0 +1,21 @@ +# Copyright 2023 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.api.definitions import network_ha +from neutron_lib.api import extensions + + +class Network_ha(extensions.APIExtensionDescriptor): + """Extension class supporting network HA.""" + api_definition = network_ha diff --git a/neutron/services/l3_router/l3_router_plugin.py b/neutron/services/l3_router/l3_router_plugin.py index 7573263bc61..1bd7bbfb73b 100644 --- a/neutron/services/l3_router/l3_router_plugin.py +++ b/neutron/services/l3_router/l3_router_plugin.py @@ -25,6 +25,7 @@ from neutron_lib.api.definitions import l3_ext_gw_mode from neutron_lib.api.definitions import l3_ext_ha_mode from neutron_lib.api.definitions import l3_flavors from neutron_lib.api.definitions import l3_port_ip_change_not_allowed +from neutron_lib.api.definitions import network_ha from neutron_lib.api.definitions import qos_gateway_ip from neutron_lib.api.definitions import \ router_admin_state_down_before_update as r_admin_state_down_before_update @@ -109,7 +110,9 @@ class L3RouterPlugin(service_base.ServicePluginBase, floatingip_pools.ALIAS, qos_gateway_ip.ALIAS, l3_port_ip_change_not_allowed.ALIAS, - r_admin_state_down_before_update.ALIAS] + r_admin_state_down_before_update.ALIAS, + network_ha.ALIAS, + ] __native_pagination_support = True __native_sorting_support = True diff --git a/neutron/tests/functional/scheduler/test_l3_agent_scheduler.py b/neutron/tests/functional/scheduler/test_l3_agent_scheduler.py index c91a494462a..646831772b0 100644 --- a/neutron/tests/functional/scheduler/test_l3_agent_scheduler.py +++ b/neutron/tests/functional/scheduler/test_l3_agent_scheduler.py @@ -16,6 +16,8 @@ import collections import random +from neutron_lib.api import attributes +from neutron_lib.api.definitions import network_ha from neutron_lib import constants from neutron_lib import context from neutron_lib.plugins import constants as plugin_constants @@ -302,6 +304,10 @@ class L3AZSchedulerBaseTest(test_db_base_plugin_v2.NeutronDbPluginV2TestCase): directory.add_plugin(plugin_constants.L3, self.l3_plugin) self.adminContext = context.get_admin_context() self.adminContext.tenant_id = '_func_test_tenant_' + # Extend network HA extension. + rname = network_ha.COLLECTION_NAME + attributes.RESOURCES[rname].update( + network_ha.RESOURCE_ATTRIBUTE_MAP[rname]) def _create_l3_agent(self, host, context, agent_mode='legacy', plugin=None, state=True, az='nova'): diff --git a/neutron/tests/unit/db/test_l3_hamode_db.py b/neutron/tests/unit/db/test_l3_hamode_db.py index 277bfcb753f..e44ee223805 100644 --- a/neutron/tests/unit/db/test_l3_hamode_db.py +++ b/neutron/tests/unit/db/test_l3_hamode_db.py @@ -14,10 +14,12 @@ from unittest import mock +from neutron_lib.api import attributes from neutron_lib.api.definitions import dvr as dvr_apidef from neutron_lib.api.definitions import external_net as extnet_apidef from neutron_lib.api.definitions import l3 as l3_apidef from neutron_lib.api.definitions import l3_ext_ha_mode +from neutron_lib.api.definitions import network_ha from neutron_lib.api.definitions import port as port_def from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import provider_net as providernet @@ -31,7 +33,6 @@ from neutron_lib.db import api as db_api from neutron_lib import exceptions as n_exc from neutron_lib.exceptions import l3 as l3_exc from neutron_lib.exceptions import l3_ext_ha_mode as l3ha_exc -from neutron_lib.objects import exceptions from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from oslo_config import cfg @@ -84,6 +85,10 @@ class L3HATestFramework(testlib_api.SqlTestCase): self.agent1 = helpers.register_l3_agent() self.agent2 = helpers.register_l3_agent( 'host_2', constants.L3_AGENT_MODE_DVR_SNAT) + # Extend network HA extension. + rname = network_ha.COLLECTION_NAME + attributes.RESOURCES[rname].update( + network_ha.RESOURCE_ATTRIBUTE_MAP[rname]) @property def admin_ctx(self): @@ -630,7 +635,8 @@ class L3HATestCase(L3HATestFramework): with mock.patch.object(l3_hamode, 'L3HARouterNetwork', side_effect=ValueError): - self.assertRaises(ValueError, self.plugin._create_ha_network, + self.assertRaises(c_exc.CallbackFailure, + self.plugin._create_ha_network, self.admin_ctx, _uuid()) networks_after = self.core_plugin.get_networks(self.admin_ctx) @@ -682,15 +688,24 @@ class L3HATestCase(L3HATestFramework): self.admin_ctx, binding.port_id) def test_create_ha_network_tenant_binding_raises_duplicate(self): - router = self._create_router() - network = self.plugin.get_ha_network(self.admin_ctx, - router['tenant_id']) - self.plugin._create_ha_network_tenant_binding( - self.admin_ctx, 't1', network['network_id']) - with testtools.ExpectedException( - exceptions.NeutronDbObjectDuplicateEntry): + # The router creation calls first the HA network creation and the + # HA network-tenant binding ("ha_router_networks" register) + project_id = uuidutils.generate_uuid() + self._create_router(tenant_id=project_id) + network = self.core_plugin.get_networks(self.admin_ctx)[0] + ha_network = self.plugin.get_ha_network(self.admin_ctx, project_id) + self.assertEqual(project_id, ha_network.project_id) + self.assertEqual(network['id'], ha_network.network_id) + + with testtools.ExpectedException(l3_hamode_db.DuplicatedHANetwork): + network[network_ha.HA] = True + ctx = self.admin_ctx # That will create a new admin context + ctx.project_id = project_id + payload = events.DBEventPayload( + ctx, states=(network, ), resource_id=network['id'], + request_body=network) self.plugin._create_ha_network_tenant_binding( - self.admin_ctx, 't1', network['network_id']) + mock.ANY, mock.ANY, mock.ANY, payload=payload) def test_create_router_db_vr_id_allocation_goes_to_error(self): for method in ('_ensure_vr_id', @@ -956,12 +971,15 @@ class L3HATestCase(L3HATestFramework): class L3HAModeDbTestCase(L3HATestFramework): def _create_network(self, plugin, ctx, name='net', - tenant_id='tenant1', external=False): + tenant_id='tenant1', external=False, ha=False): network = {'network': {'name': name, 'shared': False, 'admin_state_up': True, 'tenant_id': tenant_id, - extnet_apidef.EXTERNAL: external}} + 'project_id': tenant_id, + extnet_apidef.EXTERNAL: external, + network_ha.HA: ha, + }} return plugin.create_network(ctx, network)['id'] def _create_subnet(self, plugin, ctx, network_id, cidr='10.0.0.0/8', @@ -1380,6 +1398,21 @@ class L3HAModeDbTestCase(L3HATestFramework): router_ids=[router['id']]) self.assertEqual(self.agent2['host'], routers[0]['gw_port_host']) + def test__before_router_create_no_network(self): + project_id = 'project1' + ha_network = self.plugin.get_ha_network(self.admin_ctx, project_id) + self.assertIsNone(ha_network) + + router = {'ha': True, 'project_id': project_id} + self.plugin._before_router_create(mock.ANY, self.admin_ctx, router) + ha_network = self.plugin.get_ha_network(self.admin_ctx, project_id) + self.assertEqual(project_id, ha_network.project_id) + + # This second call ensures the method is idempotent. + self.plugin._before_router_create(mock.ANY, self.admin_ctx, router) + ha_network = self.plugin.get_ha_network(self.admin_ctx, project_id) + self.assertEqual(project_id, ha_network.project_id) + class L3HAUserTestCase(L3HATestFramework): diff --git a/neutron/tests/unit/plugins/ml2/drivers/l2pop/test_mech_driver.py b/neutron/tests/unit/plugins/ml2/drivers/l2pop/test_mech_driver.py index b1be2da6f97..b17df1e3d3c 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/l2pop/test_mech_driver.py +++ b/neutron/tests/unit/plugins/ml2/drivers/l2pop/test_mech_driver.py @@ -16,6 +16,8 @@ from unittest import mock from neutron_lib.agent import topics +from neutron_lib.api import attributes +from neutron_lib.api.definitions import network_ha from neutron_lib.api.definitions import port as port_def from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import provider_net as pnet @@ -120,6 +122,10 @@ class TestL2PopulationRpcTestCase(test_plugin.Ml2PluginV2TestCase): uptime = ('neutron.plugins.ml2.drivers.l2pop.db.get_agent_uptime') uptime_patch = mock.patch(uptime, return_value=190) uptime_patch.start() + # Extend network HA extension. + rname = network_ha.COLLECTION_NAME + attributes.RESOURCES[rname].update( + network_ha.RESOURCE_ATTRIBUTE_MAP[rname]) def _setup_l3(self): notif_p = mock.patch.object(l3_hamode_db.L3_HA_NAT_db_mixin, diff --git a/neutron/tests/unit/scheduler/test_l3_agent_scheduler.py b/neutron/tests/unit/scheduler/test_l3_agent_scheduler.py index db41501ef96..b729e0f78b3 100644 --- a/neutron/tests/unit/scheduler/test_l3_agent_scheduler.py +++ b/neutron/tests/unit/scheduler/test_l3_agent_scheduler.py @@ -18,7 +18,9 @@ import contextlib import datetime from unittest import mock +from neutron_lib.api import attributes from neutron_lib.api.definitions import l3_ext_ha_mode +from neutron_lib.api.definitions import network_ha from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import router_availability_zone from neutron_lib.callbacks import events @@ -1455,6 +1457,10 @@ class L3HATestCaseMixin(testlib_api.SqlTestCase, self.mock_make_res = make_res.start() commit_res = mock.patch.object(quota.QuotaEngine, 'commit_reservation') self.mock_quota_commit_res = commit_res.start() + # Extend network HA extension. + rname = network_ha.COLLECTION_NAME + attributes.RESOURCES[rname].update( + network_ha.RESOURCE_ATTRIBUTE_MAP[rname]) @staticmethod def get_router_l3_agent_binding(context, router_id, l3_agent_id=None, @@ -2113,6 +2119,10 @@ class L3AgentAZLeastRoutersSchedulerTestCase(L3HATestCaseMixin): self.patch_notifier = mock.patch( 'neutron.notifiers.batch_notifier.BatchNotifier._notify') self.patch_notifier.start() + # Extend network HA extension. + rname = network_ha.COLLECTION_NAME + attributes.RESOURCES[rname].update( + network_ha.RESOURCE_ATTRIBUTE_MAP[rname]) def _register_l3_agents(self): self.agent1 = helpers.register_l3_agent(host='az1-host1', az='az1') diff --git a/releasenotes/notes/network_ha_extension-99578e7ee47f47db.yaml b/releasenotes/notes/network_ha_extension-99578e7ee47f47db.yaml new file mode 100644 index 00000000000..95e3db43521 --- /dev/null +++ b/releasenotes/notes/network_ha_extension-99578e7ee47f47db.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + A new API extension ``network-ha`` has been added. This extension adds a + new field to the network API: "ha". This field is not visible and can be + passed only in POST (create) operations. That will define that this network + is a high availability (HA) network and will create, in the same database + transaction, a ``ha_router_networks`` register. diff --git a/requirements.txt b/requirements.txt index 7e208c38f39..cb72debe32d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ Jinja2>=2.10 # BSD License (3 clause) keystonemiddleware>=5.1.0 # Apache-2.0 netaddr>=0.7.18 # BSD netifaces>=0.10.4 # MIT -neutron-lib>=3.6.1 # Apache-2.0 +neutron-lib>=3.7.0 # Apache-2.0 python-neutronclient>=7.8.0 # Apache-2.0 tenacity>=6.0.0 # Apache-2.0 SQLAlchemy>=1.4.23 # MIT