]> xenbits.xensource.com Git - osstest/openstack-nova.git/commitdiff
Extend get_all_by_filters to support resource criteria
authorSylvain Bauza <sbauza@redhat.com>
Wed, 2 Nov 2016 11:28:02 +0000 (12:28 +0100)
committerSylvain Bauza <sbauza@redhat.com>
Thu, 8 Dec 2016 11:08:19 +0000 (12:08 +0100)
Given the scheduler wants to know which RPs can support a set of different
requests, each one having a resource class with an amount, we need to
modify the current ResourceProviderList method for returning a subset.
The proposal for the request parameter is a dictionary of amounts keyed
by the resource class name.

Change-Id: I94e800dabd5cc995728898dd6d8f6d42ba645312
Partially-Implements: blueprint resource-providers-get-by-request

nova/objects/resource_provider.py
nova/tests/functional/db/test_resource_provider.py

index f013a7ce92d7f39df2ba204a28b69865057d7866..7525491decb891f89958b61e092b1921ef5c95a8 100644 (file)
@@ -10,6 +10,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import copy
+
 from oslo_db import exception as db_exc
 from oslo_log import log as logging
 from oslo_utils import versionutils
@@ -540,18 +542,135 @@ class ResourceProviderList(base.ObjectListBase, base.NovaObject):
     @staticmethod
     @db_api.api_context_manager.reader
     def _get_all_by_filters_from_db(context, filters):
+        # Eg. filters can be:
+        #  filters = {
+        #      'name': <name>,
+        #      'uuid': <uuid>,
+        #      'resources': {
+        #          'VCPU': 1,
+        #          'MEMORY_MB': 1024
+        #      }
+        #  }
         if not filters:
             filters = {}
+        else:
+            # Since we modify the filters, copy them so that we don't modify
+            # them in the calling program.
+            filters = copy.deepcopy(filters)
+        name = filters.pop('name', None)
+        uuid = filters.pop('uuid', None)
+        can_host = filters.pop('can_host', 0)
+
+        resources = filters.pop('resources', {})
+        # NOTE(sbauza): We want to key the dict by the resource class IDs
+        # and we want to make sure those class names aren't incorrect.
+        resources = {_RC_CACHE.id_from_string(r_name): amount
+                     for r_name, amount in six.iteritems(resources)}
         query = context.session.query(models.ResourceProvider)
