Skip to content

Commit f873d55

Browse files
committed
Initialize the network segment ranges only in first WSGI worker
The implementation done in [1] does not fully work across WSGi workers. The method ``NetworkSegmentRange.new_default`` tries to first check if the default segment range for a specific driver (VLAN, tunnelled) is present. However, as seen in production environments, this method is not multiprocess safe. Instead, this patch is limiting the execution of the network segment ranges initialization to the first WSGI worker (there must be at least one worker). This patch also wraps the VLAN and tunnelled drivers initialization inside a database transaction context. All the operations executed in this method (register clean-up, new default registers creation and ranges sync) are done in one single database transaction, that ensures its isolation and integrity. NOTE: The same initialization method, when called, removes the duplicated registers created by [1] in first place. A Neutron API update and restart will fix the database ``network_segment_ranges`` registers. NOTE: The ranges class variable (``_TunnelTypeDriverBase.tunnel_ranges`` or ``VlanTypeDriver.network_vlan_ranges``) stores initially the configured segment ranges (static configuration file). If the network segment range plugin is loaded, it will store the segment ranges from the database. But this variable should not be public; instead of this, the method ``get_network_segment_ranges`` provides the needed class API to retrieve this information. [1]https://review.opendev.org/c/openstack/neutron/+/938319 Closes-Bug: #2106463 Change-Id: Ibc42f900214e1f7631e266bccd083a2ef4111585 (cherry picked from commit 39d95a1)
1 parent 30837fc commit f873d55

File tree

8 files changed

+167
-57
lines changed

8 files changed

+167
-57
lines changed

neutron/common/wsgi_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
from neutron.common import utils
2121

2222

