From: Sylvain Bauza Date: Wed, 2 Nov 2016 11:28:02 +0000 (+0100) Subject: Extend get_all_by_filters to support resource criteria X-Git-Url: http://xenbits.xensource.com/gitweb?a=commitdiff_plain;h=38c6a849e79c04dc02834ced68409131a6b55e37;p=osstest%2Fopenstack-nova.git Extend get_all_by_filters to support resource criteria 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 --- diff --git a/nova/objects/resource_provider.py b/nova/objects/resource_provider.py index f013a7ce92..7525491dec 100644 --- a/nova/objects/resource_provider.py +++ b/nova/objects/resource_provider.py @@ -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': , + # '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) diff --git a/nova/tests/functional/db/test_resource_provider.py b/nova/tests/functional/db/test_resource_provider.py index 55ecf1c22e..06b57f6bde 100644 --- a/nova/tests/functional/db/test_resource_provider.py +++ b/nova/tests/functional/db/test_resource_provider.py @@ -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):