Skip to content

Commit f26079f

Browse files
Milan FencikMilan Fencik
authored andcommitted
add subport's segmentation id check
1 parent 7014a25 commit f26079f

File tree

2 files changed

+93
-0
lines changed

2 files changed

+93
-0
lines changed

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(

python/neutron-understack/neutron_understack/utils.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from neutron.db import models_v2
44
from neutron.objects import ports as port_obj
55
from neutron.objects.network import NetworkSegment
6+
from neutron.objects.network_segment_range import NetworkSegmentRange
67
from neutron.plugins.ml2.driver_context import portbindings
78
from neutron.services.trunk.plugin import TrunkPlugin
89
from neutron_lib import constants
@@ -240,3 +241,55 @@ def vlan_segment_for_physnet(
240241
and segment[api.PHYSICAL_NETWORK] == physnet
241242
):
242243
return segment
244+
245+
246+
def fetch_vlan_network_segment_ranges() -> list[NetworkSegmentRange]:
247+
context = n_context.get_admin_context()
248+
249+
return NetworkSegmentRange.get_objects(context, network_type="vlan", shared=True)
250+
251+
252+
def allowed_tenant_vlan_id_ranges() -> list[tuple[int, int]]:
253+
all_vlan_range_objects = fetch_vlan_network_segment_ranges()
254+
all_vlan_ranges = [(vr.minimum, vr.maximum) for vr in all_vlan_range_objects]
255+
merged_ranges = merge_overlapped_ranges(all_vlan_ranges)
256+
return fetch_gaps_in_ranges(merged_ranges, (1, 4094))
257+
258+
259+
def merge_overlapped_ranges(ranges: list[tuple[int, int]]) -> list[tuple[int, int]]:
260+
merged = []
261+
for start, end in sorted(ranges):
262+
if not merged or start > merged[-1][1] + 1:
263+
merged.append([start, end])
264+
else:
265+
merged[-1][1] = max(merged[-1][1], end)
266+
return [tuple(lst) for lst in merged]
267+
268+
269+
def fetch_gaps_in_ranges(
270+
ranges: list[tuple[int, int]], default_range: tuple[int, int]
271+
) -> list[tuple[int, int]]:
272+
free_ranges = []
273+
prev_end = default_range[0] - 1
274+
for start, end in ranges:
275+
if start > prev_end + 1:
276+
free_ranges.append((prev_end + 1, start - 1))
277+
prev_end = end
278+
if prev_end < default_range[1]:
279+
free_ranges.append((prev_end + 1, default_range[1]))
280+
return free_ranges
281+
282+
283+
def segmentation_id_in_ranges(
284+
segmentation_id: int, ranges: list[tuple[int, int]]
285+
) -> bool:
286+
return any(start <= segmentation_id <= end for start, end in ranges)
287+
288+
289+
def printable_ranges(ranges: list[tuple[int, int]]) -> str:
290+
return ",".join(
291+
[
292+
f"{str(tpl[0])}-{str(tpl[1])}" if tpl[0] != tpl[1] else str(tpl[0])
293+
for tpl in ranges
294+
]
295+
)

0 commit comments

Comments
 (0)