Skip to content

Commit 83d8fc6

Browse files
authored
Merge pull request #1310 from rackerlabs/fix_native_vlan_clash
feat: add subport's segmentation id check
2 parents 00ece8c + 1d5ee84 commit 83d8fc6

File tree

5 files changed

+271
-7
lines changed

5 files changed

+271
-7
lines changed

python/neutron-understack/neutron_understack/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@
8282
"Nautobot."
8383
),
8484
),
85+
cfg.ListOpt(
86+
"default_tenant_vlan_id_range",
87+
default=[1, 3799],
88+
item_type=cfg.types.Integer(min=1, max=4094),
89+
help=(
90+
"List of 2 comma separated integers, that represents a VLAN range, that"
91+
"will be used for mapped VLANs on the switches."
92+
),
93+
),
8594
]
8695

8796
l3_svc_cisco_asa_opts = [

python/neutron-understack/neutron_understack/tests/test_trunk.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from oslo_config import cfg
44

55
from neutron_understack import utils
6+
from neutron_understack.trunk import SubportSegmentationIDError
67

78

89
class TestSubportsAdded:
@@ -54,18 +55,22 @@ def test_when_subports_are_not_present(
5455
@pytest.mark.usefixtures("ironic_baremetal_port_physical_network")
5556
@pytest.mark.usefixtures("utils_fetch_subport_network_id_patch")
5657
class Test_HandleTenantVlanIDAndSwitchportConfig:
57-
def test_when_ucvni_tenant_vlan_id_is_not_set_yet(
58+
def test_that_check_subports_segmentation_id_is_called(
5859
self, mocker, understack_trunk_driver, trunk, subport, network_id, vlan_num
5960
):
6061
mocker.patch("neutron_understack.utils.fetch_port_object")
6162
mocker.patch(
6263
"neutron_understack.utils.parent_port_is_bound", return_value=False
6364
)
64-
65+
subport_seg_id_check = mocker.patch.object(
66+
understack_trunk_driver, "_check_subports_segmentation_id"
67+
)
6568
understack_trunk_driver._handle_tenant_vlan_id_and_switchport_config(
6669
[subport], trunk
6770
)
6871

72+
subport_seg_id_check.assert_called_once()
73+
6974
def test_when_parent_port_is_bound(
7075
self,
7176
mocker,
@@ -76,6 +81,7 @@ def test_when_parent_port_is_bound(
7681
port_id,
7782
vlan_network_segment,
7883
):
84+
mocker.patch.object(understack_trunk_driver, "_check_subports_segmentation_id")
7985
mocker.patch(
8086
"neutron_understack.utils.fetch_port_object", return_value=port_object
8187
)
@@ -87,9 +93,13 @@ def test_when_parent_port_is_bound(
8793
"neutron_understack.utils.network_segment_by_physnet", return_value=None
8894
)
8995
mocker.patch("neutron_understack.utils.create_binding_profile_level")
96+
add_subports_networks = mocker.patch.object(
97+
understack_trunk_driver, "_add_subports_networks_to_parent_port_switchport"
98+
)
9099
understack_trunk_driver._handle_tenant_vlan_id_and_switchport_config(
91100
[subport], trunk
92101
)
102+
add_subports_networks.assert_called_once()
93103

94104
def test_subports_add_post(
95105
self,
@@ -114,20 +124,18 @@ def test_subports_add_post(
114124
def test_when_parent_port_is_unbound(
115125
self, mocker, understack_trunk_driver, trunk, subport, port_object
116126
):
127+
mocker.patch.object(understack_trunk_driver, "_check_subports_segmentation_id")
117128
port_object.bindings[0].vif_type = portbindings.VIF_TYPE_UNBOUND
118129
mocker.patch(
119130
"neutron_understack.utils.fetch_port_object", return_value=port_object
120131
)
121-
mocker.patch.object(
132+
add_subports_networks = mocker.patch.object(
122133
understack_trunk_driver, "_add_subports_networks_to_parent_port_switchport"
123134
)
124135
understack_trunk_driver._handle_tenant_vlan_id_and_switchport_config(
125136
[subport], trunk
126137
)
127-
128-
(
129-
understack_trunk_driver._add_subports_networks_to_parent_port_switchport.assert_not_called()
130-
)
138+
add_subports_networks.assert_not_called()
131139

132140

133141
class TestSubportsDeleted:
@@ -322,3 +330,51 @@ def test_that_handle_subports_removal_is_called(
322330
subports=[],
323331
invoke_undersync=False,
324332
)
333+
334+
335+
class TestCheckSubportsSegmentationId:
336+
def test_when_trunk_id_is_network_node_trunk_id(
337+
self,
338+
mocker,
339+
understack_trunk_driver,
340+
trunk_id,
341+
):
342+
mocker.patch(
343+
"oslo_config.cfg.CONF.ml2_understack.network_node_trunk_uuid",
344+
trunk_id,
345+
)
346+
result = understack_trunk_driver._check_subports_segmentation_id([], trunk_id)
347+
assert result is None
348+
349+
def test_when_segmentation_id_is_in_allowed_range(
350+
self,
351+
mocker,
352+
understack_trunk_driver,
353+
trunk_id,
354+
subport,
355+
):
356+
allowed_ranges = mocker.patch(
357+
"neutron_understack.utils.allowed_tenant_vlan_id_ranges",
358+
return_value=[(1, 1500)],
359+
)
360+
subport.segmentation_id = 500
361+
result = understack_trunk_driver._check_subports_segmentation_id(
362+
[subport], trunk_id
363+
)
364+
allowed_ranges.assert_called_once()
365+
assert result is None
366+
367+
def test_when_segmentation_id_is_not_in_allowed_range(
368+
self,
369+
mocker,
370+
understack_trunk_driver,
371+
trunk_id,
372+
subport,
373+
):
374+
mocker.patch(
375+
"neutron_understack.utils.allowed_tenant_vlan_id_ranges",
376+
return_value=[(1, 1500)],
377+
)
378+
subport.segmentation_id = 1600
379+
with pytest.raises(SubportSegmentationIDError):
380+
understack_trunk_driver._check_subports_segmentation_id([subport], trunk_id)

python/neutron-understack/neutron_understack/tests/test_utils.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,107 @@ def test_router_interface_false_device_owner_missing(self):
129129
def test_router_interface_false_device_owner_none(self):
130130
context = PortContext(current={"device_owner": None})
131131
assert not utils.is_router_interface(context)
132+
133+
134+
class TestMergeOverlappedRanges:
135+
def test_single_range(self):
136+
assert utils.merge_overlapped_ranges([(1, 5)]) == [(1, 5)]
137+
138+
def test_no_overlap(self):
139+
ranges = [(1, 3), (5, 7), (9, 10)]
140+
expected = [(1, 3), (5, 7), (9, 10)]
141+
assert utils.merge_overlapped_ranges(ranges) == expected
142+
143+
def test_simple_overlap(self):
144+
ranges = [(1, 4), (2, 5)]
145+
expected = [(1, 5)]
146+
assert utils.merge_overlapped_ranges(ranges) == expected
147+
148+
def test_multiple_ranges(self):
149+
ranges = [(1, 3), (2, 6), (8, 10), (15, 18)]
150+
expected = [(1, 6), (8, 10), (15, 18)]
151+
assert utils.merge_overlapped_ranges(ranges) == expected
152+
153+
def test_touching_ranges(self):
154+
ranges = [(1, 4), (5, 7), (8, 10)]
155+
expected = [(1, 10)]
156+
assert utils.merge_overlapped_ranges(ranges) == expected
157+
158+
def test_unsorted_input(self):
159+
ranges = [(8, 10), (1, 3), (2, 7), (15, 18)]
160+
expected = [(1, 10), (15, 18)]
161+
assert utils.merge_overlapped_ranges(ranges) == expected
162+
163+
164+
class TestFetchGapsInRanges:
165+
def test_full_coverage(self):
166+
assert utils.fetch_gaps_in_ranges([(1, 10)], (1, 10)) == []
167+
168+
def test_gap_at_start(self):
169+
assert utils.fetch_gaps_in_ranges([(5, 8)], (1, 10)) == [(1, 4), (9, 10)]
170+
171+
def test_gap_at_end(self):
172+
assert utils.fetch_gaps_in_ranges([(1, 6)], (1, 10)) == [(7, 10)]
173+
174+
def test_gap_in_middle(self):
175+
assert utils.fetch_gaps_in_ranges([(1, 2), (5, 10)], (1, 10)) == [(3, 4)]
176+
177+
def test_multiple_gaps(self):
178+
result = utils.fetch_gaps_in_ranges([(1, 2), (4, 5), (8, 8)], (1, 10))
179+
assert result == [(3, 3), (6, 7), (9, 10)]
180+
181+
182+
class DummyRange:
183+
def __init__(self, minimum, maximum):
184+
self.minimum = minimum
185+
self.maximum = maximum
186+
187+
188+
class TestAllowedTenantVlanIdRanges:
189+
def test_multiple_non_overlapping_ranges(
190+
self,
191+
mocker,
192+
):
193+
mocker.patch(
194+
"oslo_config.cfg.CONF.ml2_understack.default_tenant_vlan_id_range",
195+
[1, 2000],
196+
)
197+
mocker.patch(
198+
"neutron_understack.utils.fetch_vlan_network_segment_ranges",
199+
return_value=[DummyRange(500, 700), DummyRange(900, 1200)],
200+
)
201+
expected_result = [(1, 499), (701, 899), (1201, 2000)]
202+
result = utils.allowed_tenant_vlan_id_ranges()
203+
assert result == expected_result
204+
205+
def test_multiple_overlapping_ranges(
206+
self,
207+
mocker,
208+
):
209+
mocker.patch(
210+
"oslo_config.cfg.CONF.ml2_understack.default_tenant_vlan_id_range",
211+
[1, 2000],
212+
)
213+
mocker.patch(
214+
"neutron_understack.utils.fetch_vlan_network_segment_ranges",
215+
return_value=[DummyRange(500, 700), DummyRange(600, 1200)],
216+
)
217+
expected_result = [(1, 499), (1201, 2000)]
218+
result = utils.allowed_tenant_vlan_id_ranges()
219+
assert result == expected_result
220+
221+
def test_single_range(
222+
self,
223+
mocker,
224+
):
225+
mocker.patch(
226+
"oslo_config.cfg.CONF.ml2_understack.default_tenant_vlan_id_range",
227+
[1, 2000],
228+
)
229+
mocker.patch(
230+
"neutron_understack.utils.fetch_vlan_network_segment_ranges",
231+
return_value=[DummyRange(500, 700)],
232+
)
233+
expected_result = [(1, 499), (701, 2000)]
234+
result = utils.allowed_tenant_vlan_id_ranges()
235+
assert result == expected_result

python/neutron-understack/neutron_understack/trunk.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from neutron.objects.trunk import SubPort
55
from neutron.services.trunk.drivers import base as trunk_base
66
from neutron.services.trunk.models import Trunk
7+
from neutron_lib import exceptions as exc
78
from neutron_lib.api.definitions import portbindings
89
from neutron_lib.callbacks import events
910
from neutron_lib.callbacks import registry
@@ -21,6 +22,14 @@
2122
SUPPORTED_SEGMENTATION_TYPES = (trunk_consts.SEGMENTATION_TYPE_VLAN,)
2223

2324

25+
class SubportSegmentationIDError(exc.NeutronException):
26+
message = (
27+
"Segmentation ID: %(seg_id)s cannot be set to the Subport: "
28+
"%(subport_id)s as it falls outside of allowed ranges: "
29+
"%(network_segment_ranges)s. Please use different Segmentation ID."
30+
)
31+
32+
2433
class UnderStackTrunkDriver(trunk_base.DriverBase):
2534
def __init__(
2635
self,
@@ -96,13 +105,43 @@ def register(self, resource, event, trigger, payload=None):
96105
def _handle_tenant_vlan_id_and_switchport_config(
97106
self, subports: list[SubPort], trunk: Trunk
98107
) -> None:
108+
self._check_subports_segmentation_id(subports, trunk.id)
99109
parent_port_obj = utils.fetch_port_object(trunk.port_id)
100110

101111
if utils.parent_port_is_bound(parent_port_obj):
102112
self._add_subports_networks_to_parent_port_switchport(
103113
parent_port_obj, subports
104114
)
105115

116+
def _check_subports_segmentation_id(
117+
self, subports: list[SubPort], trunk_id: str
118+
) -> None:
119+
"""Checks if a subport's segmentation_id is within the allowed range.
120+
121+
A switchport cannot have a mapped VLAN ID equal to the native VLAN ID.
122+
Since the user specifies the VLAN ID (segmentation_id) when adding a
123+
subport, an error is raised if it falls within any VLAN network segment
124+
range, as these ranges are used to allocate VLAN tags for all VLAN
125+
segments, including native VLANs.
126+
127+
The only case where this check is not required is for a network node
128+
trunk, since its subport segmentation_ids are the same as the network
129+
segment VLAN tags allocated to the subports. Therefore, there is no
130+
possibility of conflict with the native VLAN.
131+
"""
132+
if trunk_id == cfg.CONF.ml2_understack.network_node_trunk_uuid:
133+
return
134+
135+
ns_ranges = utils.allowed_tenant_vlan_id_ranges()
136+
for subport in subports:
137+
seg_id = subport.segmentation_id
138+
if not utils.segmentation_id_in_ranges(seg_id, ns_ranges):
139+
raise SubportSegmentationIDError(
140+
seg_id=seg_id,
141+
subport_id=subport.port_id,
142+
network_segment_ranges=utils.printable_ranges(ns_ranges),
143+
)
144+
106145
def configure_trunk(self, trunk_details: dict, port_id: str) -> None:
107146
parent_port_obj = utils.fetch_port_object(port_id)
108147
subports = trunk_details.get("sub_ports", [])
@@ -146,6 +185,7 @@ def _add_subports_networks_to_parent_port_switchport(
146185
vlan_group_name = self.ironic_client.baremetal_port_physical_network(
147186
local_link_info
148187
)
188+
149189
self._handle_segment_allocation(subports, vlan_group_name, binding_host)
150190

151191
def clean_trunk(

0 commit comments

Comments
 (0)