Add framework for managing segment id pools.

Also added the virst implementation, vxlan, to the NVP driver.
This commit is contained in:
Brad Morgan
2016-01-23 01:17:04 -08:00
parent cb3b2b2a0d
commit ed46b2f0b4
14 changed files with 1373 additions and 5 deletions

View File

@@ -0,0 +1,100 @@
# Copyright (c) 2013 OpenStack Foundation
#
# 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.
from neutron.api import extensions
from neutron import manager
from neutron import wsgi
from oslo_log import log as logging
RESOURCE_NAME = 'segment_allocation_range'
RESOURCE_COLLECTION = RESOURCE_NAME + "s"
EXTENDED_ATTRIBUTES_2_0 = {
RESOURCE_COLLECTION: {}
}
attr_dict = EXTENDED_ATTRIBUTES_2_0[RESOURCE_COLLECTION]
attr_dict[RESOURCE_NAME] = {'allow_post': True,
'allow_put': False,
'is_visible': True}
LOG = logging.getLogger(__name__)
class SegmentAllocationRangesController(wsgi.Controller):
def __init__(self, plugin):
self._resource_name = RESOURCE_NAME
self._plugin = plugin
def create(self, request, body=None):
body = self._deserialize(request.body, request.get_content_type())
return {"segment_allocation_range":
self._plugin.create_segment_allocation_range(
request.context, body)}
def index(self, request):
context = request.context
return {"segment_allocation_ranges":
self._plugin.get_segment_allocation_ranges(
context, **request.GET)}
def show(self, request, id):
context = request.context
return {"segment_allocation_range":
self._plugin.get_segment_allocation_range(context, id)}
def delete(self, request, id, **kwargs):
context = request.context
return self._plugin.delete_segment_allocation_range(context, id)
class Segment_allocation_ranges(extensions.ExtensionDescriptor):
"""Segment Allocation Range support."""
@classmethod
def get_name(cls):
return "Network segment allocation ranges."
@classmethod
def get_alias(cls):
return RESOURCE_COLLECTION
@classmethod
def get_description(cls):
return ("Expose functions for cloud admin to manage network"
"segment allocation ranges.")
@classmethod
def get_namespace(cls):
return ("http://docs.openstack.org/network/ext/"
"segment-allocation-ranges/api/v2.0")
@classmethod
def get_updated(cls):
return "2013-02-19T10:00:00-00:00"
def get_extended_resources(self, version):
if version == "2.0":
return EXTENDED_ATTRIBUTES_2_0
else:
return {}
@classmethod
def get_resources(cls):
"""Returns Ext Resources."""
plugin = manager.NeutronManager.get_plugin()
controller = SegmentAllocationRangesController(plugin)
return [extensions.ResourceExtension(
Segment_allocation_ranges.get_alias(),
controller)]

View File

@@ -1051,3 +1051,63 @@ def _lock_delete(context, target):
def lock_holder_delete(context, target, lock_holder):
context.session.delete(lock_holder)
_lock_delete(context, target)
@scoped
def segment_allocation_range_find(context, lock_mode=False, **filters):
query = context.session.query(models.SegmentAllocationRange)
if lock_mode:
query = query.with_lockmode("update")
model_filters = _model_query(
context, models.SegmentAllocationRange, filters)
query = query.filter(*model_filters)
return query
def segment_allocation_find(context, lock_mode=False, **filters):
"""Query for segment allocations."""
range_ids = filters.pop("segment_allocation_range_ids", None)
query = context.session.query(models.SegmentAllocation)
if lock_mode:
query = query.with_lockmode("update")
query = query.filter_by(**filters)
# Optionally filter by given list of range ids
if range_ids:
query.filter(
models.SegmentAllocation.segment_allocation_range_id.in_(
range_ids))
return query
def segment_allocation_update(context, sa, **sa_dict):
sa.update(sa_dict)
context.session.add(sa)
return sa
def segment_allocation_range_populate_bulk(context, sa_dicts):
"""Bulk-insert deallocated segment allocations.
NOTE(morgabra): This is quite performant when populating large ranges,
but you don't get any ORM conveniences or protections here.
"""
context.session.bulk_insert_mappings(
models.SegmentAllocation,
sa_dicts
)
def segment_allocation_range_create(context, **sa_range_dict):
new_range = models.SegmentAllocationRange()
new_range.update(sa_range_dict)
context.session.add(new_range)
return new_range
def segment_allocation_range_delete(context, sa_range):
context.session.delete(sa_range)

View File

@@ -0,0 +1,72 @@
"""add_segment_allocations
Revision ID: 1bd7cff90384
Revises: 374c1bdb4480
Create Date: 2016-01-25 19:26:27.899610
"""
# revision identifiers, used by Alembic.
revision = '1bd7cff90384'
down_revision = '374c1bdb4480'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'quark_segment_allocation_ranges',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('segment_id', sa.String(length=36), nullable=True),
sa.Column('segment_type', sa.String(length=36), nullable=True),
sa.Column('first_id', sa.BigInteger(), nullable=False),
sa.Column('last_id', sa.BigInteger(), nullable=False),
sa.Column('do_not_use', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id'),
mysql_engine='InnoDB'
)
op.create_index(op.f('ix_quark_segment_allocation_ranges_segment_id'),
'quark_segment_allocation_ranges', ['segment_id'],
unique=False)
op.create_index(op.f('ix_quark_segment_allocation_ranges_segment_type'),
'quark_segment_allocation_ranges', ['segment_type'],
unique=False)
op.create_table(
'quark_segment_allocations',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.BigInteger(), autoincrement=False,
nullable=False),
sa.Column('segment_id', sa.String(length=36), nullable=False),
sa.Column('segment_type', sa.String(length=36), nullable=False),
sa.Column('segment_allocation_range_id', sa.String(length=36),
nullable=True),
sa.Column('network_id', sa.String(length=36), nullable=True),
sa.Column('deallocated', sa.Boolean(), nullable=True),
sa.Column('deallocated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['segment_allocation_range_id'],
['quark_segment_allocation_ranges.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', 'segment_id', 'segment_type'),
mysql_engine='InnoDB'
)
op.create_index(op.f('ix_quark_segment_allocations_deallocated'),
'quark_segment_allocations', ['deallocated'],
unique=False)
op.create_index(op.f('ix_quark_segment_allocations_deallocated_at'),
'quark_segment_allocations', ['deallocated_at'],
unique=False)
def downgrade():
op.drop_index(op.f('ix_quark_segment_allocations_deallocated_at'),
table_name='quark_segment_allocations')
op.drop_index(op.f('ix_quark_segment_allocations_deallocated'),
table_name='quark_segment_allocations')
op.drop_table('quark_segment_allocations')
op.drop_index(op.f('ix_quark_segment_allocation_ranges_segment_type'),
table_name='quark_segment_allocation_ranges')
op.drop_index(op.f('ix_quark_segment_allocation_ranges_segment_id'),
table_name='quark_segment_allocation_ranges')
op.drop_table('quark_segment_allocation_ranges')