-        for attr in ResourceProviderList.allowed_filters:
-            if attr in filters:
-                query = query.filter(
-                    getattr(models.ResourceProvider, attr) == filters[attr])
-        query = query.filter_by(can_host=filters.get('can_host', 0))
+        if name:
+            query = query.filter(models.ResourceProvider.name == name)
+        if uuid:
+            query = query.filter(models.ResourceProvider.uuid == uuid)
+        query = query.filter(models.ResourceProvider.can_host == can_host)
+
+        if not resources:
+            # Returns quickly the list in case we don't need to check the
+            # resource usage
+            return query.all()
+
+        # NOTE(sbauza): In case we want to look at the resource criteria, then
+        # the SQL generated from this case looks something like:
+        # SELECT
+        #   rp.*
+        # FROM resource_providers AS rp
+        # JOIN inventories AS inv
+        # ON rp.id = inv.resource_provider_id
+        # LEFT JOIN (
+        #    SELECT resource_provider_id, resource_class_id, SUM(used) AS used
+        #    FROM allocations
+        #    WHERE resource_class_id IN ($RESOURCE_CLASSES)
+        #    GROUP BY resource_provider_id, resource_class_id
+        # ) AS usage
+        #     ON inv.resource_provider_id = usage.resource_provider_id
+        #     AND inv.resource_class_id = usage.resource_class_id
+        # AND (inv.resource_class_id = $X AND (used + $AMOUNT_X <= (
+        #        total + reserved) * inv.allocation_ratio) AND
+        #        inv.min_unit <= $AMOUNT_X AND inv.max_unit >= $AMOUNT_X AND
+        #        $AMOUNT_X % inv.step_size == 0)
+        #      OR (inv.resource_class_id = $Y AND (used + $AMOUNT_Y <= (
+        #        total + reserved) * inv.allocation_ratio) AND
+        #        inv.min_unit <= $AMOUNT_Y AND inv.max_unit >= $AMOUNT_Y AND
+        #        $AMOUNT_Y % inv.step_size == 0)
+        #      OR (inv.resource_class_id = $Z AND (used + $AMOUNT_Z <= (
+        #        total + reserved) * inv.allocation_ratio) AND
+        #        inv.min_unit <= $AMOUNT_Z AND inv.max_unit >= $AMOUNT_Z AND
+        #        $AMOUNT_Z % inv.step_size == 0))
+        # GROUP BY rp.uuid
+        # HAVING
+        #  COUNT(DISTINCT(inv.resource_class_id)) == len($RESOURCE_CLASSES)
+        #
+        # with a possible additional WHERE clause for the name and uuid that
+        # comes from the above filters
+
+        # First JOIN between inventories and RPs is here
+        join_clause = _RP_TBL.c.id == _INV_TBL.c.resource_provider_id
+        query = query.join(_INV_TBL, join_clause)
+
+        # Now, below is the LEFT JOIN for getting the allocations usage
+        usage = sa.select([_ALLOC_TBL.c.resource_provider_id,
+                           _ALLOC_TBL.c.consumer_id,
+                           _ALLOC_TBL.c.resource_class_id,
+                           sql.func.sum(_ALLOC_TBL.c.used).label('used')])
+        usage = usage.where(_ALLOC_TBL.c.resource_class_id.in_(
+            resources.keys()))
+        usage = usage.group_by(_ALLOC_TBL.c.resource_provider_id,
+                               _ALLOC_TBL.c.resource_class_id)
+        usage = sa.alias(usage, name='usage')
+        query = query.outerjoin(
+            usage,
+            sa.and_(
+                usage.c.resource_provider_id == (
+                    _INV_TBL.c.resource_provider_id),
+                usage.c.resource_class_id == _INV_TBL.c.resource_class_id))
+
+        # And finally, we verify for each resource class if the requested
+        # amount isn't more than the left space (considering the allocation
+        # ratio, the reserved space and the min and max amount possible sizes)
+        where_clauses = [
+            sa.and_(
+                _INV_TBL.c.resource_class_id == r_idx,
+                (func.coalesce(usage.c.used, 0) + amount <= (
+                    _INV_TBL.c.total - _INV_TBL.c.reserved
+                ) * _INV_TBL.c.allocation_ratio),
+                _INV_TBL.c.min_unit <= amount,
+                _INV_TBL.c.max_unit >= amount,
+                amount % _INV_TBL.c.step_size == 0
+            )
+            for (r_idx, amount) in six.iteritems(resources)]
+        query = query.filter(sa.or_(*where_clauses))
+        query = query.group_by(_RP_TBL.c.uuid)
+        # NOTE(sbauza): Only RPs having all the asked resources can be provided
+        query = query.having(sql.func.count(
+            sa.distinct(_INV_TBL.c.resource_class_id)) == len(resources))
+
         return query.all()
 
     @base.remotable_classmethod
     def get_all_by_filters(cls, context, filters=None):
+        """Returns a list of `ResourceProvider` objects that have sufficient
+        resources in their inventories to satisfy the amounts specified in the
+        `filters` parameter.
+
+        If no resource providers can be found, the function will return an
+        empty list.
+
+        :param context: `nova.context.RequestContext` that may be used to grab
+                        a DB connection.
+        :param filters: Can be `name`, `uuid` or `resources` where `resources`
+                        is a dict of amounts keyed by resource classes
+        :type filters: dict
+        """
+        _ensure_rc_cache(context)
         resource_providers = cls._get_all_by_filters_from_db(context, filters)
         return base.obj_make_list(context, cls(context),
                                   objects.ResourceProvider, resource_providers)
index 55ecf1c22e2b8b658ad7350bcb392c292140f1dd..06b57f6bde07c28b260201dc2a29f2da445577cc 100644 (file)
@@ -531,6 +531,111 @@ class ResourceProviderListTestCase(ResourceProviderBaseCase):
         self.assertEqual(1, len(resource_providers))
         self.assertEqual('rp_name_2', resource_providers[0].name)
 
