diff --git a/neutron/common/wsgi_utils.py b/neutron/common/wsgi_utils.py index ac3f6a48d26..07310bb483b 100644 --- a/neutron/common/wsgi_utils.py +++ b/neutron/common/wsgi_utils.py @@ -18,7 +18,7 @@ from oslo_utils import timeutils from neutron.common import utils -FIRST_WORKER_ID = 1 +FIRST_WORKER_ID = None def get_start_time(default=None, current_time=False): diff --git a/neutron/db/migration/alembic_migrations/versions/2026.1/expand/b1bca967e19d_add_unique_network_segment_range.py b/neutron/db/migration/alembic_migrations/versions/2026.1/expand/b1bca967e19d_add_unique_network_segment_range.py new file mode 100644 index 00000000000..4e2d54c3727 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/2026.1/expand/b1bca967e19d_add_unique_network_segment_range.py @@ -0,0 +1,111 @@ +# Copyright 2025 OpenStack Foundation +# +# 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. +# + +"""Add unique constraint to the network segment range + +Revision ID: b1bca967e19d +Revises: ad80a9f07c5c +Create Date: 2025-04-08 11:28:47.791807 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b1bca967e19d' +down_revision = 'd553edeb540f' + +network_segment_range_network_type = sa.Enum( + 'vlan', 'vxlan', 'gre', 'geneve', + name='network_segment_range_network_type') + +TABLE_NAME = 'network_segment_ranges' +network_segment_range_table = sa.Table( + TABLE_NAME, sa.MetaData(), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('default', sa.Boolean(), nullable=False), + sa.Column('shared', sa.Boolean(), nullable=False), + sa.Column('project_id', sa.String(length=255), nullable=True), + sa.Column('network_type', network_segment_range_network_type, + nullable=False), + sa.Column('physical_network', sa.String(length=64), nullable=False, + server_default=''), + sa.Column('minimum', sa.Integer(), nullable=True), + sa.Column('maximum', sa.Integer(), nullable=True), + sa.Column('standard_attr_id', sa.BigInteger(), nullable=False), +) + + +def upgrade(): + unique_name = 'uniq_network_segment_ranges' + unique_columns = ['default', + 'network_type', + 'physical_network', + 'minimum', + 'maximum', + ] + + inspect = sa.engine.reflection.Inspector.from_engine(op.get_bind()) + unique_constraints = inspect.get_unique_constraints(TABLE_NAME) + for unique_constraint in unique_constraints: + if unique_constraint['name'] == unique_name: + # The unique constraint already exists. + return + + migrate_values() + clear_duplicate_values() + op.alter_column(TABLE_NAME, 'physical_network', nullable=False, + server_default='', existing_type=sa.String(64)) + op.create_unique_constraint( + columns=unique_columns, + constraint_name=unique_name, + table_name=TABLE_NAME) + + +def clear_duplicate_values(): + session = sa.orm.Session(bind=op.get_bind()) + values = [] + for row in session.query(network_segment_range_table): + id = row[0] + item = {'default': row[2], + 'network_type': row[5], + 'physical_network': row[6], + 'minimum': row[7], + 'maximum': row[8]} + if item not in values: + values.append(item) + else: + session.execute( + network_segment_range_table.delete().where( + network_segment_range_table.c.id == id)) + session.commit() + + +def migrate_values(): + session = sa.orm.Session(bind=op.get_bind()) + values = [] + for row in session.query(network_segment_range_table): + values.append({'id': row[0], + 'physical_network': row[6]}) + for value in values: + physical_network = value['physical_network'] or '' + session.execute( + network_segment_range_table.update().values( + physical_network=physical_network + ).where(network_segment_range_table.c.id == value['id'])) + session.commit() diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 59616ce421e..cc87f6e9465 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -d553edeb540f +b1bca967e19d diff --git a/neutron/db/migration/alembic_migrations/versions/__init__.py b/neutron/db/migration/alembic_migrations/versions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/db/models/network_segment_range.py b/neutron/db/models/network_segment_range.py index fb261de3052..c416fdd6c63 100644 --- a/neutron/db/models/network_segment_range.py +++ b/neutron/db/models/network_segment_range.py @@ -52,7 +52,8 @@ class NetworkSegmentRange(standard_attr.HasStandardAttributes, nullable=False) # network segment range physical network, only applicable for VLAN. - physical_network = sa.Column(sa.String(64)) + physical_network = sa.Column(sa.String(64), nullable=False, + server_default='') # minimum segmentation id value minimum = sa.Column(sa.Integer) @@ -65,11 +66,18 @@ class NetworkSegmentRange(standard_attr.HasStandardAttributes, range_apidef.COLLECTION_NAME: range_apidef.RESOURCE_NAME} tag_support = True + __table_args__ = ( + sa.UniqueConstraint('default', 'network_type', + 'physical_network', + 'minimum', 'maximum', + name='uniq_network_segment_ranges'), + ) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.project_id = None if self.shared else kwargs['project_id'] is_vlan = self.network_type == constants.TYPE_VLAN - self.physical_network = kwargs['physical_network'] if is_vlan else None + self.physical_network = kwargs['physical_network'] if is_vlan else '' def __repr__(self): return "".format( diff --git a/neutron/db/segments_db.py b/neutron/db/segments_db.py index 427518db8be..1de59ed36cd 100644 --- a/neutron/db/segments_db.py +++ b/neutron/db/segments_db.py @@ -109,6 +109,10 @@ def get_segment_by_id(context, segment_id): def get_dynamic_segment(context, network_id, physical_network=None, segmentation_id=None): """Return a dynamic segment for the filters provided if one exists.""" + # Network segments have physical_network=None in tunnelled networks, unlike + # network segment ranges, that have an empty string in order to force the + # database constraint. + physical_network = physical_network or None with db_api.CONTEXT_READER.using(context): filters = { 'network_id': network_id, @@ -142,6 +146,10 @@ def delete_network_segment(context, segment_id): def network_segments_exist_in_range(context, network_type, physical_network, segment_range=None): """Check whether one or more network segments exist in a range.""" + # Network segments have physical_network=None in tunnelled networks, unlike + # network segment ranges, that have an empty string in order to force the + # database constraint. + physical_network = physical_network or None with db_api.CONTEXT_READER.using(context): filters = { 'network_type': network_type, @@ -163,6 +171,10 @@ def min_max_actual_segments_in_range(context, network_type, physical_network, """Return the minimum and maximum segmentation IDs used in a network segment range """ + # Network segments have physical_network=None in tunnelled networks, unlike + # network segment ranges, that have an empty string in order to force the + # database constraint. + physical_network = physical_network or None with db_api.CONTEXT_READER.using(context): filters = { 'network_type': network_type, diff --git a/neutron/objects/network_segment_range.py b/neutron/objects/network_segment_range.py index 233bb4d354c..7fb07e28c28 100644 --- a/neutron/objects/network_segment_range.py +++ b/neutron/objects/network_segment_range.py @@ -54,7 +54,9 @@ models_map = { @base.NeutronObjectRegistry.register class NetworkSegmentRange(base.NeutronDbObject): # Version 1.0: Initial version - VERSION = '1.0' + # Version 1.1: Add unique constraint and make 'physical_network' not + # nullable + VERSION = '1.1' db_model = range_model.NetworkSegmentRange @@ -68,7 +70,7 @@ class NetworkSegmentRange(base.NeutronDbObject): 'project_id': obj_fields.StringField(nullable=True), 'network_type': common_types.NetworkSegmentRangeNetworkTypeEnumField( nullable=False), - 'physical_network': obj_fields.StringField(nullable=True), + 'physical_network': obj_fields.StringField(nullable=False), 'minimum': obj_fields.IntegerField(nullable=True), 'maximum': obj_fields.IntegerField(nullable=True) } @@ -131,6 +133,7 @@ class NetworkSegmentRange(base.NeutronDbObject): return [segmentation_id for (segmentation_id,) in alloc_available] def _get_used_allocation_mapping(self): + phys_net = self.physical_network or None with self.db_context_reader(self.obj_context): query = self.obj_context.session.query( segments_model.NetworkSegment.segmentation_id, @@ -138,8 +141,7 @@ class NetworkSegmentRange(base.NeutronDbObject): alloc_used = (query.filter(and_( segments_model.NetworkSegment.network_type == self.network_type, - segments_model.NetworkSegment.physical_network == - self.physical_network, + segments_model.NetworkSegment.physical_network == phys_net, segments_model.NetworkSegment.segmentation_id >= self.minimum, segments_model.NetworkSegment.segmentation_id <= self.maximum)) .filter(segments_model.NetworkSegment.network_id == @@ -253,6 +255,7 @@ class NetworkSegmentRange(base.NeutronDbObject): @classmethod def new_default(cls, context, network_type, physical_network, minimum, maximum, start_time): + physical_network = physical_network or '' model = models_map.get(network_type) if not model: msg = (_("network_type '%s' unknown for getting allocation " diff --git a/neutron/plugins/ml2/drivers/type_tunnel.py b/neutron/plugins/ml2/drivers/type_tunnel.py index f61e346a3e9..4855591a2d3 100644 --- a/neutron/plugins/ml2/drivers/type_tunnel.py +++ b/neutron/plugins/ml2/drivers/type_tunnel.py @@ -22,6 +22,7 @@ from neutron_lib import constants as p_const from neutron_lib import context from neutron_lib.db import api as db_api from neutron_lib import exceptions as exc +from neutron_lib.objects import exceptions as o_exc from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from neutron_lib.plugins.ml2 import api @@ -144,7 +145,6 @@ class _TunnelTypeDriverBase(helpers.SegmentTypeDriver, metaclass=abc.ABCMeta): LOG.info("%(type)s ID ranges: %(range)s", {'type': self.get_type(), 'range': current_range}) - @db_api.retry_db_errors def _populate_new_default_network_segment_ranges(self, ctx, start_time): for tun_min, tun_max in self._tunnel_ranges: range_obj.NetworkSegmentRange.new_default( @@ -165,18 +165,22 @@ class _TunnelTypeDriverBase(helpers.SegmentTypeDriver, metaclass=abc.ABCMeta): @db_api.retry_db_errors def initialize_network_segment_range_support(self, start_time): admin_context = context.get_admin_context() - with db_api.CONTEXT_WRITER.using(admin_context): - self._delete_expired_default_network_segment_ranges( - admin_context, start_time) - self._populate_new_default_network_segment_ranges( - admin_context, start_time) - # Override self.tunnel_ranges with the network segment range - # information from DB and then do a sync_allocations since the - # segment range service plugin has not yet been loaded at this - # initialization time. - self._tunnel_ranges = self._get_network_segment_ranges_from_db( - ctx=admin_context) - self._sync_allocations(ctx=admin_context) + try: + with db_api.CONTEXT_WRITER.using(admin_context): + self._delete_expired_default_network_segment_ranges( + admin_context, start_time) + self._populate_new_default_network_segment_ranges( + admin_context, start_time) + except o_exc.NeutronDbObjectDuplicateEntry: + pass + + # Override self.tunnel_ranges with the network segment range + # information from DB and then do a sync_allocations since the + # segment range service plugin has not yet been loaded at this + # initialization time. + self._tunnel_ranges = self._get_network_segment_ranges_from_db( + ctx=admin_context) + self._sync_allocations(ctx=admin_context) def update_network_segment_range_allocations(self): self._sync_allocations() diff --git a/neutron/plugins/ml2/drivers/type_vlan.py b/neutron/plugins/ml2/drivers/type_vlan.py index aca5c6d67a0..e65d05e940e 100644 --- a/neutron/plugins/ml2/drivers/type_vlan.py +++ b/neutron/plugins/ml2/drivers/type_vlan.py @@ -20,6 +20,7 @@ from neutron_lib import constants as p_const from neutron_lib import context from neutron_lib.db import api as db_api from neutron_lib import exceptions as exc +from neutron_lib.objects import exceptions as o_exc from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from neutron_lib.plugins.ml2 import api @@ -171,18 +172,22 @@ class VlanTypeDriver(helpers.SegmentTypeDriver): @db_api.retry_db_errors def initialize_network_segment_range_support(self, start_time): admin_context = context.get_admin_context() - with db_api.CONTEXT_WRITER.using(admin_context): - self._delete_expired_default_network_segment_ranges( - admin_context, start_time) - self._populate_new_default_network_segment_ranges( - admin_context, start_time) - # Override self._network_vlan_ranges with the network segment range - # information from DB and then do a sync_allocations since the - # segment range service plugin has not yet been loaded at this - # initialization time. - self._network_vlan_ranges = ( - self._get_network_segment_ranges_from_db(ctx=admin_context)) - self._sync_vlan_allocations(ctx=admin_context) + try: + with db_api.CONTEXT_WRITER.using(admin_context): + self._delete_expired_default_network_segment_ranges( + admin_context, start_time) + self._populate_new_default_network_segment_ranges( + admin_context, start_time) + except o_exc.NeutronDbObjectDuplicateEntry: + pass + + # Override self._network_vlan_ranges with the network segment range + # information from DB and then do a sync_allocations since the + # segment range service plugin has not yet been loaded at this + # initialization time. + self._network_vlan_ranges = ( + self._get_network_segment_ranges_from_db(ctx=admin_context)) + self._sync_vlan_allocations(ctx=admin_context) def update_network_segment_range_allocations(self): self._sync_vlan_allocations() diff --git a/neutron/services/network_segment_range/plugin.py b/neutron/services/network_segment_range/plugin.py index d028a2a3085..d7b3e7b0e3e 100644 --- a/neutron/services/network_segment_range/plugin.py +++ b/neutron/services/network_segment_range/plugin.py @@ -165,7 +165,7 @@ class NetworkSegmentRangePlugin(ext_range.NetworkSegmentRangePluginBase): network_type=range_data['network_type'], physical_network=(range_data['physical_network'] if range_data['network_type'] == - const.TYPE_VLAN else None), + const.TYPE_VLAN else ''), minimum=range_data['minimum'], maximum=range_data['maximum']) ) diff --git a/neutron/tests/functional/db/migrations/test_b1bca967e19d_add_unique_network_segment_range.py b/neutron/tests/functional/db/migrations/test_b1bca967e19d_add_unique_network_segment_range.py new file mode 100644 index 00000000000..7cf81200b07 --- /dev/null +++ b/neutron/tests/functional/db/migrations/test_b1bca967e19d_add_unique_network_segment_range.py @@ -0,0 +1,106 @@ +# 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. + +import random + +from oslo_db.sqlalchemy import utils as db_utils +from oslo_utils import uuidutils + +from neutron.tests.functional.db import test_migrations + + +class NetworkSegmentRangesUniqueUpgrade(test_migrations.TestWalkMigrations): + """Validates migration that adds unique constraints for + network segment ranges. + """ + + _standard_attribute_id = 0 + + def _gen_attr_id(self, type): + self._standard_attribute_id = random.randint(100000, 2000000) + standardattributes = db_utils.get_table( + self.engine, 'standardattributes') + with self.engine.connect() as conn, conn.begin(): + conn.execute(standardattributes.insert().values({ + 'id': self._standard_attribute_id, + 'resource_type': type})) + return self._standard_attribute_id + + def _create_network_segment_ranges(self, data): + network_segment_ranges = db_utils.get_table( + self.engine, 'network_segment_ranges') + with self.engine.connect() as conn, conn.begin(): + for item in data: + range_dict = { + 'id': uuidutils.generate_uuid(), + 'standard_attr_id': self._gen_attr_id( + 'network_segment_ranges'), + } + range_dict.update(**item) + conn.execute( + network_segment_ranges.insert().values(range_dict)) + + def _pre_upgrade_b1bca967e19d(self, engine): + duplicate_data = [ + { + 'name': '', + 'default': True, + 'shared': True, + 'network_type': 'vlan', + 'physical_network': 'default', + 'minimum': 100, + 'maximum': 200 + }, + { + 'name': '', + 'default': True, + 'shared': True, + 'network_type': 'vlan', + 'physical_network': 'default', + 'minimum': 100, + 'maximum': 200 + }, + { + 'name': '', + 'default': True, + 'shared': True, + 'network_type': 'vxlan', + 'minimum': 1000, + 'maximum': 2000 + }, + { + 'name': '', + 'default': True, + 'shared': True, + 'network_type': 'vxlan', + 'minimum': 1000, + 'maximum': 2000 + }, + ] + self._create_network_segment_ranges(duplicate_data) + # Ensure there are two duplicate ranges data + range_table = db_utils.get_table(self.engine, 'network_segment_ranges') + with self.engine.connect() as conn, conn.begin(): + rows = conn.execute(range_table.select()).fetchall() + self.assertEqual(4, len(rows)) + return True + + def _check_b1bca967e19d(self, engine, data): + range_table = db_utils.get_table(self.engine, 'network_segment_ranges') + # check duplicate data is deleted + with self.engine.connect() as conn, conn.begin(): + vlan = conn.execute(range_table.select().where( + range_table.c.network_type == 'vlan')).fetchall() + self.assertEqual(1, len(vlan)) + vxlan = conn.execute(range_table.select().where( + range_table.c.network_type == 'vxlan')).fetchall() + self.assertEqual(1, len(vxlan)) diff --git a/neutron/tests/functional/plugins/ml2/drivers/test_type_tunnel.py b/neutron/tests/functional/plugins/ml2/drivers/test_type_tunnel.py index 658f9dfd2db..9ed977936b9 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/test_type_tunnel.py +++ b/neutron/tests/functional/plugins/ml2/drivers/test_type_tunnel.py @@ -14,12 +14,11 @@ # under the License. from concurrent import futures -import random -import time from neutron_lib import constants from neutron_lib import context from neutron_lib.db import api as db_api +from neutron_lib.objects import exceptions as o_exc from oslo_config import cfg from neutron.conf import common as common_config @@ -37,15 +36,18 @@ def _initialize_network_segment_range_support(type_driver, start_time): # creates the new ones. It also adds an extra second before closing the # DB transaction. admin_context = context.get_admin_context() - with db_api.CONTEXT_WRITER.using(admin_context): - time.sleep(random.randrange(1000) / 1000) - type_driver._delete_expired_default_network_segment_ranges( - admin_context, start_time) - type_driver._populate_new_default_network_segment_ranges( - admin_context, start_time) + try: + with db_api.CONTEXT_WRITER.using(admin_context): + type_driver._delete_expired_default_network_segment_ranges( + admin_context, start_time) + type_driver._populate_new_default_network_segment_ranges( + admin_context, start_time) + except o_exc.NeutronDbObjectDuplicateEntry: + pass -class TunnelTypeDriverBaseTestCase(testlib_api.SqlTestCase): +class TunnelTypeDriverBaseTestCase(testlib_api.MySQLTestCaseMixin, + testlib_api.SqlTestCase): def setUp(self): super().setUp() cfg.CONF.register_opts(common_config.core_opts) @@ -62,32 +64,45 @@ class TunnelTypeDriverBaseTestCase(testlib_api.SqlTestCase): self.type_driver = type_geneve.GeneveTypeDriver() self.type_driver.initialize() + def _check_sranges(self, sranges): + self.assertEqual(1, len(sranges)) + self.assertEqual(self.net_type, sranges[0].network_type) + self.assertEqual(self.min, sranges[0].minimum) + self.assertEqual(self.max, sranges[0].maximum) + self.assertEqual([(self.min, self.max)], + self.type_driver._tunnel_ranges) + def test_initialize_network_segment_range_support(self): # Execute the initialization several times with different start times. for start_time in range(3): self.type_driver.initialize_network_segment_range_support( start_time) sranges = range_obj.NetworkSegmentRange.get_objects(self.admin_ctx) - self.assertEqual(1, len(sranges)) - self.assertEqual(self.net_type, sranges[0].network_type) - self.assertEqual(self.min, sranges[0].minimum) - self.assertEqual(self.max, sranges[0].maximum) - self.assertEqual([(self.min, self.max)], - self.type_driver._tunnel_ranges) + self._check_sranges(sranges) - def test_initialize_network_segment_range_support_parallel_execution(self): + def _test_initialize_nsrange(self, same_init_time=True): max_workers = 3 _futures = [] with futures.ThreadPoolExecutor(max_workers=max_workers) as executor: - for idx in range(max_workers): + if same_init_time: + # All workers are started at the same init time. _futures.append(executor.submit( _initialize_network_segment_range_support, - self.type_driver, idx)) + self.type_driver, 0)) + else: + # All workers have different init times. + for idx in range(max_workers): + _futures.append(executor.submit( + _initialize_network_segment_range_support, + self.type_driver, idx)) for _future in _futures: _future.result() sranges = range_obj.NetworkSegmentRange.get_objects(self.admin_ctx) - self.assertEqual(1, len(sranges)) - self.assertEqual(self.net_type, sranges[0].network_type) - self.assertEqual(self.min, sranges[0].minimum) - self.assertEqual(self.max, sranges[0].maximum) + self._check_sranges(sranges) + + def test_initialize_nsrange_support_parallel_exec_same_init_time(self): + self._test_initialize_nsrange(same_init_time=True) + + def test_initialize_nsrange_support_parallel_exec_diff_init_time(self): + self._test_initialize_nsrange(same_init_time=False) diff --git a/neutron/tests/functional/plugins/ml2/drivers/test_type_vlan.py b/neutron/tests/functional/plugins/ml2/drivers/test_type_vlan.py new file mode 100644 index 00000000000..280e28fbb40 --- /dev/null +++ b/neutron/tests/functional/plugins/ml2/drivers/test_type_vlan.py @@ -0,0 +1,118 @@ +# Copyright 2025 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 concurrent import futures + +from neutron_lib import constants +from neutron_lib import context +from neutron_lib.db import api as db_api +from neutron_lib.objects import exceptions as o_exc +from oslo_config import cfg + +from neutron.conf import common as common_config +from neutron.conf.plugins.ml2 import config as ml2_config +from neutron.conf.plugins.ml2.drivers import driver_type as driver_type_config +from neutron.objects import network_segment_range as range_obj +from neutron.plugins.ml2.drivers import type_vlan +from neutron.tests.unit import testlib_api + + +def _initialize_network_segment_range_support(type_driver, start_time): + # This method is similar to + # ``VlanTypeDriverBase.initialize_network_segment_range_support``. + # The method first deletes the existing default network ranges and then + # creates the new ones. It also adds an extra second before closing the + # DB transaction. + admin_context = context.get_admin_context() + try: + with db_api.CONTEXT_WRITER.using(admin_context): + type_driver._delete_expired_default_network_segment_ranges( + admin_context, start_time) + type_driver._populate_new_default_network_segment_ranges( + admin_context, start_time) + except o_exc.NeutronDbObjectDuplicateEntry: + pass + + +class VlanTypeDriverBaseTestCase(testlib_api.MySQLTestCaseMixin, + testlib_api.SqlTestCase): + def setUp(self): + super().setUp() + cfg.CONF.register_opts(common_config.core_opts) + ml2_config.register_ml2_plugin_opts() + driver_type_config.register_ml2_drivers_vlan_opts() + ml2_config.cfg.CONF.set_override( + 'service_plugins', 'network_segment_range') + self.min = 1001 + self.max = 1020 + self.net_type = constants.TYPE_VLAN + self.ranges = [f'phys1:{self.min}:{self.max}', + f'phys2:{self.min}:{self.max}', + f'phys3:{self.min}:{self.max}', + ] + ml2_config.cfg.CONF.set_override( + 'network_vlan_ranges', self.ranges, group='ml2_type_vlan') + self.admin_ctx = context.get_admin_context() + self.type_driver = type_vlan.VlanTypeDriver() + self.type_driver.initialize() + + def _check_sranges(self, sranges): + self.assertEqual(len(self.ranges), len(sranges)) + for _srange in sranges: + self.assertEqual(self.net_type, _srange.network_type) + self.assertEqual(self.min, _srange.minimum) + self.assertEqual(self.max, _srange.maximum) + self.assertIn(_srange.physical_network, + ('phys1', 'phys2', 'phys3')) + + self.assertEqual({'phys1': [(self.min, self.max)], + 'phys2': [(self.min, self.max)], + 'phys3': [(self.min, self.max)]}, + self.type_driver._network_vlan_ranges) + + def test_initialize_network_segment_range_support(self): + # Execute the initialization several times with different start times. + for start_time in range(3): + self.type_driver.initialize_network_segment_range_support( + start_time) + sranges = range_obj.NetworkSegmentRange.get_objects(self.admin_ctx) + self._check_sranges(sranges) + + def _test_initialize_nsrange(self, same_init_time=True): + max_workers = 3 + _futures = [] + with futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + if same_init_time: + # All workers are started at the same init time. + _futures.append(executor.submit( + _initialize_network_segment_range_support, + self.type_driver, 0)) + else: + # All workers have different init times. + for idx in range(max_workers): + _futures.append(executor.submit( + _initialize_network_segment_range_support, + self.type_driver, idx)) + for _future in _futures: + _future.result() + + sranges = range_obj.NetworkSegmentRange.get_objects(self.admin_ctx) + self._check_sranges(sranges) + + def test__initialize_nsrange_support_parallel_exec_same_init_time(self): + self._test_initialize_nsrange(same_init_time=True) + + def test_initialize_nsrange_support_parallel_exec_diff_init_time(self): + self._test_initialize_nsrange(same_init_time=False) diff --git a/neutron/tests/unit/extensions/test_network_segment_range.py b/neutron/tests/unit/extensions/test_network_segment_range.py index 36e3fcbfe05..1d95eb5efae 100644 --- a/neutron/tests/unit/extensions/test_network_segment_range.py +++ b/neutron/tests/unit/extensions/test_network_segment_range.py @@ -171,10 +171,10 @@ class TestNetworkSegmentRange(NetworkSegmentRangeTestBase): expected_range = {'shared': True, 'project_id': None, 'network_type': constants.TYPE_VXLAN, - 'physical_network': None} + 'physical_network': ''} self._test_create_network_segment_range( network_type=constants.TYPE_VXLAN, - physical_network=None, + physical_network='', expected=expected_range) def test_create_network_segment_range_tenant_specific(self): diff --git a/neutron/tests/unit/objects/test_network_segment_range.py b/neutron/tests/unit/objects/test_network_segment_range.py index 54899336569..08e3950a5e2 100644 --- a/neutron/tests/unit/objects/test_network_segment_range.py +++ b/neutron/tests/unit/objects/test_network_segment_range.py @@ -18,6 +18,7 @@ from unittest import mock from neutron_lib import constants from neutron_lib import exceptions as n_exc +from neutron_lib.objects import exceptions as obj_exc from neutron_lib.utils import helpers from oslo_utils import timeutils from oslo_utils import uuidutils @@ -410,23 +411,17 @@ class NetworkSegmentRangeDbObjectTestCase(obj_test_base.BaseDbObjectTestCase, num_ranges = 5 for network_type in network_segment_range.models_map.keys(): for idx in range(num_ranges): - obj = self._create_network_segment_range( - 1, 10, network_type=network_type, default=True, - shared=True, start_time=start_time - idx) - obj.create() - ranges = network_segment_range.NetworkSegmentRange.get_objects( - self.context, default=True, shared=True, - network_type=network_type) - self.assertEqual(num_ranges, len(ranges)) - - network_segment_range.NetworkSegmentRange.\ - delete_expired_default_network_segment_ranges( - self.context, network_type, start_time) - # NOTE(ralonsoh): there should be just one that has the same - # "created_at" value as "start_time". + try: + obj = self._create_network_segment_range( + 1, 10, network_type=network_type, default=True, + shared=True, start_time=start_time - idx) + obj.create() + except obj_exc.NeutronDbObjectDuplicateEntry: + pass ranges = network_segment_range.NetworkSegmentRange.get_objects( self.context, default=True, shared=True, network_type=network_type) + # No duplicated entry in DB at all self.assertEqual(1, len(ranges)) def test_new_default(self): diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 6b392d590e7..f0a3e1b70eb 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -71,7 +71,7 @@ object_data = { 'NetworkPortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3', 'NetworkRBAC': '1.3-be82ed54376b85ee4f963d479ac48c91', 'NetworkSegment': '1.0-57b7f2960971e3b95ded20cbc59244a8', - 'NetworkSegmentRange': '1.0-bdec1fffc9058ea676089b1f2f2b3cf3', + 'NetworkSegmentRange': '1.1-ed71c4bd2d3f06c3da5b4a1b3069b69f', 'NetworkSubnetLock': '1.0-140de39d4b86ae346dc3d70b885bea53', 'Port': '1.10-ae84f686bfc3deb4017495134da6ef04', 'PortHardwareOffloadType': '1.0-5f424d02b144fd1832ac3e6b03662674', diff --git a/neutron/tests/unit/plugins/ml2/drivers/base_type_tunnel.py b/neutron/tests/unit/plugins/ml2/drivers/base_type_tunnel.py index 6def4d742ef..10fb3500c9c 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/base_type_tunnel.py +++ b/neutron/tests/unit/plugins/ml2/drivers/base_type_tunnel.py @@ -496,7 +496,7 @@ class TunnelTypeNetworkSegmentRangeTestMixin: self.assertIsNone(network_segment_range.project_id) self.assertEqual(self.driver.get_type(), network_segment_range.network_type) - self.assertIsNone(network_segment_range.physical_network) + self.assertEqual('', network_segment_range.physical_network) self.assertEqual(TUN_MIN, network_segment_range.minimum) self.assertEqual(TUN_MAX, network_segment_range.maximum) @@ -508,3 +508,15 @@ class TunnelTypeNetworkSegmentRangeTestMixin: ret = obj_network_segment_range.NetworkSegmentRange.get_objects( self.context, network_type=self.driver.get_type()) self.assertEqual(0, len(ret)) + + def test_try_to_create_duplicate_network_segment_ranges(self): + self.driver.initialize_network_segment_range_support(self.start_time) + ret = obj_network_segment_range.NetworkSegmentRange.get_objects( + self.context) + self.assertEqual(1, len(ret)) + + self.driver._populate_new_default_network_segment_ranges( + self.context, self.start_time) + ret = obj_network_segment_range.NetworkSegmentRange.get_objects( + self.context) + self.assertEqual(1, len(ret)) diff --git a/neutron/tests/unit/plugins/ml2/drivers/test_type_vlan.py b/neutron/tests/unit/plugins/ml2/drivers/test_type_vlan.py index 42045dc0d88..60b0f2fee86 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/test_type_vlan.py +++ b/neutron/tests/unit/plugins/ml2/drivers/test_type_vlan.py @@ -410,3 +410,15 @@ class VlanTypeTestWithNetworkSegmentRange(testlib_api.SqlTestCase): ret = obj_network_segment_range.NetworkSegmentRange.get_objects( self.context, network_type=self.driver.get_type()) self.assertEqual(0, len(ret)) + + def test_try_to_create_duplicate_network_segment_ranges(self): + self.driver.initialize_network_segment_range_support(self.start_time) + ret = obj_network_segment_range.NetworkSegmentRange.get_objects( + self.context) + self.assertEqual(2, len(ret)) + + self.driver._populate_new_default_network_segment_ranges( + self.context, self.start_time) + ret = obj_network_segment_range.NetworkSegmentRange.get_objects( + self.context) + self.assertEqual(2, len(ret))