23+
FIRST_WORKER_ID = 1
24+
25+
2326
def get_start_time(default=None, current_time=False):
2427
"""Return the 'start-time=%t' config varible in the WSGI config
2528

neutron/plugins/ml2/drivers/helpers.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import functools
1717

18-
from neutron_lib import context
1918
from neutron_lib.db import api as db_api
2019
from neutron_lib import exceptions
2120
from neutron_lib.plugins import constants as plugin_constants
@@ -165,8 +164,7 @@ def allocate_partially_specified_segment(self, context, **filters):
165164
context.elevated()):
166165
LOG.debug(' - %s', srange)
167166

168-
@db_api.retry_db_errors
169-
def _delete_expired_default_network_segment_ranges(self, start_time):
170-
ns_range.NetworkSegmentRange.\
171-
delete_expired_default_network_segment_ranges(
172-
context.get_admin_context(), self.get_type(), start_time)
167+
def _delete_expired_default_network_segment_ranges(self, ctx, start_time):
168+
(ns_range.NetworkSegmentRange.
169+
delete_expired_default_network_segment_ranges(
170+
ctx, self.get_type(), start_time))

neutron/plugins/ml2/drivers/type_tunnel.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def _initialize(self, raw_tunnel_ranges):
127127
# allocation during driver initialization, instead of using the
128128
# directory.get_plugin() method - the normal way used elsewhere to
129129
# check if a plugin is loaded.
130-
self.sync_allocations()
130+
self._sync_allocations()
131131

132132
def _parse_tunnel_ranges(self, tunnel_ranges, current_range):
133133
for entry in tunnel_ranges:
@@ -145,17 +145,15 @@ def _parse_tunnel_ranges(self, tunnel_ranges, current_range):
145145
{'type': self.get_type(), 'range': current_range})
146146

147147
@db_api.retry_db_errors
148-
def _populate_new_default_network_segment_ranges(self, start_time):
149-
ctx = context.get_admin_context()
150-
with db_api.CONTEXT_WRITER.using(ctx):
151-
for tun_min, tun_max in self.tunnel_ranges:
152-
range_obj.NetworkSegmentRange.new_default(
153-
ctx, self.get_type(), None, tun_min, tun_max, start_time)
148+
def _populate_new_default_network_segment_ranges(self, ctx, start_time):
149+
for tun_min, tun_max in self.tunnel_ranges:
150+
range_obj.NetworkSegmentRange.new_default(
151+
ctx, self.get_type(), None, tun_min, tun_max, start_time)
154152

155153
@db_api.retry_db_errors
156-
def _get_network_segment_ranges_from_db(self):
154+
def _get_network_segment_ranges_from_db(self, ctx=None):
157155
ranges = []
158-
ctx = context.get_admin_context()
156+
ctx = ctx or context.get_admin_context()
159157
with db_api.CONTEXT_READER.using(ctx):
160158
range_objs = (range_obj.NetworkSegmentRange.get_objects(
161159
ctx, network_type=self.get_type()))
@@ -164,21 +162,27 @@ def _get_network_segment_ranges_from_db(self):
164162

165163
return ranges
166164

165+
@db_api.retry_db_errors
167166
def initialize_network_segment_range_support(self, start_time):
168-
self._delete_expired_default_network_segment_ranges(start_time)
169-
self._populate_new_default_network_segment_ranges(start_time)
170-
# Override self.tunnel_ranges with the network segment range
171-
# information from DB and then do a sync_allocations since the
172-
# segment range service plugin has not yet been loaded at this
173-
# initialization time.
174-
self.tunnel_ranges = self._get_network_segment_ranges_from_db()
175-
self.sync_allocations()
167+
admin_context = context.get_admin_context()
168+
with db_api.CONTEXT_WRITER.using(admin_context):
169+
self._delete_expired_default_network_segment_ranges(
170+
admin_context, start_time)
171+
self._populate_new_default_network_segment_ranges(
172+
admin_context, start_time)
173+
# Override self.tunnel_ranges with the network segment range
174+
# information from DB and then do a sync_allocations since the
175+
# segment range service plugin has not yet been loaded at this
176+
# initialization time.
177+
self.tunnel_ranges = self._get_network_segment_ranges_from_db(
178+
ctx=admin_context)
179+
self._sync_allocations(ctx=admin_context)
176180

177181
def update_network_segment_range_allocations(self):
178-
self.sync_allocations()
182+
self._sync_allocations()
179183

180184
@db_api.retry_db_errors
181-
def sync_allocations(self):
185+
def _sync_allocations(self, ctx=None):
182186
# determine current configured allocatable tunnel ids
183187
tunnel_ids = set()
184188
ranges = self.get_network_segment_ranges()
@@ -187,7 +191,7 @@ def sync_allocations(self):
187191

188192
tunnel_id_getter = operator.attrgetter(self.segmentation_key)
189193
tunnel_col = getattr(self.model, self.segmentation_key)
190-
ctx = context.get_admin_context()
194+
ctx = ctx or context.get_admin_context()
191195
with db_api.CONTEXT_WRITER.using(ctx):
192196
# Check if the allocations are updated: if the total number of
193197
# allocations for this tunnel type matches the allocations of the

neutron/plugins/ml2/drivers/type_vlan.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,13 @@ def __init__(self):
5656
self.model_segmentation_id = vlan_alloc_model.VlanAllocation.vlan_id
5757
self._parse_network_vlan_ranges()
5858

59-
@db_api.retry_db_errors
60-
def _populate_new_default_network_segment_ranges(self, start_time):
61-
ctx = context.get_admin_context()
62-
with db_api.CONTEXT_WRITER.using(ctx):
63-
for (physical_network, vlan_ranges) in (
64-
self.network_vlan_ranges.items()):
65-
for vlan_min, vlan_max in vlan_ranges:
66-
range_obj.NetworkSegmentRange.new_default(
67-
ctx, self.get_type(), physical_network, vlan_min,
68-
vlan_max, start_time)
59+
def _populate_new_default_network_segment_ranges(self, ctx, start_time):
60+
for (physical_network, vlan_ranges) in (
61+
self.network_vlan_ranges.items()):
62+
for vlan_min, vlan_max in vlan_ranges:
63+
range_obj.NetworkSegmentRange.new_default(
64+
ctx, self.get_type(), physical_network, vlan_min,
65+
vlan_max, start_time)
6966

7067
def _parse_network_vlan_ranges(self):
7168
try:
@@ -78,8 +75,8 @@ def _parse_network_vlan_ranges(self):
7875
LOG.info("Network VLAN ranges: %s", self.network_vlan_ranges)
7976

8077
@db_api.retry_db_errors
81-
def _sync_vlan_allocations(self):
82-
ctx = context.get_admin_context()
78+
def _sync_vlan_allocations(self, ctx=None):
79+
ctx = ctx or context.get_admin_context()
8380
with db_api.CONTEXT_WRITER.using(ctx):
8481
# VLAN ranges per physical network:
8582
# {phy1: [(1, 10), (30, 50)], ...}
@@ -142,9 +139,9 @@ def _sync_vlan_allocations(self):
142139
vlan_ids)
143140

144141
@db_api.retry_db_errors
145-
def _get_network_segment_ranges_from_db(self):
142+
def _get_network_segment_ranges_from_db(self, ctx=None):
146143
ranges = {}
147-
ctx = context.get_admin_context()
144+
ctx = ctx or context.get_admin_context()
148145
with db_api.CONTEXT_READER.using(ctx):
149146
range_objs = (range_obj.NetworkSegmentRange.get_objects(
150147
ctx, network_type=self.get_type()))
@@ -171,15 +168,21 @@ def initialize(self):
171168
self._sync_vlan_allocations()
172169
LOG.info("VlanTypeDriver initialization complete")
173170

171+
@db_api.retry_db_errors
174172
def initialize_network_segment_range_support(self, start_time):
175-
self._delete_expired_default_network_segment_ranges(start_time)
176-
self._populate_new_default_network_segment_ranges(start_time)
177-
# Override self.network_vlan_ranges with the network segment range
178-
# information from DB and then do a sync_allocations since the
179-
# segment range service plugin has not yet been loaded at this
180-
# initialization time.
181-
self.network_vlan_ranges = self._get_network_segment_ranges_from_db()
182-
self._sync_vlan_allocations()
173+
admin_context = context.get_admin_context()
174+
with db_api.CONTEXT_WRITER.using(admin_context):
175+
self._delete_expired_default_network_segment_ranges(
176+
admin_context, start_time)
177+
self._populate_new_default_network_segment_ranges(
178+
admin_context, start_time)
179+
# Override self.network_vlan_ranges with the network segment range
180+
# information from DB and then do a sync_allocations since the
181+
# segment range service plugin has not yet been loaded at this
182+
# initialization time.
183+
self.network_vlan_ranges = (
184+
self._get_network_segment_ranges_from_db(ctx=admin_context))
185+
self._sync_vlan_allocations(ctx=admin_context)
183186

184187
def update_network_segment_range_allocations(self):
185188
self._sync_vlan_allocations()

neutron/plugins/ml2/managers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import stevedore
3333

3434
from neutron._i18n import _
35+
from neutron.common import wsgi_utils
3536
from neutron.conf.plugins.ml2 import config
3637
from neutron.db import segments_db
3738
from neutron.objects import ports
@@ -205,6 +206,9 @@ def initialize(self):
205206
driver.obj.initialize()
206207

207208
def initialize_network_segment_range_support(self, start_time):
209+
if wsgi_utils.get_api_worker_id() != wsgi_utils.FIRST_WORKER_ID:
210+
return
211+
208212
for network_type, driver in self.drivers.items():
209213
if network_type in constants.NETWORK_SEGMENT_RANGE_TYPES:
210214
LOG.info("Initializing driver network segment range support "
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright 2025 Red Hat Inc.
2+
# All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
16+
from concurrent import futures
17+
import time
18+
19+
from neutron_lib import constants
20+
from neutron_lib import context
21+
from neutron_lib.db import api as db_api
22+
from oslo_config import cfg
23+
24+
from neutron.conf import common as common_config
25+
from neutron.conf.plugins.ml2 import config as ml2_config
26+
from neutron.conf.plugins.ml2.drivers import driver_type as driver_type_config
27+
from neutron.objects import network_segment_range as range_obj
28+
from neutron.plugins.ml2.drivers import type_geneve
29+
from neutron.tests.unit import testlib_api
30+
31+
32+
def _initialize_network_segment_range_support(type_driver, start_time):
33+
# This method is similar to
34+
# ``_TunnelTypeDriverBase.initialize_network_segment_range_support``.
35+
# The method first deletes the existing default network ranges and then
36+
# creates the new ones. It also adds an extra second before closing the
37+
# DB transaction.
38+
admin_context = context.get_admin_context()
39+
with db_api.CONTEXT_WRITER.using(admin_context):
40+
type_driver._delete_expired_default_network_segment_ranges(
41+
admin_context, start_time)
42+
type_driver._populate_new_default_network_segment_ranges(
43+
admin_context, start_time)
44+
time.sleep(1)
45+
46+
47+
class TunnelTypeDriverBaseTestCase(testlib_api.SqlTestCase):
48+
def setUp(self):
49+
super().setUp()
50+
cfg.CONF.register_opts(common_config.core_opts)
51+
ml2_config.register_ml2_plugin_opts()
52+
driver_type_config.register_ml2_drivers_geneve_opts()
53+
ml2_config.cfg.CONF.set_override(
54+
'service_plugins', 'network_segment_range')
55+
self.min = 1001
56+
self.max = 1020
57+
self.net_type = constants.TYPE_GENEVE
58+
ml2_config.cfg.CONF.set_override(
59+
'vni_ranges', f'{self.min}:{self.max}', group='ml2_type_geneve')
60+
self.admin_ctx = context.get_admin_context()
61+
self.type_driver = type_geneve.GeneveTypeDriver()
62+
self.type_driver.initialize()
63+
64+
def test_initialize_network_segment_range_support(self):
65+
# Execute the initialization several times with different start times.
66+
for start_time in range(3):
67+
self.type_driver.initialize_network_segment_range_support(
68+
start_time)
69+
sranges = range_obj.NetworkSegmentRange.get_objects(self.admin_ctx)
70+
self.assertEqual(1, len(sranges))
71+
self.assertEqual(self.net_type, sranges[0].network_type)
72+
self.assertEqual(self.min, sranges[0].minimum)
73+
self.assertEqual(self.max, sranges[0].maximum)
74+
self.assertEqual([(self.min, self.max)],
75+
self.type_driver.tunnel_ranges)
76+
77+
def test_initialize_network_segment_range_support_parallel_execution(self):
78+
max_workers = 3
79+
_futures = []
80+
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
81+
for idx in range(max_workers):
82+
_futures.append(executor.submit(
83+
_initialize_network_segment_range_support,
84+
self.type_driver, idx))
85+
for _future in _futures:
86+
_future.result()
87+
88+
sranges = range_obj.NetworkSegmentRange.get_objects(self.admin_ctx)
89+
self.assertEqual(1, len(sranges))
90+
self.assertEqual(self.net_type, sranges[0].network_type)
91+
self.assertEqual(self.min, sranges[0].minimum)
92+
self.assertEqual(self.max, sranges[0].maximum)

neutron/tests/unit/plugins/ml2/drivers/base_type_tunnel.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def setUp(self):
4949
super().setUp()
5050
self.driver = self.DRIVER_CLASS()
5151
self.driver.tunnel_ranges = TUNNEL_RANGES
52-
self.driver.sync_allocations()
52+
self.driver._sync_allocations()
5353
self.context = context.Context()
5454

5555
def test_tunnel_type(self):
@@ -84,7 +84,7 @@ def test_sync_tunnel_allocations(self):
8484
self.driver.get_allocation(self.context, (TUN_MAX + 1)))
8585

8686
self.driver.tunnel_ranges = UPDATED_TUNNEL_RANGES
87-
self.driver.sync_allocations()
87+
self.driver._sync_allocations()
8888

8989
self.assertIsNone(
9090
self.driver.get_allocation(self.context, (TUN_MIN + 5 - 1)))
@@ -108,7 +108,7 @@ def _test_sync_allocations_and_allocated(self, tunnel_id):
108108
self.driver.reserve_provider_segment(self.context, segment)
109109

110110
self.driver.tunnel_ranges = UPDATED_TUNNEL_RANGES
111-
self.driver.sync_allocations()
111+
self.driver._sync_allocations()
112112

113113
self.assertTrue(
114114
self.driver.get_allocation(self.context, tunnel_id).allocated)
@@ -127,7 +127,7 @@ def verify_no_chunk(iterable, chunk_size):
127127
return []
128128
with mock.patch.object(
129129
type_tunnel, 'chunks', side_effect=verify_no_chunk) as chunks:
130-
self.driver.sync_allocations()
130+
self.driver._sync_allocations()
131131
# No writing operation is done, fast exit: current allocations
132132
# already present.
133133
self.assertEqual(0, len(chunks.mock_calls))
@@ -295,7 +295,7 @@ def setUp(self):
295295
super().setUp()
296296
self.driver = self.DRIVER_CLASS()
297297
self.driver.tunnel_ranges = self.TUNNEL_MULTI_RANGES
298-
self.driver.sync_allocations()
298+
self.driver._sync_allocations()
299299
self.context = context.Context()
300300

301301
def test_release_segment(self):
@@ -486,7 +486,7 @@ def test__populate_new_default_network_segment_ranges(self):
486486
# one of the `service_plugins`
487487
self.driver._initialize(RAW_TUNNEL_RANGES)
488488
self.driver.initialize_network_segment_range_support(self.start_time)
489-
self.driver.sync_allocations()
489+
self.driver._sync_allocations()
490490
ret = obj_network_segment_range.NetworkSegmentRange.get_objects(
491491
self.context)
492492
self.assertEqual(1, len(ret))
@@ -502,9 +502,9 @@ def test__populate_new_default_network_segment_ranges(self):
502502

503503
def test__delete_expired_default_network_segment_ranges(self):
504504
self.driver.tunnel_ranges = TUNNEL_RANGES
505-
self.driver.sync_allocations()
505+
self.driver._sync_allocations()
506506
self.driver._delete_expired_default_network_segment_ranges(
507-
self.start_time)
507+
self.context, self.start_time)
508508
ret = obj_network_segment_range.NetworkSegmentRange.get_objects(
509509
self.context, network_type=self.driver.get_type())
510510
self.assertEqual(0, len(ret))

neutron/tests/unit/plugins/ml2/drivers/test_type_vlan.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@
2525
from oslo_config import cfg
2626
from testtools import matchers
2727

28+
from neutron.common import wsgi_utils
29+
from neutron.conf.plugins.ml2 import config as ml2_config
2830
from neutron.objects import network_segment_range as obj_network_segment_range
2931
from neutron.objects.plugins.ml2 import vlanallocation as vlan_alloc_obj
3032
from neutron.plugins.ml2.drivers import type_vlan
3133
from neutron.tests.unit import testlib_api
3234

35+
3336
PROVIDER_NET = 'phys_net1'
3437
TENANT_NET = 'phys_net2'
3538
UNCONFIGURED_NET = 'no_net'
@@ -361,6 +364,9 @@ def test_allocate_tenant_segment_in_order_of_config(self):
361364
class VlanTypeTestWithNetworkSegmentRange(testlib_api.SqlTestCase):
362365

363366
def setUp(self):
367+
ml2_config.register_ml2_plugin_opts()
368+
mock.patch.object(wsgi_utils, 'get_api_worker_id',
369+
return_value=wsgi_utils.FIRST_WORKER_ID).start()
364370
super().setUp()
365371
cfg.CONF.set_override('network_vlan_ranges',
366372
NETWORK_VLAN_RANGES,
@@ -400,7 +406,7 @@ def test__populate_new_default_network_segment_ranges(self):
400406

401407
def test__delete_expired_default_network_segment_ranges(self):
402408
self.driver._delete_expired_default_network_segment_ranges(
403-
self.start_time)
409+
self.context, self.start_time)
404410
ret = obj_network_segment_range.NetworkSegmentRange.get_objects(
405411
self.context, network_type=self.driver.get_type())
406412
self.assertEqual(0, len(ret))

0 commit comments

Comments
 (0)