View File

@@ -1 +1 @@
374c1bdb4480
1bd7cff90384

View File

@@ -558,3 +558,37 @@ class LockHolder(BASEV2):
sa.ForeignKey("quark_locks.id"),
nullable=False)
name = sa.Column(sa.String(255), nullable=True)
class SegmentAllocation(BASEV2):
"""A segment allocation."""
__tablename__ = "quark_segment_allocations"
# a particular segment id is unique across the segment and type - this data
# is denormalized to give us some safety around allocations, as well
# as allow us to look up allocations to reallocate without a join.
id = sa.Column(sa.BigInteger(), primary_key=True, autoincrement=False)
segment_id = sa.Column(sa.String(36), primary_key=True)
segment_type = sa.Column(sa.String(36), primary_key=True)
segment_allocation_range_id = sa.Column(
sa.String(36),
sa.ForeignKey("quark_segment_allocation_ranges.id",
ondelete="CASCADE"))
network_id = sa.Column(sa.String(36), nullable=True)
deallocated = sa.Column(sa.Boolean(), index=True)
deallocated_at = sa.Column(sa.DateTime(), index=True)
class SegmentAllocationRange(BASEV2, models.HasId):
"""Ranges of space for segment ids available for allocation."""
__tablename__ = "quark_segment_allocation_ranges"
segment_id = sa.Column(sa.String(36), index=True)
segment_type = sa.Column(sa.String(36), index=True)
first_id = sa.Column(sa.BigInteger(), nullable=False)
last_id = sa.Column(sa.BigInteger(), nullable=False)
do_not_use = sa.Column(sa.Boolean(), default=False, nullable=False)

View File