+    def test_get_all_by_filters_with_resources(self):
+        for rp_i in ['1', '2']:
+            uuid = getattr(uuidsentinel, 'rp_uuid_' + rp_i)
+            name = 'rp_name_' + rp_i
+            rp = objects.ResourceProvider(self.context, name=name, uuid=uuid)
+            rp.create()
+            inv = objects.Inventory(
+                resource_provider=rp,
+                resource_class=fields.ResourceClass.VCPU,
+                min_unit=1,
+                max_unit=2,
+                total=2,
+                allocation_ratio=1.0)
+            inv.obj_set_defaults()
+
+            inv2 = objects.Inventory(
+                resource_provider=rp,
+                resource_class=fields.ResourceClass.DISK_GB,
+                total=1024, reserved=2,
+                min_unit=1,
+                max_unit=1024,
+                allocation_ratio=1.0)
+            inv2.obj_set_defaults()
+
+            # Write that specific inventory for testing min/max units and steps
+            inv3 = objects.Inventory(
+                resource_provider=rp,
+                resource_class=fields.ResourceClass.MEMORY_MB,
+                total=1024, reserved=2,
+                min_unit=2,
+                max_unit=4,
+                step_size=2,
+                allocation_ratio=1.0)
+            inv3.obj_set_defaults()
+
+            inv_list = objects.InventoryList(objects=[inv, inv2, inv3])
+            rp.set_inventory(inv_list)
+
+            # Create the VCPU allocation only for the first RP
+            if rp_i != '1':
+                continue
+            allocation_1 = objects.Allocation(
+                resource_provider=rp,
+                consumer_id=uuidsentinel.consumer,
+                resource_class=fields.ResourceClass.VCPU,
+                used=1)
+            allocation_list = objects.AllocationList(
+                self.context, objects=[allocation_1])
+            allocation_list.create_all()
+
+        # Both RPs should accept that request given the only current allocation
+        # for the first RP is leaving one VCPU
+        resource_providers = objects.ResourceProviderList.get_all_by_filters(
+            self.context, {'resources': {fields.ResourceClass.VCPU: 1}})
+        self.assertEqual(2, len(resource_providers))
+        # Now, when asking for 2 VCPUs, only the second RP should accept that
+        # given the current allocation for the first RP
+        resource_providers = objects.ResourceProviderList.get_all_by_filters(
+            self.context, {'resources': {fields.ResourceClass.VCPU: 2}})
+        self.assertEqual(1, len(resource_providers))
+        # Adding a second resource request should be okay for the 2nd RP
+        # given it has enough disk but we also need to make sure that the
+        # first RP is not acceptable because of the VCPU request
+        resource_providers = objects.ResourceProviderList.get_all_by_filters(
+            self.context, {'resources': {fields.ResourceClass.VCPU: 2,
+                                         fields.ResourceClass.DISK_GB: 1022}})
+        self.assertEqual(1, len(resource_providers))
+        # Now, we are asking for both disk and VCPU resources that all the RPs
+        # can't accept (as the 2nd RP is having a reserved size)
+        resource_providers = objects.ResourceProviderList.get_all_by_filters(
+            self.context, {'resources': {fields.ResourceClass.VCPU: 2,
+                                         fields.ResourceClass.DISK_GB: 1024}})
+        self.assertEqual(0, len(resource_providers))
+
+        # We also want to verify that asking for a specific RP can also be
+        # checking the resource usage.
+        resource_providers = objects.ResourceProviderList.get_all_by_filters(
+            self.context, {'name': 'rp_name_1',
+                           'resources': {fields.ResourceClass.VCPU: 1}})
+        self.assertEqual(1, len(resource_providers))
+
+        # Let's verify that the min and max units are checked too
+        # Case 1: amount is in between min and max and modulo step_size
+        resource_providers = objects.ResourceProviderList.get_all_by_filters(
+            self.context, {'resources': {fields.ResourceClass.MEMORY_MB: 2}})
+        self.assertEqual(2, len(resource_providers))
+        # Case 2: amount is less than min_unit
+        resource_providers = objects.ResourceProviderList.get_all_by_filters(
+            self.context, {'resources': {fields.ResourceClass.MEMORY_MB: 1}})
+        self.assertEqual(0, len(resource_providers))
+        # Case 3: amount is more than min_unit
+        resource_providers = objects.ResourceProviderList.get_all_by_filters(
+            self.context, {'resources': {fields.ResourceClass.MEMORY_MB: 5}})
+        self.assertEqual(0, len(resource_providers))
+        # Case 4: amount is not modulo step_size
+        resource_providers = objects.ResourceProviderList.get_all_by_filters(
+            self.context, {'resources': {fields.ResourceClass.MEMORY_MB: 3}})
+        self.assertEqual(0, len(resource_providers))
+
+    def test_get_all_by_filters_with_resources_not_existing(self):
+        self.assertRaises(
+            exception.ResourceClassNotFound,
+            objects.ResourceProviderList.get_all_by_filters,
+            self.context, {'resources': {'FOOBAR': 3}})
+
 
 class TestResourceProviderAggregates(test.NoDBTestCase):