Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions python/neutron-understack/neutron_understack/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@
"Nautobot."
),
),
cfg.ListOpt(
"default_tenant_vlan_id_range",
default=[1, 3799],
item_type=cfg.types.Integer(min=1, max=4094),
help=(
"List of 2 comma separated integers, that represents a VLAN range, that"
"will be used for mapped VLANs on the switches."
),
),
]

l3_svc_cisco_asa_opts = [
Expand Down
40 changes: 40 additions & 0 deletions python/neutron-understack/neutron_understack/trunk.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from neutron.objects.trunk import SubPort
from neutron.services.trunk.drivers import base as trunk_base
from neutron.services.trunk.models import Trunk
from neutron_lib import exceptions as exc
from neutron_lib.api.definitions import portbindings
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
Expand All @@ -21,6 +22,14 @@
SUPPORTED_SEGMENTATION_TYPES = (trunk_consts.SEGMENTATION_TYPE_VLAN,)


class SubportSegmentationIDError(exc.NeutronException):
message = (
"Segmentation ID: %(seg_id)s cannot be set to the Subport: "
"%(subport_id)s as it falls outside of allowed ranges: "
"%(network_segment_ranges)s. Please use different Segmentation ID."
)


class UnderStackTrunkDriver(trunk_base.DriverBase):
def __init__(
self,
Expand Down Expand Up @@ -96,13 +105,43 @@ def register(self, resource, event, trigger, payload=None):
def _handle_tenant_vlan_id_and_switchport_config(
self, subports: list[SubPort], trunk: Trunk
) -> None:
self._check_subports_segmentation_id(subports, trunk.id)
parent_port_obj = utils.fetch_port_object(trunk.port_id)

if utils.parent_port_is_bound(parent_port_obj):
self._add_subports_networks_to_parent_port_switchport(
parent_port_obj, subports
)

def _check_subports_segmentation_id(
self, subports: list[SubPort], trunk_id: str
) -> None:
"""Checks if a subport's segmentation_id is within the allowed range.
A switchport cannot have a mapped VLAN ID equal to the native VLAN ID.
Since the user specifies the VLAN ID (segmentation_id) when adding a
subport, an error is raised if it falls within any VLAN network segment
range, as these ranges are used to allocate VLAN tags for all VLAN
segments, including native VLANs.
The only case where this check is not required is for a network node
trunk, since its subport segmentation_ids are the same as the network
segment VLAN tags allocated to the subports. Therefore, there is no
possibility of conflict with the native VLAN.
"""
if trunk_id == cfg.CONF.ml2_understack.network_node_trunk_uuid:
return

ns_ranges = utils.allowed_tenant_vlan_id_ranges()
for subport in subports:
seg_id = subport.segmentation_id
if not utils.segmentation_id_in_ranges(seg_id, ns_ranges):
raise SubportSegmentationIDError(
seg_id=seg_id,
subport_id=subport.port_id,
network_segment_ranges=utils.printable_ranges(ns_ranges),
)

def configure_trunk(self, trunk_details: dict, port_id: str) -> None:
parent_port_obj = utils.fetch_port_object(port_id)
subports = trunk_details.get("sub_ports", [])
Expand Down Expand Up @@ -146,6 +185,7 @@ def _add_subports_networks_to_parent_port_switchport(
vlan_group_name = self.ironic_client.baremetal_port_physical_network(
local_link_info
)

self._handle_segment_allocation(subports, vlan_group_name, binding_host)

def clean_trunk(
Expand Down
55 changes: 55 additions & 0 deletions python/neutron-understack/neutron_understack/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from neutron.db import models_v2
from neutron.objects import ports as port_obj
from neutron.objects.network import NetworkSegment
from neutron.objects.network_segment_range import NetworkSegmentRange
from neutron.plugins.ml2.driver_context import portbindings
from neutron.services.trunk.plugin import TrunkPlugin
from neutron_lib import constants
Expand All @@ -11,6 +12,7 @@
from neutron_lib.api.definitions import segment as segment_def
from neutron_lib.plugins import directory
from neutron_lib.plugins.ml2 import api
from oslo_config import cfg

from neutron_understack.ml2_type_annotations import NetworkSegmentDict
from neutron_understack.ml2_type_annotations import PortContext
Expand Down Expand Up @@ -240,3 +242,56 @@ def vlan_segment_for_physnet(
and segment[api.PHYSICAL_NETWORK] == physnet
):
return segment


def fetch_vlan_network_segment_ranges() -> list[NetworkSegmentRange]:
context = n_context.get_admin_context()

return NetworkSegmentRange.get_objects(context, network_type="vlan", shared=True)


def allowed_tenant_vlan_id_ranges() -> list[tuple[int, int]]:
all_vlan_range_objects = fetch_vlan_network_segment_ranges()
all_vlan_ranges = [(vr.minimum, vr.maximum) for vr in all_vlan_range_objects]
merged_ranges = merge_overlapped_ranges(all_vlan_ranges)
default_range = tuple(cfg.CONF.ml2_understack.default_tenant_vlan_id_range)
return fetch_gaps_in_ranges(merged_ranges, default_range)


def merge_overlapped_ranges(ranges: list[tuple[int, int]]) -> list[tuple[int, int]]:
merged = []
for start, end in sorted(ranges):
if not merged or start > merged[-1][1] + 1:
merged.append([start, end])
else:
merged[-1][1] = max(merged[-1][1], end)
return [tuple(lst) for lst in merged]


def fetch_gaps_in_ranges(
ranges: list[tuple[int, int]], default_range: tuple[int, int]
) -> list[tuple[int, int]]:
free_ranges = []
prev_end = default_range[0] - 1
for start, end in ranges:
if start > prev_end + 1:
free_ranges.append((prev_end + 1, start - 1))
prev_end = end
if prev_end < default_range[1]:
free_ranges.append((prev_end + 1, default_range[1]))
return free_ranges


def segmentation_id_in_ranges(
segmentation_id: int, ranges: list[tuple[int, int]]
) -> bool:
return any(start <= segmentation_id <= end for start, end in ranges)


def printable_ranges(ranges: list[tuple[int, int]]) -> str:
return ",".join(
[
f"{str(tpl[0])}-{str(tpl[1])}" if tpl[0] != tpl[1] else str(tpl[0])
for tpl in ranges
]
)
Loading