Synchronize the network segment range initialization

The VLAN and tunnelled network segment range initialization is performed
in each Neutron API worker during the initialization transient period.
This patch adds the following optimizations:
* The ``network_segment_ranges`` expired register deletion is based on
  the Neutron API start time, that is unique for all workers. The new
  registers created by other API workers won't be deleted.
* A new method ``NetworkSegmentRange.new_default`` is used to create
  the new default registers that is process-safe, using the database
  transactional engine.

Closes-Bug: #2086602
Change-Id: Id1551dcd786202384393556e15e91de72aa7272e
This commit is contained in:
Rodolfo Alonso Hernandez
2025-01-02 11:18:52 +00:00
parent 2ef2311593
commit d3bc0b920b
13 changed files with 169 additions and 58 deletions

View File

@@ -1125,3 +1125,8 @@ def read_file(path: str) -> str:
def ts_to_datetime(timestamp):
"""Converts timestamp (in seconds) to datetime"""
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
def datetime_to_ts(_datetime):
"""Converts datetime to timestamp in seconds"""
return int(datetime.datetime.timestamp(_datetime))

View File

@@ -13,20 +13,34 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_utils import timeutils
def get_start_time():
from neutron.common import utils
def get_start_time(default=None, current_time=False):
"""Return the 'start-time=%t' config varible in the WSGI config
This variable contains the start time of the WSGI server. Check
https://uwsgi-docs.readthedocs.io/en/latest/Configuration.html
#magic-variables
:param default: (int or float) in case the uwsgi option 'start-time' is not
available or the uwsgi module cannot be loaded, the method
will return this value.
:param current_time: (bool) if ``default`` is None and this flag is set,
the method will return the current time.
:return: (int) start time in seconds.
"""
if not default and current_time:
default = utils.datetime_to_ts(timeutils.utcnow())
default = int(default) if default else None
try:
# pylint: disable=import-outside-toplevel
import uwsgi
start_time = uwsgi.opt.get('start-time')
if not start_time:
return
return default
return int(start_time.decode(encoding='utf-8'))
except ImportError:
return
return default

View File

@@ -21,6 +21,7 @@ from neutron_lib.db import resource_extend
from neutron_lib.db import utils as db_utils
from neutron_lib import exceptions as n_exc
from neutron_lib.objects import common_types
from oslo_utils import uuidutils
from oslo_versionedobjects import fields as obj_fields
from sqlalchemy import and_
from sqlalchemy import not_
@@ -29,6 +30,7 @@ from sqlalchemy import sql
from neutron._i18n import _
from neutron.common import _constants as common_constants
from neutron.common import utils as n_utils
from neutron.db.models import network_segment_range as range_model
from neutron.db.models.plugins.ml2 import geneveallocation as \
geneve_alloc_model
@@ -229,3 +231,46 @@ class NetworkSegmentRange(base.NeutronDbObject):
for _range in segment_ranges]
query = query.filter(or_(*clauses))
return query.limit(common_constants.IDPOOL_SELECT_SIZE).all()
@classmethod
def delete_expired_default_network_segment_ranges(
cls, context, network_type, start_time):
model = models_map.get(network_type)
if not model:
msg = (_("network_type '%s' unknown for getting allocation "
"information") % network_type)
raise n_exc.InvalidInput(error_message=msg)
created_at = n_utils.ts_to_datetime(start_time)
with cls.db_context_writer(context):
nsr_ids = context.session.query(cls.db_model.id).filter(
cls.db_model.default == sql.expression.true(),
cls.db_model.network_type == network_type,
cls.db_model.created_at != created_at).all()
nsr_ids = [nsr_id[0] for nsr_id in nsr_ids]
if nsr_ids:
NetworkSegmentRange.delete_objects(context, id=nsr_ids)
@classmethod
def new_default(cls, context, network_type, physical_network,
minimum, maximum, start_time):
model = models_map.get(network_type)
if not model:
msg = (_("network_type '%s' unknown for getting allocation "
"information") % network_type)
raise n_exc.InvalidInput(error_message=msg)
created_at = n_utils.ts_to_datetime(start_time)
with cls.db_context_writer(context):
if context.session.query(cls.db_model).filter(
cls.db_model.default == sql.expression.true(),
cls.db_model.shared == sql.expression.true(),
cls.db_model.network_type == network_type,
cls.db_model.physical_network == physical_network,
cls.db_model.minimum == minimum,
cls.db_model.maximum == maximum,
cls.db_model.created_at == created_at,
).count():
return
cls(context, id=uuidutils.generate_uuid(), default=True, shared=True,
network_type=network_type, physical_network=physical_network,
minimum=minimum, maximum=maximum, created_at=created_at).create()