@@ -29,6 +29,7 @@ from quark.drivers import base
from quark.drivers import security_groups as sg_driver
from quark.environment import Capabilities
from quark import exceptions
from quark import segment_allocations
from quark import utils
LOG = logging.getLogger(__name__)
@@ -42,6 +43,10 @@ nvp_opts = [
cfg.StrOpt('default_tz_type',
help=_('The type of connector to use for the default tz'),
default="stt"),
cfg.ListOpt('additional_default_tz_types',
default=[],
help=_('List of additional default tz types to bind to the '
'default tz')),
cfg.StrOpt('default_tz',
help=_('The default transport zone UUID')),
cfg.ListOpt('controller_connection',
@@ -81,10 +86,43 @@ physical_net_type_map = {
"flat": "bridge",
"bridge": "bridge",
"vlan": "bridge",
"local": "local"
"local": "local",
}
CONF.register_opts(nvp_opts, "NVP")
SA_REGISTRY = segment_allocations.REGISTRY
class TransportZoneBinding(object):
net_type = None
def add(self, context, switch, tz_id, network_id):
raise NotImplementedError()
def remove(self, context, tz_id, network_id):
raise NotImplementedError()
class VXLanTransportZoneBinding(TransportZoneBinding):
net_type = 'vxlan'
def add(self, context, switch, tz_id, network_id):
driver = SA_REGISTRY.get_strategy(self.net_type)
alloc = driver.allocate(context, tz_id, network_id)
switch.transport_zone(
tz_id, self.net_type, vxlan_id=alloc["id"])
def remove(self, context, tz_id, network_id):
driver = SA_REGISTRY.get_strategy(self.net_type)
driver.deallocate(context, tz_id, network_id)
# A map of net_type (vlan, vxlan) to TransportZoneBinding impl.
TZ_BINDINGS = {
VXLanTransportZoneBinding.net_type: VXLanTransportZoneBinding()
}
def _tag_roll(tags):
@@ -105,6 +143,7 @@ class NVPDriver(base.BaseDriver):
self.sg_driver = None
if Capabilities.SECURITY_GROUPS in CONF.QUARK.environment_capabilities:
self.sg_driver = sg_driver.SecurityGroupDriver()
super(NVPDriver, self).__init__()
@classmethod
@@ -234,6 +273,11 @@ class NVPDriver(base.BaseDriver):
for switch in lswitches["results"]:
try:
self._lswitch_delete(context, switch["uuid"])
# NOTE(morgabra) If we haven't thrown here, we can be sure the
# resource was deleted from NSX. So we give a chance for any
# previously allocated segment ids to deallocate.
self._remove_default_tz_bindings(
context, network_id)
except aiclib.core.AICException as ae:
if ae.code == 404:
LOG.info("LSwitch/Network %s not found in NVP."
@@ -570,7 +614,7 @@ class NVPDriver(base.BaseDriver):
if phys_net and not net_type:
raise exceptions.ProvidernetParamError(
msg="provider:network_type parameter required")
if net_type not in ("bridge", "vlan") and segment_id:
if net_type not in ("bridge", "vlan", "vxlan") and segment_id:
raise exceptions.SegmentIdUnsupported(net_type=net_type)
if net_type == "vlan" and not segment_id:
raise exceptions.SegmentIdRequired(net_type=net_type)
@@ -588,6 +632,54 @@ class NVPDriver(base.BaseDriver):
transport_type=phys_type,
vlan_id=segment_id)
def _add_default_tz_bindings(self, context, switch, network_id):
"""Configure any additional default transport zone bindings."""
default_tz = CONF.NVP.default_tz
# If there is no default tz specified it's pointless to try
# and add any additional default tz bindings.
if not default_tz:
LOG.warn("additional_default_tz_types specified, "
"but no default_tz. Skipping "
"_add_default_tz_bindings().")
return
# This should never be called without a neutron network uuid,
# we require it to bind some segment allocations.
if not network_id:
LOG.warn("neutron network_id not specified, skipping "
"_add_default_tz_bindings()")
return
for net_type in CONF.NVP.additional_default_tz_types:
if net_type in TZ_BINDINGS:
binding = TZ_BINDINGS[net_type]
binding.add(context, switch, default_tz, network_id)
else:
LOG.warn("Unknown default tz type %s" % (net_type))
def _remove_default_tz_bindings(self, context, network_id):
"""Deconfigure any additional default transport zone bindings."""
default_tz = CONF.NVP.default_tz
if not default_tz:
LOG.warn("additional_default_tz_types specified, "
"but no default_tz. Skipping "
"_remove_default_tz_bindings().")
return
if not network_id:
LOG.warn("neutron network_id not specified, skipping "
"_remove_default_tz_bindings()")
return
for net_type in CONF.NVP.additional_default_tz_types:
if net_type in TZ_BINDINGS:
binding = TZ_BINDINGS[net_type]
binding.remove(context, default_tz, network_id)
else:
LOG.warn("Unknown default tz type %s" % (net_type))
@utils.retry_loop(CONF.NVP.operation_retries)
def _lswitch_create(self, context, network_name=None, tags=None,
network_id=None, phys_net=None,
@@ -611,10 +703,16 @@ class NVPDriver(base.BaseDriver):
if network_id:
tags.append({"tag": network_id, "scope": "neutron_net_id"})
switch.tags(tags)
LOG.debug("Creating lswitch for network %s" % network_id)
# TODO(morgabra) It seems like this whole interaction here is
# broken. We force-add either the id/type from the network, or
# the config default, *then* we still call _config_provider_attrs?
# It seems like we should listen to the network then fall back
# to the config defaults, but I'm leaving this as-is for now.
pnet = phys_net or CONF.NVP.default_tz
ptype = phys_type or CONF.NVP.default_tz_type
switch.transport_zone(pnet, ptype)
LOG.debug("Creating lswitch for network %s" % network_id)
# When connecting to public or snet, we need switches that are
# connected to their respective public/private transport zones
@@ -622,6 +720,14 @@ class NVPDriver(base.BaseDriver):
# uses VLAN 122 in netdev. Probably need this to be configurable
self._config_provider_attrs(connection, switch, phys_net,
phys_type, segment_id)
# NOTE(morgabra) A hook for any statically-configured tz bindings
# that should be added to the switch before create()
# TODO(morgabra) I'm not sure the normal usage of provider net
# attrs, and which should superscede which. This all needs a
# refactor after some discovery probably.
self._add_default_tz_bindings(context, switch, network_id)
res = switch.create()
try:
uuid = res["uuid"]

View File

@@ -42,6 +42,8 @@ class OptimizedNVPDriver(NVPDriver):
for switch in lswitches:
try:
self._lswitch_delete(context, switch.nvp_id)
self._remove_default_tz_bindings(
context, network_id)
except aiclib.core.AICException as ae:
LOG.info("LSwitch/Network %s found in database."
" Adding to orphaned database table."

View File

@@ -21,6 +21,25 @@ class MacAddressRangeInUse(exceptions.InUse):
message = _("MAC address range %(mac_address_range_id) in use.")
class InvalidSegmentAllocationRange(exceptions.NeutronException):
message = _("Invalid segment allocation range: %(msg)s.")
class SegmentAllocationRangeNotFound(exceptions.NotFound):
message = _(
"Segment allocation range %(segment_allocation_range_id)s not found.")
class SegmentAllocationRangeInUse(exceptions.InUse):
message = _(
"Segment allocation range %(segment_allocation_range_id)s in use.")
class SegmentAllocationFailure(exceptions.Conflict):
message = _("No more segment ids available for segment "
"type:%(segment_type)s id:%(segment_id)s.")
class RouteNotFound(exceptions.NotFound):
message = _("Route %(route_id)s not found.")

View File

@@ -36,6 +36,7 @@ from quark.plugin_modules import ports
from quark.plugin_modules import router
from quark.plugin_modules import routes
from quark.plugin_modules import security_groups
from quark.plugin_modules import segment_allocation_ranges
from quark.plugin_modules import subnets
LOG = logging.getLogger(__name__)
@@ -129,7 +130,7 @@ class Plugin(neutron_plugin_base_v2.NeutronPluginBaseV2,
"provider", "ip_policies", "quotas",
"networks_quark", "router",
"ip_availabilities", "ports_quark",
"floatingip"]
"floatingip", "segment_allocation_ranges"]
def __init__(self):
LOG.info("Starting quark plugin")
@@ -449,3 +450,25 @@ class Plugin(neutron_plugin_base_v2.NeutronPluginBaseV2,
def get_ip_availability(self, **kwargs):
return ip_availability.get_ip_availability(**kwargs)
@sessioned
def get_segment_allocation_range(self, context, id, fields=None):
return segment_allocation_ranges.get_segment_allocation_range(
context, id, fields)
@sessioned
def get_segment_allocation_ranges(self, context, **filters):
return segment_allocation_ranges.get_segment_allocation_ranges(
context, **filters)
@sessioned
def create_segment_allocation_range(self, context, sa_range):
self._fix_missing_tenant_id(
context, sa_range["segment_allocation_range"])
return segment_allocation_ranges.create_segment_allocation_range(
context, sa_range)
@sessioned
def delete_segment_allocation_range(self, context, id):
segment_allocation_ranges.delete_segment_allocation_range(
context, id)

View File

@@ -0,0 +1,144 @@
# Copyright 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.
from neutron.common import exceptions
from oslo_log import log as logging
from quark.db import api as db_api
from quark import exceptions as quark_exceptions
from quark import plugin_views as v
from quark import segment_allocations
SA_REGISTRY = segment_allocations.REGISTRY
LOG = logging.getLogger(__name__)
def get_segment_allocation_range(context, id, fields=None):
LOG.info("get_segment_allocation_range %s for tenant %s fields %s" %
(id, context.tenant_id, fields))
if not context.is_admin:
raise exceptions.NotAuthorized()
sa_range = db_api.segment_allocation_range_find(
context, id=id, scope=db_api.ONE)
if not sa_range:
raise quark_exceptions.SegmentAllocationRangeNotFound(
segment_allocation_range_id=id)
# Count up allocations so we can calculate how many are free.
allocs = db_api.segment_allocation_find(
context,
segment_allocation_range_id=sa_range["id"],
deallocated=False).count()
return v._make_segment_allocation_range_dict(
sa_range, allocations=allocs)
def get_segment_allocation_ranges(context, **filters):
LOG.info("get_segment_allocation_ranges for tenant %s" % context.tenant_id)
if not context.is_admin:
raise exceptions.NotAuthorized()
sa_ranges = db_api.segment_allocation_range_find(
context, scope=db_api.ALL, **filters)
return [v._make_segment_allocation_range_dict(m) for m in sa_ranges]
def create_segment_allocation_range(context, sa_range):
LOG.info("create_segment_allocation_range for tenant %s"
% context.tenant_id)
if not context.is_admin:
raise exceptions.NotAuthorized()
sa_range = sa_range.get("segment_allocation_range")
if not sa_range:
raise exceptions.BadRequest(resource="segment_allocation_range",
msg=("segment_allocation_range not in "
"request body."))
# TODO(morgabra) Figure out how to get the api extension to validate this
# for us.
# parse required fields
for k in ["first_id", "last_id", "segment_id", "segment_type"]:
sa_range[k] = sa_range.get(k, None)
if sa_range[k] is None:
raise exceptions.BadRequest(
resource="segment_allocation_range",
msg=("Missing required key %s in request body." % (k)))
# parse optional fields
for k in ["do_not_use"]:
sa_range[k] = sa_range.get(k, None)
# use the segment registry to validate and create/populate the range
if not SA_REGISTRY.is_valid_strategy(sa_range["segment_type"]):
raise exceptions.BadRequest(
resource="segment_allocation_range",
msg=("Unknown segment type '%s'" % (k)))
strategy = SA_REGISTRY.get_strategy(sa_range["segment_type"])
# Create the new range
with context.session.begin():
new_range = strategy.create_range(context, sa_range)
# Bulk-populate the range, this could take a while for huge ranges
# (millions) so we do this in chunks outside the transaction. That
# means we need to rollback the range creation if it fails for
# whatever reason (and it will cascade delete any added allocations)
try:
strategy.populate_range(context, new_range)
except Exception:
LOG.exception("Failed to populate segment allocation range.")
delete_segment_allocation_range(context, new_range["id"])
raise
return v._make_segment_allocation_range_dict(new_range)
def _delete_segment_allocation_range(context, sa_range):
allocs = db_api.segment_allocation_find(
context,
segment_allocation_range_id=sa_range["id"],
deallocated=False).count()
if allocs:
raise quark_exceptions.SegmentAllocationRangeInUse(
segment_allocation_range_id=sa_range["id"])
db_api.segment_allocation_range_delete(context, sa_range)
def delete_segment_allocation_range(context, sa_id):
"""Delete a segment_allocation_range.
: param context: neutron api request context
: param id: UUID representing the segment_allocation_range to delete.
"""
LOG.info("delete_segment_allocation_range %s for tenant %s" %
(sa_id, context.tenant_id))
if not context.is_admin:
raise exceptions.NotAuthorized()
with context.session.begin():
sa_range = db_api.segment_allocation_range_find(
context, id=sa_id, scope=db_api.ONE)
if not sa_range:
raise quark_exceptions.SegmentAllocationRangeNotFound(
segment_allocation_range_id=sa_id)
_delete_segment_allocation_range(context, sa_range)

View File

@@ -270,6 +270,23 @@ def _make_mac_range_dict(mac_range):
"cidr": mac_range["cidr"]}
def _make_segment_allocation_range_dict(sa_range, allocations=None):
size = len(xrange(sa_range["first_id"], sa_range["last_id"] + 1))
sa_dict = {
"id": sa_range["id"],
"segment_id": sa_range["segment_id"],
"segment_type": sa_range["segment_type"],
"first_id": sa_range["first_id"],
"last_id": sa_range["last_id"],
"do_not_use": sa_range["do_not_use"],
"size": size}
if allocations is not None:
sa_dict["free_ids"] = sa_dict["size"] - allocations
return sa_dict
def _make_route_dict(route):
return {"id": route["id"],
"cidr": route["cidr"],

View File

@@ -0,0 +1,295 @@
# Copyright 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.
"""
Provide strategies for allocating network segments. (vlan, vxlan, etc)
"""
from quark.db import api as db_api
from quark import exceptions as quark_exceptions
from oslo_log import log as logging
from oslo_utils import timeutils
import itertools
import random
LOG = logging.getLogger(__name__)
class BaseSegmentAllocation(object):
segment_type = None
def _validate_range(self, context, sa_range):
raise NotImplementedError()
def _chunks(self, iterable, chunk_size):
"""Chunks data into chunk with size<=chunk_size."""
iterator = iter(iterable)
chunk = list(itertools.islice(iterator, 0, chunk_size))
while chunk:
yield chunk
chunk = list(itertools.islice(iterator, 0, chunk_size))
def _check_collisions(self, new_range, existing_ranges):
"""Check for overlapping ranges."""
def _contains(num, r1):
return (num >= r1[0] and
num <= r1[1])
def _is_overlap(r1, r2):
return (_contains(r1[0], r2) or
_contains(r1[1], r2) or
_contains(r2[0], r1) or
_contains(r2[1], r1))
for existing_range in existing_ranges:
if _is_overlap(new_range, existing_range):
return True
return False
def _make_segment_allocation_dict(self, id, sa_range):
return dict(
id=id,
segment_id=sa_range["segment_id"],
segment_type=sa_range["segment_type"],
segment_allocation_range_id=sa_range["id"],
deallocated=True
)
def _populate_range(self, context, sa_range):
first_id = sa_range["first_id"]
last_id = sa_range["last_id"]
id_range = xrange(first_id, last_id + 1)
LOG.info("Starting segment allocation population for "
"range:%s size:%s."
% (sa_range["id"], len(id_range)))
total_added = 0
for chunk in self._chunks(id_range, 5000):
sa_dicts = []
for segment_id in chunk:
sa_dict = self._make_segment_allocation_dict(
segment_id, sa_range)
sa_dicts.append(sa_dict)
db_api.segment_allocation_range_populate_bulk(context, sa_dicts)
context.session.flush()
total_added = total_added + len(sa_dicts)
LOG.info("Populated %s/%s segment ids for range:%s"
% (total_added, len(id_range), sa_range["id"]))
LOG.info("Finished segment allocation population for "
"range:%s size:%s."
% (sa_range["id"], len(id_range)))
def _create_range(self, context, sa_range):
with context.session.begin(subtransactions=True):
# Validate any range-specific things, like min/max ids.
self._validate_range(context, sa_range)
# Check any existing ranges for this segment for collisions
segment_id = sa_range["segment_id"]
segment_type = sa_range["segment_type"]
filters = {"segment_id": segment_id,
"segment_type": segment_type}
existing_ranges = db_api.segment_allocation_range_find(
context, lock_mode=True, scope=db_api.ALL, **filters)
collides = self._check_collisions(
(sa_range["first_id"], sa_range["last_id"]),
[(r["first_id"], r["last_id"]) for r in existing_ranges])
if collides:
raise quark_exceptions.InvalidSegmentAllocationRange(
msg=("The specified allocation collides with existing "
"range"))
return db_api.segment_allocation_range_create(
context, **sa_range)
def create_range(self, context, sa_range):
return self._create_range(context, sa_range)
def populate_range(self, context, sa_range):
return self._populate_range(context, sa_range)
def _try_allocate(self, context, segment_id, network_id):
"""Find a deallocated network segment id and reallocate it.
NOTE(morgabra) This locks the segment table, but only the rows
in use by the segment, which is pretty handy if we ever have
more than 1 segment or segment type.
"""
LOG.info("Attempting to allocate segment for network %s "
"segment_id %s segment_type %s"
% (network_id, segment_id, self.segment_type))
filter_dict = {
"segment_id": segment_id,
"segment_type": self.segment_type,
"do_not_use": False
}
available_ranges = db_api.segment_allocation_range_find(
context, scope=db_api.ALL, **filter_dict)
available_range_ids = [r["id"] for r in available_ranges]
try:
with context.session.begin(subtransactions=True):
# Search for any deallocated segment ids for the
# given segment.
filter_dict = {
"deallocated": True,
"segment_id": segment_id,
"segment_type": self.segment_type,
"segment_allocation_range_ids": available_range_ids
}
# NOTE(morgabra) We select 100 deallocated segment ids from
# the table here, and then choose 1 randomly. This is to help
# alleviate the case where an uncaught exception might leave
# an allocation active on a remote service but we do not have
# a record of it locally. If we *do* end up choosing a
# conflicted id, the caller should simply allocate another one
# and mark them all as reserved. If a single object has
# multiple reservations on the same segment, they will not be
# deallocated, and the operator must resolve the conficts
# manually.
allocations = db_api.segment_allocation_find(
context, lock_mode=True, **filter_dict).limit(100).all()
if allocations:
allocation = random.choice(allocations)
# Allocate the chosen segment.
update_dict = {
"deallocated": False,
"deallocated_at": None,
"network_id": network_id
}
allocation = db_api.segment_allocation_update(
context, allocation, **update_dict)
LOG.info("Allocated segment %s for network %s "
"segment_id %s segment_type %s"
% (allocation["id"], network_id, segment_id,
self.segment_type))
return allocation
except Exception:
LOG.exception("Error in segment reallocation.")
LOG.info("Cannot find reallocatable segment for network %s "
"segment_id %s segment_type %s"
% (network_id, segment_id, self.segment_type))
def allocate(self, context, segment_id, network_id):
allocation = self._try_allocate(
context, segment_id, network_id)
if allocation:
return allocation
raise quark_exceptions.SegmentAllocationFailure(
segment_id=segment_id, segment_type=self.segment_type)
def _try_deallocate(self, context, segment_id, network_id):
LOG.info("Attempting to deallocate segment for network %s "
"segment_id %s segment_type %s"
% (network_id, segment_id, self.segment_type))
with context.session.begin(subtransactions=True):
filter_dict = {
"deallocated": False,
"segment_id": segment_id,
"segment_type": self.segment_type,
"network_id": network_id
}
allocations = db_api.segment_allocation_find(
context, **filter_dict).all()
if not allocations:
LOG.info("Could not find allocated segment for network %s "
"segment_id %s segment_type %s for deallocate."
% (network_id, segment_id, self.segment_type))
return
if len(allocations) > 1:
LOG.error("Found multiple allocated segments for network %s "
"segment_id %s segment_type %s for deallocate. "
"Refusing to deallocate, these allocations are now "
"orphaned."
% (network_id, segment_id, self.segment_type))
return
allocation = allocations[0]
# Deallocate the found segment.
update_dict = {
"deallocated": True,
"deallocated_at": timeutils.utcnow(),
"network_id": None
}
allocation = db_api.segment_allocation_update(
context, allocation, **update_dict)
LOG.info("Deallocated %s allocated segment(s) for network %s "
"segment_id %s segment_type %s"
% (len(allocations), network_id, segment_id,
self.segment_type))
def deallocate(self, context, segment_id, network_id):
self._try_deallocate(context, segment_id, network_id)
class VXLANSegmentAllocation(BaseSegmentAllocation):
VXLAN_MIN = 1
VXLAN_MAX = (2 ** 24) - 1
segment_type = 'vxlan'
def _validate_range(self, context, sa_range):
# Validate that the range is legal and makes sense.
try:
first_id = sa_range["first_id"]
last_id = sa_range["last_id"]
first_id, last_id = (int(first_id), int(last_id))
assert first_id >= self.VXLAN_MIN
assert last_id <= self.VXLAN_MAX
assert first_id <= last_id
except Exception:
raise quark_exceptions.InvalidSegmentAllocationRange(
msg="The specified allocation range is invalid")
class SegmentAllocationRegistry(object):
def __init__(self):
self.strategies = {
VXLANSegmentAllocation.segment_type: VXLANSegmentAllocation(),
}
def is_valid_strategy(self, strategy_name):
if strategy_name in self.strategies:
return True
return False
def get_strategy(self, strategy_name):
if self.is_valid_strategy(strategy_name):
return self.strategies[strategy_name]
raise Exception("Segment allocation strategy %s not found."
% (strategy_name))
REGISTRY = SegmentAllocationRegistry()

View File

@@ -0,0 +1,430 @@
# Copyright 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.
from neutron.common import exceptions
from quark.db import api as db_api
from quark import exceptions as quark_exceptions
import quark.plugin_modules.segment_allocation_ranges as sa_ranges_api
from quark import segment_allocations
from quark.tests.functional.base import BaseFunctionalTest
class QuarkSegmentAllocationTest(BaseFunctionalTest):
def setUp(self):
super(QuarkSegmentAllocationTest, self).setUp()
self.segment_type = 'vxlan'
self.segment_id = 'segment_id'
self.old_context = self.context
self.context = self.context.elevated()
def _make_segment_allocation_range_dict(self, segment_type=None,
segment_id=None,
first_id=1,
last_id=5):
if not segment_type:
segment_type = self.segment_type
if not segment_id:
segment_id = self.segment_id
return {
'segment_type': segment_type,
'segment_id': segment_id,
'first_id': first_id,
'last_id': last_id,
}
def _populate_segment_allocation_range(self, sa_range):
"""Populate a given segment range."""
# Range of ids to allocate, first to last (inclusive)
id_range = xrange(sa_range['first_id'],
sa_range['last_id'] + 1)
sa_dicts = []
total = 0
for i in id_range:
sa_dicts.append({
'segment_id': sa_range['segment_id'],
'segment_type': sa_range['segment_type'],
'id': i,
'segment_allocation_range_id': sa_range['id'],
'deallocated': True
})
total = total + 1
db_api.segment_allocation_range_populate_bulk(self.context, sa_dicts)
self.context.session.flush()
# assert our allocation were actually created
allocs = db_api.segment_allocation_find(
self.context, segment_allocation_range_id=sa_range['id']).all()
self.assertEqual(len(allocs), len(id_range))
def _create_segment_allocation_range(self, **kwargs):
"""Create a segment allocation range in the database."""
sa_dict = self._make_segment_allocation_range_dict(**kwargs)
sa_range = db_api.segment_allocation_range_create(
self.context, **sa_dict)
self.context.session.flush()
self._populate_segment_allocation_range(sa_range)
return sa_range
def _allocate_segment(self, sa_range, count=1):
"""Populate a given segment range."""
allocs = []
for i in xrange(sa_range['first_id'], sa_range['first_id'] + count):
filters = {
'segment_allocation_range_id': sa_range['id'],
'deallocated': True
}
alloc = db_api.segment_allocation_find(
self.context, **filters).first()
if not alloc:
raise Exception("Could not find deallocated id.")
update = {
'deallocated': False
}
allocs.append(
db_api.segment_allocation_update(
self.context, alloc, **update)
)
self.context.session.flush()
self.assertEqual(len(allocs), count)
return allocs
def _sa_range_to_dict(self, sa_range, allocations=None):
"""Helper to turn a model into a dict for assertions."""
size = (sa_range['last_id'] + 1) - sa_range['first_id']
sa_range_dict = dict(sa_range)
sa_range_dict.pop('created_at')
sa_range_dict['size'] = size
if allocations is not None:
sa_range_dict['free_ids'] = size - allocations
return sa_range_dict
class QuarkTestVXLANSegmentAllocation(QuarkSegmentAllocationTest):
def setUp(self):
super(QuarkTestVXLANSegmentAllocation, self).setUp()
self.registry = segment_allocations.SegmentAllocationRegistry()
self.driver = self.registry.get_strategy('vxlan')
def test_segment_allocation(self):
sa_range = self._create_segment_allocation_range()
# assert we allocate and update correctly
alloc = self.driver.allocate(
self.context, sa_range['segment_id'], 'network_id_1')
self.assertEqual(alloc['segment_type'], sa_range['segment_type'])
self.assertEqual(alloc['segment_id'], sa_range['segment_id'])
self.assertEqual(alloc['network_id'], 'network_id_1')
# assert the remaining allocations remain unallocated
allocs = db_api.segment_allocation_find(
self.context).all()
allocs.remove(alloc)
self.assertEqual(len(allocs), 4)
self.assertTrue(all([a["deallocated"] for a in allocs]))
return sa_range, alloc
def test_segment_deallocation(self):
# We call the allocate test to set up an initial allocation
# and assert that it actually worked.
sa_range, alloc = self.test_segment_allocation()
self.driver.deallocate(
self.context, sa_range['segment_id'], 'network_id_1')
# assert that our previous allocation is now free
allocs = db_api.segment_allocation_find(
self.context, id=alloc['id'],
segment_id=sa_range['segment_id']).all()
self.assertEqual(len(allocs), 1)
self.assertTrue(allocs[0]["deallocated"])
self.assertEqual(allocs[0]["network_id"], None)
def test_allocation_segment_full(self):
# create a range, and allocate everything
sa_range = self._create_segment_allocation_range()
self._allocate_segment(sa_range, count=5)
self.assertRaises(
quark_exceptions.SegmentAllocationFailure,
self.driver.allocate,
self.context, sa_range['segment_id'], 'network_id_2')
class QuarkTestCreateSegmentAllocationRange(QuarkSegmentAllocationTest):
def test_create_segment_allocation_range_unauthorized(self):
sa_range_dict = self._make_segment_allocation_range_dict()
sa_range_request = {"segment_allocation_range": sa_range_dict}
self.assertRaises(
exceptions.NotAuthorized,
sa_ranges_api.create_segment_allocation_range,
self.old_context, sa_range_request)
def test_create_segment_allocation_range(self):
"""Assert a range is created."""
# Create a segment allocation range
sa_range_dict = self._make_segment_allocation_range_dict()
sa_range_request = {"segment_allocation_range": sa_range_dict}
sa_range = sa_ranges_api.create_segment_allocation_range(
self.context, sa_range_request)
# Find all ranges added in the db
sa_range_models = db_api.segment_allocation_range_find(
self.context, scope=db_api.ALL)
# Assert we actually added the range to the db with correct
# values and returned the correct response.
self.assertEqual(len(sa_range_models), 1)
self.assertEqual(self._sa_range_to_dict(sa_range_models[0]),
sa_range)
def test_create_segment_allocation_range_invalid_fails(self):
"""Assert segments with invalid ranges are disallowed."""
sa_range_dict = self._make_segment_allocation_range_dict()
sa_range_request = {"segment_allocation_range": sa_range_dict}
invalid_ranges = [
(0, 5), # first_id < MIN,
(1, 2 ** 24 + 1), # last_id > MAX,
(5, 1), # last_id < first_id,
('a', 5), # invalid data
]
for first_id, last_id in invalid_ranges:
sa_range_dict['first_id'] = first_id
sa_range_dict['last_id'] = last_id
self.assertRaises(
quark_exceptions.InvalidSegmentAllocationRange,
sa_ranges_api.create_segment_allocation_range,
self.context, sa_range_request)
def test_create_segment_allocation_range_creates_allocations(self):
"""Assert created segments populate the allocation table."""
sa_range_dict = self._make_segment_allocation_range_dict()
sa_range_request = {"segment_allocation_range": sa_range_dict}
sa_range = sa_ranges_api.create_segment_allocation_range(
self.context, sa_range_request)
allocs = db_api.segment_allocation_find(
self.context, segment_allocation_range_id=sa_range['id']).all()
self.assertEqual(len(allocs), sa_range['size'])
def test_create_segment_allocation_ranges(self):
"""Assert segments with same type/id are allowed."""
sa_range_dict = self._make_segment_allocation_range_dict()
sa_range_request = {"segment_allocation_range": sa_range_dict}
valid_ranges = [
(10, 15),
(5, 9),
(16, 20),
]
for first_id, last_id in valid_ranges:
sa_range_dict['first_id'] = first_id
sa_range_dict['last_id'] = last_id
sa_range = sa_ranges_api.create_segment_allocation_range(
self.context, sa_range_request)
# Find all ranges added in the db
sa_range_models = db_api.segment_allocation_range_find(
self.context, id=sa_range['id'], scope=db_api.ALL)
# Assert we actually added the range to the db with correct
# values and returned the correct response.
self.assertEqual(len(sa_range_models), 1)
self.assertEqual(self._sa_range_to_dict(sa_range_models[0]),
sa_range)
def test_create_segment_allocation_ranges_diff_overlap_allowed(self):
"""Assert different segments with overlapping ids are allowed."""
sa_range_dict = self._make_segment_allocation_range_dict()
sa_range_request = {"segment_allocation_range": sa_range_dict}
segment_ids = [
'segment1',
'segment2',
'segment3'
]
for segment_id in segment_ids:
sa_range_dict['first_id'] = 1
sa_range_dict['last_id'] = 5
sa_range_dict['segment_id'] = segment_id
sa_range = sa_ranges_api.create_segment_allocation_range(
self.context, sa_range_request)
# Find all ranges added in the db
sa_range_models = db_api.segment_allocation_range_find(
self.context, id=sa_range['id'], scope=db_api.ALL)
# Assert we actually added the range to the db with correct
# values and returned the correct response.
self.assertEqual(len(sa_range_models), 1)
self.assertEqual(self._sa_range_to_dict(sa_range_models[0]),
sa_range)
def test_create_segment_allocation_ranges_same_overlap_fails(self):
"""Assert same segments with overlapping ids are disallowed."""
sa_range_dict = self._make_segment_allocation_range_dict()
sa_range_request = {"segment_allocation_range": sa_range_dict}
# create initial segment with range 10-15
sa_range_dict['first_id'] = 10
sa_range_dict['last_id'] = 15
sa_ranges_api.create_segment_allocation_range(
self.context, sa_range_request)
invalid_ranges = [
(10, 15), # same range
(5, 10), # collides at start
(15, 20), # collides at end
(8, 12), # overlaps from start
(12, 17), # overlaps from end
(9, 16), # superset
(11, 14) # subset
]
for first_id, last_id in invalid_ranges:
sa_range_dict['first_id'] = first_id
sa_range_dict['last_id'] = last_id
self.assertRaises(
quark_exceptions.InvalidSegmentAllocationRange,
sa_ranges_api.create_segment_allocation_range,
self.context, sa_range_request)
class QuarkTestGetSegmentAllocationRange(QuarkSegmentAllocationTest):
def test_get_segment_allocation_range_unauthorized(self):
sa_range = self._create_segment_allocation_range()
self.assertRaises(
exceptions.NotAuthorized,
sa_ranges_api.get_segment_allocation_range,
self.old_context, sa_range["id"])
def test_get_segment_allocation_range_not_found(self):
self._create_segment_allocation_range()
self.assertRaises(
exceptions.NotFound,
sa_ranges_api.get_segment_allocation_range,
self.context, "some_id")
def test_get_segment_allocation_range(self):
sa_range = self._create_segment_allocation_range()
result = sa_ranges_api.get_segment_allocation_range(
self.context, sa_range['id'])
expected_result = self._sa_range_to_dict(sa_range, allocations=0)
self.assertEqual(expected_result, result)
def test_get_segment_allocation_range_with_allocations(self):
sa_range = self._create_segment_allocation_range()
allocs = self._allocate_segment(sa_range, count=2)
result = sa_ranges_api.get_segment_allocation_range(
self.context, sa_range['id'])
expected_result = self._sa_range_to_dict(
sa_range, allocations=len(allocs))
self.assertEqual(expected_result, result)
class QuarkTestGetSegmentAllocationRanges(QuarkSegmentAllocationTest):
def test_get_segment_allocation_ranges_unauthorized(self):
self.assertRaises(
exceptions.NotAuthorized,
sa_ranges_api.get_segment_allocation_ranges,
self.old_context)
def test_get_segment_allocation_ranges_empty(self):
result = sa_ranges_api.get_segment_allocation_ranges(self.context)
self.assertEqual([], result)
def test_get_segment_allocation_ranges(self):
sa_range = self._create_segment_allocation_range()
sa_range2 = self._create_segment_allocation_range(
first_id=6, last_id=10)
result = sa_ranges_api.get_segment_allocation_ranges(self.context)
ex_result = [self._sa_range_to_dict(r) for r in [sa_range, sa_range2]]
self.assertEqual(ex_result, result)
class QuarkTestDeleteSegmentAllocationRange(QuarkSegmentAllocationTest):
def test_delete_segment_allocation_range_not_found(self):
self._create_segment_allocation_range()
self.assertRaises(
exceptions.NotFound,
sa_ranges_api.delete_segment_allocation_range,
self.context, "some_id")
def test_delete_segment_allocation_range_unauthorized(self):
sa_range = self._create_segment_allocation_range()
# assert non-admins are not authorized
self.assertRaises(
exceptions.NotAuthorized,
sa_ranges_api.delete_segment_allocation_range,
self.old_context, sa_range["id"])
# assert the range was not deleted
sa_ranges = db_api.segment_allocation_range_find(
self.context, id=sa_range["id"], scope=db_api.ALL)
self.assertEqual(sa_ranges, [sa_range])
def test_delete_segment_allocation_range_in_use_fails(self):
sa_range = self._create_segment_allocation_range()
self._allocate_segment(sa_range, count=1)
self.assertRaises(
exceptions.InUse,
sa_ranges_api.delete_segment_allocation_range,
self.context, sa_range["id"])
# assert the range was not deleted
sa_ranges = db_api.segment_allocation_range_find(
self.context, id=sa_range["id"], scope=db_api.ALL)
self.assertEqual(sa_ranges, [sa_range])
def test_delete_segment_allocation_range_deletes(self):
sa_range = self._create_segment_allocation_range()
sa_range_id = sa_range["id"]
sa_ranges_api.delete_segment_allocation_range(
self.context, sa_range_id)
# assert that the range and it's unused allocations are deleted
sa_range = db_api.segment_allocation_range_find(
self.context, id=sa_range_id, scope=db_api.ALL)
allocs = db_api.segment_allocation_find(
self.context, segment_allocation_range_id=sa_range_id).all()
self.assertEqual(sa_range, [])
self.assertEqual(allocs, [])

View File

@@ -145,6 +145,72 @@ class TestNVPDriverCreateNetwork(TestNVPDriver):
connection.lswitch().display_name.assert_called_with(long_n[:40])
class TestNVPDriverDefaultTransportZoneBindings(TestNVPDriver):
def setUp(self):
super(TestNVPDriverDefaultTransportZoneBindings, self).setUp()
cfg.CONF.set_override(
'additional_default_tz_types', ['vxlan'], 'NVP')
cfg.CONF.set_override(
'default_tz', 'tz_uuid', 'NVP')
cfg.CONF.set_override(
'default_tz_type', 'stt', 'NVP')
def tearDown(self):
super(TestNVPDriverDefaultTransportZoneBindings, self).setUp()
cfg.CONF.clear_override('additional_default_tz_types', 'NVP')
cfg.CONF.clear_override('default_tz', 'NVP')
cfg.CONF.clear_override('default_tz_type', 'NVP')
@contextlib.contextmanager
def _stubs(self):
with contextlib.nested(
mock.patch("quark.drivers.nvp_driver.SA_REGISTRY."
"get_strategy"),
mock.patch("%s._connection" % self.d_pkg),
mock.patch("%s._lswitches_for_network" % self.d_pkg),
) as (sa_get_strategy, conn, switch_list):
connection = self._create_connection()
conn.return_value = connection
ret = {"results": [{"uuid": self.lswitch_uuid}]}
switch_list().results = mock.Mock(return_value=ret)
sa_strategy = mock.Mock()
sa_get_strategy.return_value = sa_strategy
sa_strategy.allocate.return_value = {"id": 123}
yield sa_get_strategy, sa_strategy, connection
def test_default_tz_bindings_net_create(self):
with self._stubs() as (sa_get_strategy, sa_strategy, connection):
self.driver.create_network(
self.context, "test", network_id="network_id")
self.assertTrue(connection.lswitch().create.called)
# assert vxlan tz manager was called
sa_strategy.allocate.assert_called_once_with(
self.context, 'tz_uuid', 'network_id')
# assert transport_zone was called:
# once for the default configured tz type (stt)
# once for the additional default tz type (vxlan)
self.assertEqual(
connection.lswitch().transport_zone.call_args_list,
[mock.call('tz_uuid', 'stt'),
mock.call('tz_uuid', 'vxlan', vxlan_id=123)]
)
def test_default_tz_bindings_net_delete(self):
with self._stubs() as (sa_get_strategy, sa_strategy, connection):
self.driver.delete_network(self.context, "network_id")
self.assertTrue(connection.lswitch().delete.called)
sa_strategy.deallocate.assert_called_once_with(
self.context, 'tz_uuid', 'network_id')
class TestNVPDriverProviderNetwork(TestNVPDriver):
"""Testing all of the network types is unnecessary, but a nice have."""