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:
parent
d1bfc3d70a
commit
4109ee9bb4
neutron
common/ovn
db
extensions
services/l3_router
tests
functional/scheduler
unit
db
plugins/ml2/drivers/l2pop
scheduler
releasenotes/notes
requirements.txt@ -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,
|
||||
|
@ -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)
|
||||
|
21
neutron/extensions/network_ha.py
Normal file
21
neutron/extensions/network_ha.py
Normal 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
|
@ -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
|
||||
|
@ -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'):
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
@ -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.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user