Add unique constraint for network segment ranges
The network segment range initialization is performed in each Neutron API worker during the initialization transient period which may create multiple instances. This patch add uniques constraints to avoid it. Depends-On: https://review.opendev.org/c/openstack/neutron-tempest-plugin/+/961361 Closes-Bug: #2106463 Related-Bug: #2122107 Signed-off-by: LIU Yulong <i@liuyulong.me> Co-authored-by: Rodolfo Alonso Hernandez <ralonsoh@redhat.com> Change-Id: I061b904506af3c2187b3d057f91de7c9a547e712
This commit is contained in:
committed by
Rodolfo Alonso Hernandez
parent
aab1d698dc
commit
4f1e351169
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
@@ -1 +1 @@
|
||||
d553edeb540f
|
||||
b1bca967e19d
|
||||
|
||||
@@ -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 "<NetworkSegmentRange({},{},{},{},{},{} - {},{})>".format(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'])
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
|
||||
118
neutron/tests/functional/plugins/ml2/drivers/test_type_vlan.py
Normal file
118
neutron/tests/functional/plugins/ml2/drivers/test_type_vlan.py
Normal file
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user