From 85e287d3ef698548c03c635a81d3b60b6ebe9ba8 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Wed, 20 Feb 2019 03:16:17 +0000 Subject: [PATCH] Add get_counts() to InstanceMappingList This counts instance mappings for the purpopse of counting quota usage for instances. By counting instances via instance mappings, the count is resilient to down cells in a multi-cell environment. Part of blueprint count-quota-usage-from-placement Change-Id: I7c1ae466a6a6b6d82548396e0c94b1cdab600b5c --- nova/objects/instance_mapping.py | 43 ++++++++++++++++++- .../functional/db/test_instance_mapping.py | 28 ++++++++++++ nova/tests/unit/objects/test_objects.py | 2 +- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/nova/objects/instance_mapping.py b/nova/objects/instance_mapping.py index 26b5c1670278..c4565021824c 100644 --- a/nova/objects/instance_mapping.py +++ b/nova/objects/instance_mapping.py @@ -17,6 +17,7 @@ from oslo_utils import versionutils import six from sqlalchemy.orm import joinedload from sqlalchemy.sql import false +from sqlalchemy.sql import func from sqlalchemy.sql import or_ from nova import context as nova_context @@ -294,7 +295,8 @@ class InstanceMappingList(base.ObjectListBase, base.NovaObject): # Version 1.0: Initial version # Version 1.1: Added get_by_cell_id method. # Version 1.2: Added get_by_instance_uuids method - VERSION = '1.2' + # Version 1.3: Added get_counts() + VERSION = '1.3' fields = { 'objects': fields.ListOfObjectsField('InstanceMapping'), @@ -391,3 +393,42 @@ class InstanceMappingList(base.ObjectListBase, base.NovaObject): context, cell_uuid, project_id, limit) return base.obj_make_list(context, cls(), objects.InstanceMapping, db_mappings) + + @staticmethod + @db_api.api_context_manager.reader + def _get_counts_in_db(context, project_id, user_id=None): + project_query = context.session.query( + func.count(api_models.InstanceMapping.id)).\ + filter_by(queued_for_delete=False).\ + filter_by(project_id=project_id) + project_result = project_query.scalar() + counts = {'project': {'instances': project_result}} + if user_id: + user_result = project_query.filter_by(user_id=user_id).scalar() + counts['user'] = {'instances': user_result} + return counts + + @base.remotable_classmethod + def get_counts(cls, context, project_id, user_id=None): + """Get the counts of InstanceMapping objects in the database. + + The count is used to represent the count of instances for the purpose + of counting quota usage. Instances that are queued_for_deleted=True are + not included in the count (deleted and SOFT_DELETED instances). + Instances that are queued_for_deleted=None are not included in the + count because we are not certain about whether or not they are deleted. + When counting quota usage, we will fall back on the legacy counting + method and count instances, cores, and ram from cell databases if any + unmigrated instance mappings (user_id=None or queued_for_delete=None) + are detected, to avoid using a potentially inaccurate count. + + :param context: The request context for database access + :param project_id: The project_id to count across + :param user_id: The user_id to count across + :returns: A dict containing the project-scoped counts and user-scoped + counts if user_id is specified. For example: + + {'project': {'instances': }, + 'user': {'instances': }} + """ + return cls._get_counts_in_db(context, project_id, user_id=user_id) diff --git a/nova/tests/functional/db/test_instance_mapping.py b/nova/tests/functional/db/test_instance_mapping.py index f89835bfd1d8..1b740df629fd 100644 --- a/nova/tests/functional/db/test_instance_mapping.py +++ b/nova/tests/functional/db/test_instance_mapping.py @@ -577,3 +577,31 @@ class InstanceMappingListTestCase(test.NoDBTestCase): cm.uuid, None)) self.assertEqual(2, len(ims)) + + def test_get_counts(self): + create_mapping(project_id='fake-project', user_id='fake-user', + queued_for_delete=False) + # mapping with another user + create_mapping(project_id='fake-project', user_id='other-user', + queued_for_delete=False) + # mapping in another project + create_mapping(project_id='other-project', user_id='fake-user', + queued_for_delete=False) + # queued_for_delete=True, should not be counted + create_mapping(project_id='fake-project', user_id='fake-user', + queued_for_delete=True) + # queued_for_delete=None (not yet migrated), should not be counted + create_mapping(project_id='fake-project', user_id='fake-user', + queued_for_delete=None) + + # Count only across a project + counts = instance_mapping.InstanceMappingList.get_counts( + self.context, 'fake-project') + self.assertEqual(2, counts['project']['instances']) + self.assertNotIn('user', counts) + + # Count across a project and a user + counts = instance_mapping.InstanceMappingList.get_counts( + self.context, 'fake-project', user_id='fake-user') + self.assertEqual(2, counts['project']['instances']) + self.assertEqual(1, counts['user']['instances']) diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index d2befbe361c0..f216c1fa8017 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1108,7 +1108,7 @@ object_data = { 'InstanceInfoCache': '1.5-cd8b96fefe0fc8d4d337243ba0bf0e1e', 'InstanceList': '2.4-d2c5723da8c1d08e07cb00160edfd292', 'InstanceMapping': '1.2-3bd375e65c8eb9c45498d2f87b882e03', - 'InstanceMappingList': '1.2-ee638619aa3d8a82a59c0c83bfa64d78', + 'InstanceMappingList': '1.3-d34b6ebb076d542ae0f8b440534118da', 'InstanceNUMACell': '1.4-7c1eb9a198dee076b4de0840e45f4f55', 'InstanceNUMATopology': '1.3-ec0030cb0402a49c96da7051c037082a', 'InstancePCIRequest': '1.3-f6d324f1c337fad4f34892ed5f484c9a',