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

View File

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

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

View File

@ -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'):

View File

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

View File

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

View File

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

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