View File

@@ -166,8 +166,7 @@ class SegmentTypeDriver(BaseTypeDriver):
LOG.debug(' - %s', srange)
@db_api.retry_db_errors
def _delete_expired_default_network_segment_ranges(self):
ctx = context.get_admin_context()
with db_api.CONTEXT_WRITER.using(ctx):
filters = {'default': True, 'network_type': self.get_type()}
ns_range.NetworkSegmentRange.delete_objects(ctx, **filters)
def _delete_expired_default_network_segment_ranges(self, start_time):
ns_range.NetworkSegmentRange.\
delete_expired_default_network_segment_ranges(
context.get_admin_context(), self.get_type(), start_time)

View File

@@ -29,7 +29,6 @@ from neutron_lib.plugins import utils as plugin_utils
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log
from oslo_utils import uuidutils
from sqlalchemy import or_
from neutron._i18n import _
@@ -146,21 +145,12 @@ class _TunnelTypeDriverBase(helpers.SegmentTypeDriver, metaclass=abc.ABCMeta):
{'type': self.get_type(), 'range': current_range})
@db_api.retry_db_errors
def _populate_new_default_network_segment_ranges(self):
def _populate_new_default_network_segment_ranges(self, start_time):
ctx = context.get_admin_context()
for tun_min, tun_max in self.tunnel_ranges:
res = {
'id': uuidutils.generate_uuid(),
'name': '',
'default': True,
'shared': True,
'network_type': self.get_type(),
'minimum': tun_min,
'maximum': tun_max}
with db_api.CONTEXT_WRITER.using(ctx):
new_default_range_obj = (
range_obj.NetworkSegmentRange(ctx, **res))
new_default_range_obj.create()
with db_api.CONTEXT_WRITER.using(ctx):
for tun_min, tun_max in self.tunnel_ranges:
range_obj.NetworkSegmentRange.new_default(
ctx, self.get_type(), None, tun_min, tun_max, start_time)
@db_api.retry_db_errors
def _get_network_segment_ranges_from_db(self):
@@ -174,9 +164,9 @@ class _TunnelTypeDriverBase(helpers.SegmentTypeDriver, metaclass=abc.ABCMeta):
return ranges
def initialize_network_segment_range_support(self):
self._delete_expired_default_network_segment_ranges()
self._populate_new_default_network_segment_ranges()
def initialize_network_segment_range_support(self, start_time):
self._delete_expired_default_network_segment_ranges(start_time)
self._populate_new_default_network_segment_ranges(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

View File

@@ -26,7 +26,6 @@ from neutron_lib.plugins.ml2 import api
from neutron_lib.plugins import utils as plugin_utils
from oslo_config import cfg
from oslo_log import log
from oslo_utils import uuidutils
from neutron._i18n import _
from neutron.conf.plugins.ml2.drivers import driver_type
@@ -58,24 +57,15 @@ class VlanTypeDriver(helpers.SegmentTypeDriver):
self._parse_network_vlan_ranges()
@db_api.retry_db_errors
def _populate_new_default_network_segment_ranges(self):
def _populate_new_default_network_segment_ranges(self, start_time):
ctx = context.get_admin_context()
for (physical_network, vlan_ranges) in (
self.network_vlan_ranges.items()):
for vlan_min, vlan_max in vlan_ranges:
res = {
'id': uuidutils.generate_uuid(),
'name': '',
'default': True,
'shared': True,
'network_type': p_const.TYPE_VLAN,
'physical_network': physical_network,
'minimum': vlan_min,
'maximum': vlan_max}
with db_api.CONTEXT_WRITER.using(ctx):
new_default_range_obj = (
range_obj.NetworkSegmentRange(ctx, **res))
new_default_range_obj.create()
with db_api.CONTEXT_WRITER.using(ctx):
for (physical_network, vlan_ranges) in (
self.network_vlan_ranges.items()):
for vlan_min, vlan_max in vlan_ranges:
range_obj.NetworkSegmentRange.new_default(
ctx, self.get_type(), physical_network, vlan_min,
vlan_max, start_time)
def _parse_network_vlan_ranges(self):
try:
@@ -181,9 +171,9 @@ class VlanTypeDriver(helpers.SegmentTypeDriver):
self._sync_vlan_allocations()
LOG.info("VlanTypeDriver initialization complete")
def initialize_network_segment_range_support(self):
self._delete_expired_default_network_segment_ranges()
self._populate_new_default_network_segment_ranges()
def initialize_network_segment_range_support(self, start_time):
self._delete_expired_default_network_segment_ranges(start_time)
self._populate_new_default_network_segment_ranges(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

View File

@@ -203,12 +203,12 @@ class TypeManager(stevedore.named.NamedExtensionManager):
LOG.info("Initializing driver for type '%s'", network_type)
driver.obj.initialize()
def initialize_network_segment_range_support(self):
def initialize_network_segment_range_support(self, start_time):
for network_type, driver in self.drivers.items():
if network_type in constants.NETWORK_SEGMENT_RANGE_TYPES:
LOG.info("Initializing driver network segment range support "
"for type '%s'", network_type)
driver.obj.initialize_network_segment_range_support()
driver.obj.initialize_network_segment_range_support(start_time)
def _add_network_segment(self, context, network_id, segment,
segment_index=0):

View File

@@ -25,6 +25,7 @@ from oslo_log import helpers as log_helpers
from oslo_log import log
from neutron._i18n import _
from neutron.common import wsgi_utils
from neutron.db import segments_db
from neutron.extensions import network_segment_range as ext_range
from neutron.objects import base as base_obj
@@ -65,8 +66,10 @@ class NetworkSegmentRangePlugin(ext_range.NetworkSegmentRangePluginBase):
def __init__(self):
super().__init__()
self._start_time = wsgi_utils.get_start_time(current_time=True)
self.type_manager = directory.get_plugin().type_manager
self.type_manager.initialize_network_segment_range_support()
self.type_manager.initialize_network_segment_range_support(
self._start_time)
def _get_network_segment_range(self, context, id):
obj = obj_network_segment_range.NetworkSegmentRange.get_object(

View File

@@ -19,8 +19,10 @@ from unittest import mock
from neutron_lib import constants
from neutron_lib import exceptions as n_exc
from neutron_lib.utils import helpers
from oslo_utils import timeutils
from oslo_utils import uuidutils
from neutron.common import utils as n_utils
from neutron.objects import network as net_obj
from neutron.objects import network_segment_range
from neutron.objects.plugins.ml2 import base as ml2_base
@@ -107,15 +109,19 @@ class NetworkSegmentRangeDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
def _create_network_segment_range(
self, minimum, maximum, network_type=None, physical_network=None,
project_id=None, default=False, shared=False):
project_id=None, default=False, shared=False, start_time=None):
kwargs = self.get_random_db_fields()
created_at = (n_utils.ts_to_datetime(start_time) if start_time else
timeutils.utcnow())
kwargs.update({'network_type': network_type or constants.TYPE_VLAN,
'physical_network': physical_network or 'foo',
'minimum': minimum,
'maximum': maximum,
'default': default,
'shared': shared,
'project_id': project_id})
'project_id': project_id,
'created_at': created_at,
})
db_obj = self._test_class.db_model(**kwargs)
obj_fields = self._test_class.modify_fields_from_db(db_obj)
obj = self._test_class(self.context, **obj_fields)
@@ -398,3 +404,45 @@ class NetworkSegmentRangeDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
self.assertEqual(len(available_ids), len(allocations))
for alloc in allocations:
self.assertIn(alloc.segmentation_id, available_ids)
def test_delete_expired_default_network_segment_ranges(self):
start_time = n_utils.datetime_to_ts(timeutils.utcnow())
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".
ranges = network_segment_range.NetworkSegmentRange.get_objects(
self.context, default=True, shared=True,
network_type=network_type)
self.assertEqual(1, len(ranges))
def test_new_default(self):
start_time = n_utils.datetime_to_ts(timeutils.utcnow())
for network_type in network_segment_range.models_map.keys():
physical_network = ('foo' if network_type == constants.TYPE_VLAN
else None)
ranges = network_segment_range.NetworkSegmentRange.get_objects(
self.context, network_type=network_type)
self.assertEqual(0, len(ranges))
# The method "new_default" is idempotent, call it twice.
for _ in range(2):
network_segment_range.NetworkSegmentRange.new_default(
self.context, network_type, physical_network,
1, 10, start_time)
ranges = network_segment_range.NetworkSegmentRange.get_objects(
self.context, network_type=network_type)
self.assertEqual(1, len(ranges))

