Use the new network HA parameter

This patch implements the new network HA boolean field API extension.
This field is an input only parameter for POST operations (creation).
By default is "False". When enabled, the Neutron server will create
a ``ha_router_networks`` register in the same transaction of the
network creation.

If by any circumstance (a race condition, for example), another
``ha_router_networks`` exists in the same project, a
``DBDuplicateEntry`` exception will be raised and the transaction
will be rolled back.

Partial-Bug: #2016198
Change-Id: Ie42c13ecbe4abcad9229b71f6942e393fd0f2e4e
This commit is contained in:
Rodolfo Alonso Hernandez 2023-04-27 15:36:13 +00:00 committed by Rodolfo Alonso
parent d1bfc3d70a
commit 4109ee9bb4
10 changed files with 138 additions and 43 deletions

View File

@ -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 logging
from neutron_lib.api.definitions import multiprovidernet from neutron_lib.api.definitions import multiprovidernet
from neutron_lib.api.definitions import network_availability_zone 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_ip_availability
from neutron_lib.api.definitions import network_mtu from neutron_lib.api.definitions import network_mtu
from neutron_lib.api.definitions import network_mtu_writable from neutron_lib.api.definitions import network_mtu_writable
@ -122,6 +123,7 @@ ML2_SUPPORTED_API_EXTENSIONS = [
extra_dhcp_opt.ALIAS, extra_dhcp_opt.ALIAS,
filter_validation.ALIAS, filter_validation.ALIAS,
multiprovidernet.ALIAS, multiprovidernet.ALIAS,
network_ha.ALIAS,
network_mtu.ALIAS, network_mtu.ALIAS,
network_mtu_writable.ALIAS, network_mtu_writable.ALIAS,
network_availability_zone.ALIAS, network_availability_zone.ALIAS,

View File

@ -19,6 +19,7 @@ import random
import netaddr import netaddr
from neutron_lib.api.definitions import l3 as l3_apidef 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 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 port as port_def
from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net as providernet 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() 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 @registry.has_registry_receivers
class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin, class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin,
router_az_db.RouterAvailabilityZoneMixin): 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, return p_utils.create_subnet(self._core_plugin, context,
{'subnet': args}) {'subnet': args})
def _create_ha_network_tenant_binding(self, context, tenant_id, @registry.receives(resources.NETWORK, [events.PRECOMMIT_CREATE])
network_id): def _create_ha_network_tenant_binding(self, resource, event, trigger,
ha_network = l3_hamode.L3HARouterNetwork( payload=None):
context, project_id=tenant_id, network_id=network_id) if not payload.request_body.get(network_ha_apidef.HA):
ha_network.create() return
# we need to check if someone else just inserted at exactly the
# same time as us because there is no constrain in L3HARouterNetwork network = payload.latest_state
# that prevents multiple networks per tenant context = payload.context
if l3_hamode.L3HARouterNetwork.count( ha_network = l3_hamode.L3HARouterNetwork(payload.context,
context, project_id=tenant_id) > 1: project_id=context.project_id,
# we need to throw an error so our network is deleted network_id=network['id'])
# and the process is started over where the existing try:
# network will be selected. ha_network.create()
raise db_exc.DBDuplicateEntry(columns=['tenant_id']) except obj_base.NeutronDbObjectDuplicateEntry:
return None, ha_network raise DuplicatedHANetwork(project_id=context.project_id)
def _add_ha_network_settings(self, network): def _add_ha_network_settings(self, network):
if cfg.CONF.l3_ha_network_type: 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): def _create_ha_network(self, context, tenant_id):
admin_ctx = context.elevated() 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': args = {'network':
{'name': constants.HA_NETWORK_NAME % tenant_id, {'name': constants.HA_NETWORK_NAME % tenant_id,
'tenant_id': '', 'tenant_id': '',
'shared': False, 'shared': False,
'admin_state_up': True}} 'admin_state_up': True,
network_ha_apidef.HA: True,
}}
self._add_ha_network_settings(args['network']) self._add_ha_network_settings(args['network'])
creation = functools.partial(p_utils.create_network, network = p_utils.create_network(self._core_plugin, admin_ctx, args)
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)
try: try:
self._create_ha_subnet(admin_ctx, network['id'], tenant_id) self._create_ha_subnet(admin_ctx, network['id'], tenant_id)
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
self._core_plugin.delete_network(admin_ctx, network['id']) 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): def get_number_of_agents_for_scheduling(self, context):
"""Return number of agents on which the router will be scheduled.""" """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 # ensure the HA network exists before we start router creation so
# we can provide meaningful errors back to the user if no network # we can provide meaningful errors back to the user if no network
# can be allocated # can be allocated
if not self.get_ha_network(context, router['tenant_id']): # TODO(ralonsoh): remove once bp/keystone-v3 migration finishes.
self._create_ha_network(context, router['tenant_id']) 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], @registry.receives(resources.ROUTER, [events.PRECOMMIT_CREATE],
priority_group.PRIORITY_ROUTER_EXTENDED_ATTRIBUTE) priority_group.PRIORITY_ROUTER_EXTENDED_ATTRIBUTE)

View File

@ -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

View File

@ -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_ext_ha_mode
from neutron_lib.api.definitions import l3_flavors 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 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 qos_gateway_ip
from neutron_lib.api.definitions import \ from neutron_lib.api.definitions import \
router_admin_state_down_before_update as r_admin_state_down_before_update 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, floatingip_pools.ALIAS,
qos_gateway_ip.ALIAS, qos_gateway_ip.ALIAS,
l3_port_ip_change_not_allowed.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_pagination_support = True
__native_sorting_support = True __native_sorting_support = True

View File

@ -16,6 +16,8 @@
import collections import collections
import random 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 constants
from neutron_lib import context from neutron_lib import context
from neutron_lib.plugins import constants as plugin_constants 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) directory.add_plugin(plugin_constants.L3, self.l3_plugin)
self.adminContext = context.get_admin_context() self.adminContext = context.get_admin_context()
self.adminContext.tenant_id = '_func_test_tenant_' 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, def _create_l3_agent(self, host, context, agent_mode='legacy', plugin=None,
state=True, az='nova'): state=True, az='nova'):

View File

@ -14,10 +14,12 @@
from unittest import mock 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 dvr as dvr_apidef
from neutron_lib.api.definitions import external_net as extnet_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 as l3_apidef
from neutron_lib.api.definitions import l3_ext_ha_mode 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 port as port_def
from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net as providernet 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 import exceptions as n_exc
from neutron_lib.exceptions import l3 as l3_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.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 constants as plugin_constants
from neutron_lib.plugins import directory from neutron_lib.plugins import directory
from oslo_config import cfg from oslo_config import cfg
@ -84,6 +85,10 @@ class L3HATestFramework(testlib_api.SqlTestCase):
self.agent1 = helpers.register_l3_agent() self.agent1 = helpers.register_l3_agent()
self.agent2 = helpers.register_l3_agent( self.agent2 = helpers.register_l3_agent(
'host_2', constants.L3_AGENT_MODE_DVR_SNAT) '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 @property
def admin_ctx(self): def admin_ctx(self):
@ -630,7 +635,8 @@ class L3HATestCase(L3HATestFramework):
with mock.patch.object(l3_hamode, 'L3HARouterNetwork', with mock.patch.object(l3_hamode, 'L3HARouterNetwork',
side_effect=ValueError): 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()) self.admin_ctx, _uuid())
networks_after = self.core_plugin.get_networks(self.admin_ctx) networks_after = self.core_plugin.get_networks(self.admin_ctx)
@ -682,15 +688,24 @@ class L3HATestCase(L3HATestFramework):
self.admin_ctx, binding.port_id) self.admin_ctx, binding.port_id)
def test_create_ha_network_tenant_binding_raises_duplicate(self): def test_create_ha_network_tenant_binding_raises_duplicate(self):
router = self._create_router() # The router creation calls first the HA network creation and the
network = self.plugin.get_ha_network(self.admin_ctx, # HA network-tenant binding ("ha_router_networks" register)
router['tenant_id']) project_id = uuidutils.generate_uuid()
self.plugin._create_ha_network_tenant_binding( self._create_router(tenant_id=project_id)
self.admin_ctx, 't1', network['network_id']) network = self.core_plugin.get_networks(self.admin_ctx)[0]
with testtools.ExpectedException( ha_network = self.plugin.get_ha_network(self.admin_ctx, project_id)
exceptions.NeutronDbObjectDuplicateEntry): 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.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): def test_create_router_db_vr_id_allocation_goes_to_error(self):
for method in ('_ensure_vr_id', for method in ('_ensure_vr_id',
@ -956,12 +971,15 @@ class L3HATestCase(L3HATestFramework):
class L3HAModeDbTestCase(L3HATestFramework): class L3HAModeDbTestCase(L3HATestFramework):
def _create_network(self, plugin, ctx, name='net', def _create_network(self, plugin, ctx, name='net',
tenant_id='tenant1', external=False): tenant_id='tenant1', external=False, ha=False):
network = {'network': {'name': name, network = {'network': {'name': name,
'shared': False, 'shared': False,
'admin_state_up': True, 'admin_state_up': True,
'tenant_id': tenant_id, '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'] return plugin.create_network(ctx, network)['id']
def _create_subnet(self, plugin, ctx, network_id, cidr='10.0.0.0/8', 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']]) router_ids=[router['id']])
self.assertEqual(self.agent2['host'], routers[0]['gw_port_host']) 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): class L3HAUserTestCase(L3HATestFramework):

View File

@ -16,6 +16,8 @@
from unittest import mock from unittest import mock
from neutron_lib.agent import topics 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 port as port_def
from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net as pnet 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 = ('neutron.plugins.ml2.drivers.l2pop.db.get_agent_uptime')
uptime_patch = mock.patch(uptime, return_value=190) uptime_patch = mock.patch(uptime, return_value=190)
uptime_patch.start() 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): def _setup_l3(self):
notif_p = mock.patch.object(l3_hamode_db.L3_HA_NAT_db_mixin, notif_p = mock.patch.object(l3_hamode_db.L3_HA_NAT_db_mixin,

View File

@ -18,7 +18,9 @@ import contextlib
import datetime import datetime
from unittest import mock 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 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 portbindings
from neutron_lib.api.definitions import router_availability_zone from neutron_lib.api.definitions import router_availability_zone
from neutron_lib.callbacks import events from neutron_lib.callbacks import events
@ -1455,6 +1457,10 @@ class L3HATestCaseMixin(testlib_api.SqlTestCase,
self.mock_make_res = make_res.start() self.mock_make_res = make_res.start()
commit_res = mock.patch.object(quota.QuotaEngine, 'commit_reservation') commit_res = mock.patch.object(quota.QuotaEngine, 'commit_reservation')
self.mock_quota_commit_res = commit_res.start() 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 @staticmethod
def get_router_l3_agent_binding(context, router_id, l3_agent_id=None, 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( self.patch_notifier = mock.patch(
'neutron.notifiers.batch_notifier.BatchNotifier._notify') 'neutron.notifiers.batch_notifier.BatchNotifier._notify')
self.patch_notifier.start() 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): def _register_l3_agents(self):
self.agent1 = helpers.register_l3_agent(host='az1-host1', az='az1') self.agent1 = helpers.register_l3_agent(host='az1-host1', az='az1')

View File

@ -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.

View File

@ -20,7 +20,7 @@ Jinja2>=2.10 # BSD License (3 clause)
keystonemiddleware>=5.1.0 # Apache-2.0 keystonemiddleware>=5.1.0 # Apache-2.0
netaddr>=0.7.18 # BSD netaddr>=0.7.18 # BSD
netifaces>=0.10.4 # MIT 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 python-neutronclient>=7.8.0 # Apache-2.0
tenacity>=6.0.0 # Apache-2.0 tenacity>=6.0.0 # Apache-2.0
SQLAlchemy>=1.4.23 # MIT SQLAlchemy>=1.4.23 # MIT