
Since [1], the physical network VLAN ranges parser populates the ranges for those entries without a defined range, allowing all valid VLAN ranges ([1, 4094]). Some VLAN driver tests, relying on the previous implementation, considered that the physical network without a defined VLAN range does not have segments to allocated (those segments are created on the fly by "SegmentTypeDriverallocate_fully_specified_segment). Since [1], all physical network segments are stored in the "ml2_vlan_allocations" table and set as non allocated. This patch also reverts [2]. When the physical networks are defined in "network_vlan_ranges", there is no distinction between tenant and provided networks; the physical network segments are assigned by the user. It is possible to create a provider network without defining the segmentation ID, it will be provided by the Neutron VLAN driver, if there are free segments for the required physical network. [1]https://review.opendev.org/c/openstack/neutron-lib/+/779515 [2]1376df7873c2ac77c256ab2fed928de41a2c1d58 Closes-Bug: #1919280 Related-Bug: #1918274 Related-Bug: #1649750 Change-Id: I191e020ddb97dcf8fb41139d35bfd699e125379b
330 lines
15 KiB
Python
330 lines
15 KiB
Python
# Copyright (c) 2013 OpenStack Foundation
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import collections
|
|
import sys
|
|
|
|
from neutron_lib import constants as p_const
|
|
from neutron_lib import context
|
|
from neutron_lib.db import api as db_api
|
|
from neutron_lib import exceptions as exc
|
|
from neutron_lib.plugins import constants as plugin_constants
|
|
from neutron_lib.plugins import directory
|
|
from neutron_lib.plugins.ml2 import api
|
|
from neutron_lib.plugins import utils as plugin_utils
|
|
from oslo_config import cfg
|
|
from oslo_log import log
|
|
from oslo_utils import uuidutils
|
|
|
|
from neutron._i18n import _
|
|
from neutron.conf.plugins.ml2.drivers import driver_type
|
|
from neutron.db.models.plugins.ml2 import vlanallocation as vlan_alloc_model
|
|
from neutron.objects import network_segment_range as range_obj
|
|
from neutron.objects.plugins.ml2 import vlanallocation as vlanalloc
|
|
from neutron.plugins.ml2.drivers import helpers
|
|
from neutron.services.network_segment_range import plugin as range_plugin
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
driver_type.register_ml2_drivers_vlan_opts()
|
|
|
|
|
|
class VlanTypeDriver(helpers.SegmentTypeDriver):
|
|
"""Manage state for VLAN networks with ML2.
|
|
|
|
The VlanTypeDriver implements the 'vlan' network_type. VLAN
|
|
network segments provide connectivity between VMs and other
|
|
devices using any connected IEEE 802.1Q conformant
|
|
physical_network segmented into virtual networks via IEEE 802.1Q
|
|
headers. Up to 4094 VLAN network segments can exist on each
|
|
available physical_network.
|
|
"""
|
|
|
|
def __init__(self):
|
|
super(VlanTypeDriver, self).__init__(vlanalloc.VlanAllocation)
|
|
self.model_segmentation_id = vlan_alloc_model.VlanAllocation.vlan_id
|
|
self._parse_network_vlan_ranges()
|
|
|
|
@db_api.retry_db_errors
|
|
def _populate_new_default_network_segment_ranges(self):
|
|
ctx = context.get_admin_context()
|
|
for (physical_network, vlan_ranges) in (
|
|
self.network_vlan_ranges.items()):
|
|
for vlan_min, vlan_max in vlan_ranges:
|
|
res = {
|
|
'id': uuidutils.generate_uuid(),
|
|
'name': '',
|
|
'default': True,
|
|
'shared': True,
|
|
'network_type': p_const.TYPE_VLAN,
|
|
'physical_network': physical_network,
|
|
'minimum': vlan_min,
|
|
'maximum': vlan_max}
|
|
with db_api.CONTEXT_WRITER.using(ctx):
|
|
new_default_range_obj = (
|
|
range_obj.NetworkSegmentRange(ctx, **res))
|
|
new_default_range_obj.create()
|
|
|
|
def _parse_network_vlan_ranges(self):
|
|
try:
|
|
self.network_vlan_ranges = plugin_utils.parse_network_vlan_ranges(
|
|
cfg.CONF.ml2_type_vlan.network_vlan_ranges)
|
|
except Exception:
|
|
LOG.exception("Failed to parse network_vlan_ranges. "
|
|
"Service terminated!")
|
|
sys.exit(1)
|
|
LOG.info("Network VLAN ranges: %s", self.network_vlan_ranges)
|
|
|
|
@db_api.retry_db_errors
|
|
def _sync_vlan_allocations(self):
|
|
ctx = context.get_admin_context()
|
|
with db_api.CONTEXT_WRITER.using(ctx):
|
|
# VLAN ranges per physical network:
|
|
# {phy1: [(1, 10), (30, 50)], ...}
|
|
ranges = self.get_network_segment_ranges()
|
|
|
|
# Delete those VLAN registers from unconfigured physical networks
|
|
physnets = vlanalloc.VlanAllocation.get_physical_networks(ctx)
|
|
physnets_unconfigured = physnets - set(ranges)
|
|
if physnets_unconfigured:
|
|
LOG.debug('Removing any VLAN register on physical networks %s',
|
|
physnets_unconfigured)
|
|
vlanalloc.VlanAllocation.delete_physical_networks(
|
|
ctx, physnets_unconfigured)
|
|
|
|
# Get existing allocations for all configured physical networks
|
|
allocations = collections.defaultdict(list)
|
|
for alloc in vlanalloc.VlanAllocation.get_objects(ctx):
|
|
allocations[alloc.physical_network].append(alloc)
|
|
|
|
for physical_network, vlan_ranges in ranges.items():
|
|
# determine current configured allocatable vlans for
|
|
# this physical network
|
|
vlan_ids = set()
|
|
for vlan_min, vlan_max in vlan_ranges:
|
|
vlan_ids |= set(range(vlan_min, vlan_max + 1))
|
|
|
|
# remove from table unallocated vlans not currently
|
|
# allocatable
|
|
if physical_network in allocations:
|
|
for alloc in allocations[physical_network]:
|
|
try:
|
|
# see if vlan is allocatable
|
|
vlan_ids.remove(alloc.vlan_id)
|
|
except KeyError:
|
|
# it's not allocatable, so check if its allocated
|
|
if not alloc.allocated:
|
|
# it's not, so remove it from table
|
|
LOG.debug("Removing vlan %(vlan_id)s on "
|
|
"physical network "
|
|
"%(physical_network)s from pool",
|
|
{'vlan_id': alloc.vlan_id,
|
|
'physical_network':
|
|
physical_network})
|
|
# This UPDATE WHERE statement blocks anyone
|
|
# from concurrently changing the allocation
|
|
# values to True while our transaction is
|
|
# open so we don't accidentally delete
|
|
# allocated segments. If someone has already
|
|
# allocated, update_objects will return 0 so we
|
|
# don't delete.
|
|
if vlanalloc.VlanAllocation.update_objects(
|
|
ctx, values={'allocated': False},
|
|
allocated=False, vlan_id=alloc.vlan_id,
|
|
physical_network=physical_network):
|
|
alloc.delete()
|
|
del allocations[physical_network]
|
|
|
|
# Add missing allocatable VLAN registers for "physical_network"
|
|
vlanalloc.VlanAllocation.bulk_create(ctx, physical_network,
|
|
vlan_ids)
|
|
|
|
@db_api.retry_db_errors
|
|
def _get_network_segment_ranges_from_db(self):
|
|
ranges = {}
|
|
ctx = context.get_admin_context()
|
|
with db_api.CONTEXT_READER.using(ctx):
|
|
range_objs = (range_obj.NetworkSegmentRange.get_objects(
|
|
ctx, network_type=self.get_type()))
|
|
for obj in range_objs:
|
|
physical_network = obj['physical_network']
|
|
if physical_network not in ranges:
|
|
ranges[physical_network] = []
|
|
ranges[physical_network].append((obj['minimum'],
|
|
obj['maximum']))
|
|
return ranges
|
|
|
|
def get_type(self):
|
|
return p_const.TYPE_VLAN
|
|
|
|
def initialize(self):
|
|
if not range_plugin.is_network_segment_range_enabled():
|
|
# service plugins are initialized/loaded after the ML2 driver
|
|
# initialization. Thus, we base on the information whether
|
|
# ``network_segment_range`` service plugin is enabled/defined in
|
|
# ``neutron.conf`` to decide whether to skip the first time sync
|
|
# allocation during driver initialization, instead of using the
|
|
# directory.get_plugin() method - the normal way used elsewhere to
|
|
# check if a plugin is loaded.
|
|
self._sync_vlan_allocations()
|
|
LOG.info("VlanTypeDriver initialization complete")
|
|
|
|
def initialize_network_segment_range_support(self):
|
|
self._delete_expired_default_network_segment_ranges()
|
|
self._populate_new_default_network_segment_ranges()
|
|
# Override self.network_vlan_ranges with the network segment range
|
|
# information from DB and then do a sync_allocations since the
|
|
# segment range service plugin has not yet been loaded at this
|
|
# initialization time.
|
|
self.network_vlan_ranges = self._get_network_segment_ranges_from_db()
|
|
self._sync_vlan_allocations()
|
|
|
|
def update_network_segment_range_allocations(self):
|
|
self._sync_vlan_allocations()
|
|
|
|
def get_network_segment_ranges(self):
|
|
"""Get the driver network segment ranges.
|
|
|
|
Queries all VLAN network segment ranges from DB if the
|
|
``NETWORK_SEGMENT_RANGE`` service plugin is enabled. Otherwise,
|
|
they will be loaded from the host config file - `ml2_conf.ini`.
|
|
"""
|
|
ranges = self.network_vlan_ranges
|
|
if directory.get_plugin(plugin_constants.NETWORK_SEGMENT_RANGE):
|
|
ranges = self._get_network_segment_ranges_from_db()
|
|
|
|
return ranges
|
|
|
|
def is_partial_segment(self, segment):
|
|
return segment.get(api.SEGMENTATION_ID) is None
|
|
|
|
def validate_provider_segment(self, segment):
|
|
physical_network = segment.get(api.PHYSICAL_NETWORK)
|
|
segmentation_id = segment.get(api.SEGMENTATION_ID)
|
|
ranges = self.get_network_segment_ranges()
|
|
if physical_network:
|
|
if physical_network not in ranges:
|
|
msg = (_("physical_network '%s' unknown "
|
|
"for VLAN provider network") % physical_network)
|
|
raise exc.InvalidInput(error_message=msg)
|
|
if segmentation_id is not None:
|
|
if not plugin_utils.is_valid_vlan_tag(segmentation_id):
|
|
msg = (_("segmentation_id out of range (%(min)s through "
|
|
"%(max)s)") %
|
|
{'min': p_const.MIN_VLAN_TAG,
|
|
'max': p_const.MAX_VLAN_TAG})
|
|
raise exc.InvalidInput(error_message=msg)
|
|
elif segmentation_id is not None:
|
|
msg = _("segmentation_id requires physical_network for VLAN "
|
|
"provider network")
|
|
raise exc.InvalidInput(error_message=msg)
|
|
|
|
for key, value in segment.items():
|
|
if value and key not in [api.NETWORK_TYPE,
|
|
api.PHYSICAL_NETWORK,
|
|
api.SEGMENTATION_ID]:
|
|
msg = _("%s prohibited for VLAN provider network") % key
|
|
raise exc.InvalidInput(error_message=msg)
|
|
|
|
def reserve_provider_segment(self, context, segment, filters=None):
|
|
filters = filters or {}
|
|
project_id = filters.get('project_id')
|
|
filters = {}
|
|
physical_network = segment.get(api.PHYSICAL_NETWORK)
|
|
if physical_network is not None:
|
|
filters['physical_network'] = physical_network
|
|
vlan_id = segment.get(api.SEGMENTATION_ID)
|
|
if vlan_id is not None:
|
|
filters['vlan_id'] = vlan_id
|
|
|
|
if self.is_partial_segment(segment):
|
|
if (directory.get_plugin(
|
|
plugin_constants.NETWORK_SEGMENT_RANGE) and project_id):
|
|
filters['project_id'] = project_id
|
|
alloc = self.allocate_partially_specified_segment(
|
|
context, **filters)
|
|
if not alloc:
|
|
raise exc.NoNetworkAvailable()
|
|
else:
|
|
alloc = self.allocate_fully_specified_segment(
|
|
context, **filters)
|
|
if not alloc:
|
|
raise exc.VlanIdInUse(**filters)
|
|
|
|
return {api.NETWORK_TYPE: p_const.TYPE_VLAN,
|
|
api.PHYSICAL_NETWORK: alloc.physical_network,
|
|
api.SEGMENTATION_ID: alloc.vlan_id,
|
|
api.MTU: self.get_mtu(alloc.physical_network)}
|
|
|
|
def allocate_tenant_segment(self, context, filters=None):
|
|
filters = filters or {}
|
|
ranges = self.get_network_segment_ranges()
|
|
for physnet in ranges:
|
|
filters['physical_network'] = physnet
|
|
alloc = self.allocate_partially_specified_segment(
|
|
context, **filters)
|
|
if alloc:
|
|
break
|
|
else:
|
|
return
|
|
return {api.NETWORK_TYPE: p_const.TYPE_VLAN,
|
|
api.PHYSICAL_NETWORK: alloc.physical_network,
|
|
api.SEGMENTATION_ID: alloc.vlan_id,
|
|
api.MTU: self.get_mtu(alloc.physical_network)}
|
|
|
|
def release_segment(self, context, segment):
|
|
physical_network = segment[api.PHYSICAL_NETWORK]
|
|
vlan_id = segment[api.SEGMENTATION_ID]
|
|
|
|
vlan_ranges = self.get_network_segment_ranges()
|
|
ranges = vlan_ranges.get(physical_network, [])
|
|
inside = any(lo <= vlan_id <= hi for lo, hi in ranges)
|
|
count = False
|
|
|
|
with db_api.CONTEXT_WRITER.using(context):
|
|
alloc = vlanalloc.VlanAllocation.get_object(
|
|
context, physical_network=physical_network, vlan_id=vlan_id)
|
|
if alloc:
|
|
if inside and alloc.allocated:
|
|
count = True
|
|
alloc.allocated = False
|
|
alloc.update()
|
|
LOG.debug("Releasing vlan %(vlan_id)s on physical "
|
|
"network %(physical_network)s to pool",
|
|
{'vlan_id': vlan_id,
|
|
'physical_network': physical_network})
|
|
else:
|
|
count = True
|
|
alloc.delete()
|
|
LOG.debug("Releasing vlan %(vlan_id)s on physical "
|
|
"network %(physical_network)s outside pool",
|
|
{'vlan_id': vlan_id,
|
|
'physical_network': physical_network})
|
|
|
|
if not count:
|
|
LOG.warning("No vlan_id %(vlan_id)s found on physical "
|
|
"network %(physical_network)s",
|
|
{'vlan_id': vlan_id,
|
|
'physical_network': physical_network})
|
|
|
|
def get_mtu(self, physical_network):
|
|
seg_mtu = super(VlanTypeDriver, self).get_mtu()
|
|
mtu = []
|
|
if seg_mtu > 0:
|
|
mtu.append(seg_mtu)
|
|
if physical_network in self.physnet_mtus:
|
|
mtu.append(int(self.physnet_mtus[physical_network]))
|
|
return min(mtu) if mtu else 0
|