View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import random
from unittest import mock
from neutron_lib import constants as p_const
@@ -477,13 +478,14 @@ class TunnelTypeNetworkSegmentRangeTestMixin:
cfg.CONF.set_override('service_plugins', [SERVICE_PLUGIN_KLASS])
self.context = context.Context()
self.driver = self.DRIVER_CLASS()
self.start_time = random.randint(10**5, 10**6)
def test__populate_new_default_network_segment_ranges(self):
# _populate_new_default_network_segment_ranges will be called when
# the type driver initializes with `network_segment_range` loaded as
# one of the `service_plugins`
self.driver._initialize(RAW_TUNNEL_RANGES)
self.driver.initialize_network_segment_range_support()
self.driver.initialize_network_segment_range_support(self.start_time)
self.driver.sync_allocations()
ret = obj_network_segment_range.NetworkSegmentRange.get_objects(
self.context)
@@ -501,7 +503,8 @@ class TunnelTypeNetworkSegmentRangeTestMixin:
def test__delete_expired_default_network_segment_ranges(self):
self.driver.tunnel_ranges = TUNNEL_RANGES
self.driver.sync_allocations()
self.driver._delete_expired_default_network_segment_ranges()
self.driver._delete_expired_default_network_segment_ranges(
self.start_time)
ret = obj_network_segment_range.NetworkSegmentRange.get_objects(
self.context)
self.context, network_type=self.driver.get_type())
self.assertEqual(0, len(ret))

