neutron/neutron/plugins/ml2/drivers/helpers.py

230 lines
9.8 KiB
Python

# Copyright (c) 2014 Thales Services SAS
# 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 random
from neutron_lib import constants as p_const
from neutron_lib import context as neutron_ctx
from neutron_lib.db import api as db_api
from neutron_lib import exceptions
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 p_utils
from neutron_lib.utils import helpers
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log
from sqlalchemy import and_
from sqlalchemy import sql
from neutron.db.models import network_segment_range as range_model
from neutron.objects import base as base_obj
LOG = log.getLogger(__name__)
IDPOOL_SELECT_SIZE = 100
class BaseTypeDriver(api.ML2TypeDriver):
"""BaseTypeDriver for functions common to Segment and flat."""
def __init__(self):
try:
self.physnet_mtus = helpers.parse_mappings(
cfg.CONF.ml2.physical_network_mtus, unique_values=False
)
except Exception as e:
LOG.error("Failed to parse physical_network_mtus: %s", e)
self.physnet_mtus = []
def get_mtu(self, physical_network=None):
return p_utils.get_deployment_physnet_mtu()
class SegmentTypeDriver(BaseTypeDriver):
"""SegmentTypeDriver for segment allocation.
Provide methods helping to perform segment allocation fully or partially
specified.
"""
def __init__(self, model):
super(SegmentTypeDriver, self).__init__()
if issubclass(model, base_obj.NeutronDbObject):
self.model = model.db_model
else:
self.model = model
self.primary_keys = set(dict(self.model.__table__.columns))
self.primary_keys.remove("allocated")
# TODO(ataraday): get rid of this method when old TypeDriver won't be used
def _get_session(self, arg):
if isinstance(arg, neutron_ctx.Context):
return arg.session, db_api.CONTEXT_WRITER.using(arg)
return arg, arg.session.begin(subtransactions=True)
def build_segment_query(self, session, **filters):
# Only uses filters that correspond to columns defined by this model.
# Subclasses may use/support additional filters
columns = set(dict(self.model.__table__.columns))
model_filters = dict((k, filters[k])
for k in columns & set(filters.keys()))
return [session.query(self.model).filter_by(allocated=False,
**model_filters)]
def build_segment_queries_for_tenant_and_shared_ranges(self, session,
**filters):
"""Enforces that segments are allocated from network segment ranges
that are owned by the tenant, and then from shared ranges, but never
from ranges owned by other tenants.
This method also enforces that other network segment range attributes
are used when constraining the set of possible segments to be used.
"""
network_type = self.get_type()
project_id = filters.pop('project_id', None)
columns = set(dict(self.model.__table__.columns))
model_filters = dict((k, filters[k])
for k in columns & set(filters.keys()))
query = (session.query(self.model)
.filter_by(allocated=False, **model_filters))
query = query.join(
range_model.NetworkSegmentRange,
and_(range_model.NetworkSegmentRange.network_type == network_type,
self.model.physical_network ==
range_model.NetworkSegmentRange.physical_network if
network_type == p_const.TYPE_VLAN else
sql.expression.true()))
query = query.filter(and_(self.model_segmentation_id >=
range_model.NetworkSegmentRange.minimum,
self.model_segmentation_id <=
range_model.NetworkSegmentRange.maximum))
query_project_id = (query.filter(
range_model.NetworkSegmentRange.project_id == project_id) if
project_id is not None else [])
query_shared = query.filter(
range_model.NetworkSegmentRange.shared == sql.expression.true())
return [query_project_id] + [query_shared]
def allocate_fully_specified_segment(self, context, **raw_segment):
"""Allocate segment fully specified by raw_segment.
If segment exists, then try to allocate it and return db object
If segment does not exists, then try to create it and return db object
If allocation/creation failed, then return None
"""
network_type = self.get_type()
session, ctx_manager = self._get_session(context)
try:
with ctx_manager:
alloc = (
session.query(self.model).filter_by(**raw_segment).
first())
if alloc:
if alloc.allocated:
# Segment already allocated
return
else:
# Segment not allocated
LOG.debug("%(type)s segment %(segment)s allocate "
"started ",
{"type": network_type,
"segment": raw_segment})
count = (session.query(self.model).
filter_by(allocated=False, **raw_segment).
update({"allocated": True}))
if count:
LOG.debug("%(type)s segment %(segment)s allocate "
"done ",
{"type": network_type,
"segment": raw_segment})
return alloc
# Segment allocated or deleted since select
LOG.debug("%(type)s segment %(segment)s allocate "
"failed: segment has been allocated or "
"deleted",
{"type": network_type,
"segment": raw_segment})
# Segment to create or already allocated
LOG.debug("%(type)s segment %(segment)s create started",
{"type": network_type, "segment": raw_segment})
alloc = self.model(allocated=True, **raw_segment)
alloc.save(session)
LOG.debug("%(type)s segment %(segment)s create done",
{"type": network_type, "segment": raw_segment})
except db_exc.DBDuplicateEntry:
# Segment already allocated (insert failure)
alloc = None
LOG.debug("%(type)s segment %(segment)s create failed",
{"type": network_type, "segment": raw_segment})
return alloc
def allocate_partially_specified_segment(self, context, **filters):
"""Allocate model segment from pool partially specified by filters.
Return allocated db object or None.
"""
network_type = self.get_type()
session, ctx_manager = self._get_session(context)
with ctx_manager:
queries = (self.build_segment_queries_for_tenant_and_shared_ranges(
session, **filters)
if directory.get_plugin(
plugin_constants.NETWORK_SEGMENT_RANGE) else
self.build_segment_query(session, **filters))
for select in queries:
# Selected segment can be allocated before update by someone
# else
allocs = select.limit(IDPOOL_SELECT_SIZE).all()
if not allocs:
# No resource available
continue
alloc = random.choice(allocs)
raw_segment = dict((k, alloc[k]) for k in self.primary_keys)
LOG.debug("%(type)s segment allocate from pool "
"started with %(segment)s ",
{"type": network_type,
"segment": raw_segment})
count = (session.query(self.model).
filter_by(allocated=False, **raw_segment).
update({"allocated": True}))
if count:
LOG.debug("%(type)s segment allocate from pool "
"success with %(segment)s ",
{"type": network_type,
"segment": raw_segment})
return alloc
# Segment allocated since select
LOG.debug("Allocate %(type)s segment from pool "
"failed with segment %(segment)s",
{"type": network_type,
"segment": raw_segment})
# saving real exception in case we exceeded amount of attempts
raise db_exc.RetryRequest(
exceptions.NoNetworkFoundInMaximumAllowedAttempts())