diff --git a/quark/api/extensions/segment_allocation_ranges.py b/quark/api/extensions/segment_allocation_ranges.py new file mode 100644 index 0000000..f7a5f07 --- /dev/null +++ b/quark/api/extensions/segment_allocation_ranges.py @@ -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)] diff --git a/quark/db/api.py b/quark/db/api.py index 42014df..e654ba1 100644 --- a/quark/db/api.py +++ b/quark/db/api.py @@ -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) diff --git a/quark/db/migration/alembic/versions/1bd7cff90384_add_segment_allocations.py b/quark/db/migration/alembic/versions/1bd7cff90384_add_segment_allocations.py new file mode 100644 index 0000000..c3a9efa --- /dev/null +++ b/quark/db/migration/alembic/versions/1bd7cff90384_add_segment_allocations.py @@ -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') diff --git a/quark/db/migration/alembic/versions/HEAD b/quark/db/migration/alembic/versions/HEAD index 27305bf..0ba0a5b 100644 --- a/quark/db/migration/alembic/versions/HEAD +++ b/quark/db/migration/alembic/versions/HEAD @@ -1 +1 @@ -374c1bdb4480 \ No newline at end of file +1bd7cff90384 \ No newline at end of file diff --git a/quark/db/models.py b/quark/db/models.py index 6ab28f0..0f8966d 100644 --- a/quark/db/models.py +++ b/quark/db/models.py @@ -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) diff --git a/quark/drivers/nvp_driver.py b/quark/drivers/nvp_driver.py index 6e7af22..fc180ff 100644 --- a/quark/drivers/nvp_driver.py +++ b/quark/drivers/nvp_driver.py @@ -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"] diff --git a/quark/drivers/optimized_nvp_driver.py b/quark/drivers/optimized_nvp_driver.py index e948528..8fc3df4 100644 --- a/quark/drivers/optimized_nvp_driver.py +++ b/quark/drivers/optimized_nvp_driver.py @@ -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." diff --git a/quark/exceptions.py b/quark/exceptions.py index 0823777..3b526c2 100644 --- a/quark/exceptions.py +++ b/quark/exceptions.py @@ -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.") diff --git a/quark/plugin.py b/quark/plugin.py index 0ff3f0a..beca6f0 100644 --- a/quark/plugin.py +++ b/quark/plugin.py @@ -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) diff --git a/quark/plugin_modules/segment_allocation_ranges.py b/quark/plugin_modules/segment_allocation_ranges.py new file mode 100644 index 0000000..edc251b --- /dev/null +++ b/quark/plugin_modules/segment_allocation_ranges.py @@ -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) diff --git a/quark/plugin_views.py b/quark/plugin_views.py index 1dbb810..632643f 100644 --- a/quark/plugin_views.py +++ b/quark/plugin_views.py @@ -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"], diff --git a/quark/segment_allocations.py b/quark/segment_allocations.py new file mode 100644 index 0000000..103e361 --- /dev/null +++ b/quark/segment_allocations.py @@ -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() diff --git a/quark/tests/functional/plugin_modules/test_segment_allocation.py b/quark/tests/functional/plugin_modules/test_segment_allocation.py new file mode 100644 index 0000000..2efbc70 --- /dev/null +++ b/quark/tests/functional/plugin_modules/test_segment_allocation.py @@ -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, []) diff --git a/quark/tests/test_nvp_driver.py b/quark/tests/test_nvp_driver.py index 73725f0..a43119f 100644 --- a/quark/tests/test_nvp_driver.py +++ b/quark/tests/test_nvp_driver.py @@ -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."""