View File

@@ -19,8 +19,10 @@ from neutron_lib import context
from neutron_lib.plugins import utils as plugin_utils
from oslo_config import cfg
from oslo_db import exception as exc
from oslo_utils import timeutils
from sqlalchemy.orm import query
from neutron.common import utils as n_utils
from neutron.plugins.ml2.drivers import type_vlan
from neutron.tests.unit import testlib_api
@@ -152,5 +154,6 @@ class HelpersTestWithNetworkSegmentRange(HelpersTest):
NETWORK_VLAN_RANGES_CFG_ENTRIES)
self.context = context.get_admin_context()
self.driver = type_vlan.VlanTypeDriver()
self.driver.initialize_network_segment_range_support()
start_time = n_utils.datetime_to_ts(timeutils.utcnow())
self.driver.initialize_network_segment_range_support(start_time)
self.driver._sync_vlan_allocations()

View File

@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import random
from unittest import mock
from neutron_lib import constants as p_const
@@ -371,6 +372,7 @@ class VlanTypeTestWithNetworkSegmentRange(testlib_api.SqlTestCase):
self.driver._sync_vlan_allocations()
self.context = context.Context()
self.setup_coreplugin(CORE_PLUGIN)
self.start_time = random.randint(10**5, 10**6)
def test__populate_new_default_network_segment_ranges(self):
# _populate_new_default_network_segment_ranges will be called when
@@ -397,7 +399,8 @@ class VlanTypeTestWithNetworkSegmentRange(testlib_api.SqlTestCase):
self.assertEqual(VLAN_MAX, network_segment_range.maximum)
def test__delete_expired_default_network_segment_ranges(self):
self.driver._delete_expired_default_network_segment_ranges()
self.driver._delete_expired_default_network_segment_ranges(
self.start_time)
ret = obj_network_segment_range.NetworkSegmentRange.get_objects(
self.context)
self.context, network_type=self.driver.get_type())
self.assertEqual(0, len(ret))

View File

@@ -0,0 +1,8 @@
---
other:
- |
The ``network_segment_ranges`` registers are now initialized based on the
Neutron API start time. The type driver class cleans up the database
for those registers not matching the network type and the "created_at"
timestamp and uses the process-safe method
``NetworkSegmentRange.new_default`` to create the new registers.