Add framework for managing segment id pools.
Also added the virst implementation, vxlan, to the NVP driver.
This commit is contained in:
100
quark/api/extensions/segment_allocation_ranges.py
Normal file
100
quark/api/extensions/segment_allocation_ranges.py
Normal 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)]
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
@@ -1 +1 @@
|
||||
374c1bdb4480
|
||||
1bd7cff90384
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
144
quark/plugin_modules/segment_allocation_ranges.py
Normal file
144
quark/plugin_modules/segment_allocation_ranges.py
Normal 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)
|
||||
@@ -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"],
|
||||
|
||||
295
quark/segment_allocations.py
Normal file
295
quark/segment_allocations.py
Normal 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()
|
||||
430
quark/tests/functional/plugin_modules/test_segment_allocation.py
Normal file
430
quark/tests/functional/plugin_modules/test_segment_allocation.py
Normal 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, [])
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user