Skip to content

Commit 0c37077

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

File tree

3 files changed

+104
-0
lines changed

3 files changed

+104
-0
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/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: 55 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
@@ -11,6 +12,7 @@
1112
from neutron_lib.api.definitions import segment as segment_def
1213
from neutron_lib.plugins import directory
1314
from neutron_lib.plugins.ml2 import api
15+
from oslo_config import cfg
1416

1517
from neutron_understack.ml2_type_annotations import NetworkSegmentDict
1618
from neutron_understack.ml2_type_annotations import PortContext
@@ -240,3 +242,56 @@ def vlan_segment_for_physnet(
240242
and segment[api.PHYSICAL_NETWORK] == physnet
241243
):
242244
return segment
245+
246+
247+
def fetch_vlan_network_segment_ranges() -> list[NetworkSegmentRange]:
248+
context = n_context.get_admin_context()
249+
250+
return NetworkSegmentRange.get_objects(context, network_type="vlan", shared=True)
251+
252+
253+
def allowed_tenant_vlan_id_ranges() -> list[tuple[int, int]]:
254+
all_vlan_range_objects = fetch_vlan_network_segment_ranges()
255+
all_vlan_ranges = [(vr.minimum, vr.maximum) for vr in all_vlan_range_objects]
256+
merged_ranges = merge_overlapped_ranges(all_vlan_ranges)
257+
default_range = tuple(cfg.CONF.ml2_understack.default_tenant_vlan_id_range)
258+
return fetch_gaps_in_ranges(merged_ranges, default_range)
259+
260+
261+
def merge_overlapped_ranges(ranges: list[tuple[int, int]]) -> list[tuple[int, int]]:
262+
merged = []
263+
for start, end in sorted(ranges):
264+
if not merged or start > merged[-1][1] + 1:
265+
merged.append([start, end])
266+
else:
267+
merged[-1][1] = max(merged[-1][1], end)
268+
return [tuple(lst) for lst in merged]
269+
270+
271+
def fetch_gaps_in_ranges(
272+
ranges: list[tuple[int, int]], default_range: tuple[int, int]
273+
) -> list[tuple[int, int]]:
274+
free_ranges = []
275+
prev_end = default_range[0] - 1
276+
for start, end in ranges:
277+
if start > prev_end + 1:
278+
free_ranges.append((prev_end + 1, start - 1))
279+
prev_end = end
280+
if prev_end < default_range[1]:
281+
free_ranges.append((prev_end + 1, default_range[1]))
282+
return free_ranges
283+
284+
285+
def segmentation_id_in_ranges(
286+
segmentation_id: int, ranges: list[tuple[int, int]]
287+
) -> bool:
288+
return any(start <= segmentation_id <= end for start, end in ranges)
289+
290+
291+
def printable_ranges(ranges: list[tuple[int, int]]) -> str:
292+
return ",".join(
293+
[
294+
f"{str(tpl[0])}-{str(tpl[1])}" if tpl[0] != tpl[1] else str(tpl[0])
295+
for tpl in ranges
296+
]
297+
)

0 commit comments

Comments
 (0)