diff --git a/placement/db/sqlalchemy/alembic/versions/422ece571366_add_consumer_types_table.py b/placement/db/sqlalchemy/alembic/versions/422ece571366_add_consumer_types_table.py new file mode 100644 index 000000000..e8bb0d1b0 --- /dev/null +++ b/placement/db/sqlalchemy/alembic/versions/422ece571366_add_consumer_types_table.py @@ -0,0 +1,57 @@ +# 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. + +"""Add consumer_types table + +Revision ID: 422ece571366 +Revises: b5c396305c25 +Create Date: 2019-07-02 13:47:04.165692 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '422ece571366' +down_revision = 'b5c396305c25' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'consumer_types', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.Unicode(length=255), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name', name='uniq_consumer_types0name'), + ) + + with op.batch_alter_table('consumers') as batch_op: + batch_op.add_column( + sa.Column( + 'consumer_type_id', sa.Integer(), + sa.ForeignKey('consumer_types.id', + name='consumers_consumer_type_id_fkey'), + nullable=True + ) + ) + + op.create_index( + 'consumers_consumer_type_id_idx', + 'consumers', + ['consumer_type_id'], + unique=False + ) diff --git a/placement/db/sqlalchemy/models.py b/placement/db/sqlalchemy/models.py index 95e095678..ad388410f 100644 --- a/placement/db/sqlalchemy/models.py +++ b/placement/db/sqlalchemy/models.py @@ -221,6 +221,7 @@ class Consumer(BASE): Index('consumers_project_id_uuid_idx', 'project_id', 'uuid'), Index('consumers_project_id_user_id_uuid_idx', 'project_id', 'user_id', 'uuid'), + Index('consumers_consumer_type_id_idx', 'consumer_type_id'), schema.UniqueConstraint('uuid', name='uniq_consumers0uuid'), ) @@ -229,3 +230,17 @@ class Consumer(BASE): project_id = Column(Integer, nullable=False) user_id = Column(Integer, nullable=False) generation = Column(Integer, nullable=False, server_default="0", default=0) + consumer_type_id = Column( + Integer, ForeignKey('consumer_types.id'), nullable=True) + + +class ConsumerType(BASE): + """Represents a consumer's type.""" + + __tablename__ = 'consumer_types' + __table_args__ = ( + schema.UniqueConstraint('name', name='uniq_consumer_types0name'), + ) + + id = Column(Integer, primary_key=True, nullable=False, autoincrement=True) + name = Column(Unicode(255), nullable=False) diff --git a/placement/exception.py b/placement/exception.py index 11a779c48..cc7a1ea5d 100644 --- a/placement/exception.py +++ b/placement/exception.py @@ -201,3 +201,11 @@ class ConsumerNotFound(NotFound): class ConsumerExists(Exists): msg_fmt = "The consumer %(uuid)s already exists." + + +class ConsumerTypeNotFound(NotFound): + msg_fmt = "No such consumer type: %(name)s." + + +class ConsumerTypeExists(Exists): + msg_fmt = "The consumer type %(name)s already exists." diff --git a/placement/objects/allocation.py b/placement/objects/allocation.py index 524606fd3..a35fc45eb 100644 --- a/placement/objects/allocation.py +++ b/placement/objects/allocation.py @@ -290,6 +290,7 @@ def _get_allocations_by_consumer_uuid(ctx, consumer_uuid): allocs.c.used, consumer.c.id.label("consumer_id"), consumer.c.generation.label("consumer_generation"), + consumer.c.consumer_type_id, sql.func.coalesce( consumer.c.uuid, allocs.c.consumer_id).label("consumer_uuid"), project.c.id.label("project_id"), @@ -445,6 +446,7 @@ def get_all_by_consumer_id(context, consumer_id): context, id=db_first['consumer_id'], uuid=db_first['consumer_uuid'], generation=db_first['consumer_generation'], + consumer_type_id=db_first['consumer_type_id'], project=project_obj.Project( context, id=db_first['project_id'], external_id=db_first['project_external_id']), diff --git a/placement/objects/consumer.py b/placement/objects/consumer.py index aaea49177..5968284d9 100644 --- a/placement/objects/consumer.py +++ b/placement/objects/consumer.py @@ -91,7 +91,7 @@ def delete_consumers_if_no_allocations(ctx, consumer_uuids): def _get_consumer_by_uuid(ctx, uuid): # The SQL for this looks like the following: # SELECT - # c.id, c.uuid, + # c.id, c.uuid, c.consumer_type_id, # p.id AS project_id, p.external_id AS project_external_id, # u.id AS user_id, u.external_id AS user_external_id, # c.updated_at, c.created_at @@ -107,6 +107,7 @@ def _get_consumer_by_uuid(ctx, uuid): cols = [ consumers.c.id, consumers.c.uuid, + consumers.c.consumer_type_id, projects.c.id.label("project_id"), projects.c.external_id.label("project_external_id"), users.c.id.label("user_id"), @@ -143,13 +144,15 @@ def _delete_consumer(ctx, consumer): class Consumer(object): def __init__(self, context, id=None, uuid=None, project=None, user=None, - generation=None, updated_at=None, created_at=None): + generation=None, consumer_type_id=None, updated_at=None, + created_at=None): self._context = context self.id = id self.uuid = uuid self.project = project self.user = user self.generation = generation + self.consumer_type_id = consumer_type_id self.updated_at = updated_at self.created_at = created_at @@ -158,6 +161,7 @@ class Consumer(object): target.id = source['id'] target.uuid = source['uuid'] target.generation = source['generation'] + target.consumer_type_id = source['consumer_type_id'] target.created_at = source['created_at'] target.updated_at = source['updated_at'] @@ -181,7 +185,7 @@ class Consumer(object): def _create_in_db(ctx): db_obj = models.Consumer( uuid=self.uuid, project_id=self.project.id, - user_id=self.user.id) + user_id=self.user.id, consumer_type_id=self.consumer_type_id) try: db_obj.save(ctx.session) # NOTE(jaypipes): We don't do the normal _from_db_object() @@ -200,7 +204,8 @@ class Consumer(object): @db_api.placement_context_manager.writer def _update_in_db(ctx): upd_stmt = CONSUMER_TBL.update().values( - project_id=self.project.id, user_id=self.user.id) + project_id=self.project.id, user_id=self.user.id, + consumer_type_id=self.consumer_type_id) # NOTE(jaypipes): We add the generation check to the WHERE clause # above just for safety. We don't need to check that the statement # actually updated a single row. If it did not, then the diff --git a/placement/objects/consumer_type.py b/placement/objects/consumer_type.py new file mode 100644 index 000000000..9a7011f67 --- /dev/null +++ b/placement/objects/consumer_type.py @@ -0,0 +1,114 @@ +# 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 oslo_db import exception as db_exc +import sqlalchemy as sa + +from placement.db.sqlalchemy import models +from placement import db_api +from placement import exception + +CONSUMER_TYPE_TBL = models.ConsumerType.__table__ +_CONSUMER_TYPES_LOCK = 'consumer_types_sync' +_CONSUMER_TYPES_SYNCED = False +NULL_CONSUMER_TYPE_ALIAS = 'unknown' + + +@db_api.placement_context_manager.reader +def _get_consumer_type_by_id(ctx, id): + # The SQL for this looks like the following: + # SELECT + # c.id, c.name, + # c.updated_at, c.created_at + # FROM consumer_types c + # WHERE c.id = $id + consumer_types = sa.alias(CONSUMER_TYPE_TBL, name="c") + cols = [ + consumer_types.c.id, + consumer_types.c.name, + consumer_types.c.updated_at, + consumer_types.c.created_at + ] + sel = sa.select(cols).where(consumer_types.c.id == id) + res = ctx.session.execute(sel).fetchone() + if not res: + raise exception.ConsumerTypeNotFound(name=id) + + return dict(res) + + +@db_api.placement_context_manager.reader +def _get_consumer_type_by_name(ctx, name): + # The SQL for this looks like the following: + # SELECT + # c.id, c.name, + # c.updated_at, c.created_at + # FROM consumer_types c + # WHERE c.name = $name + consumer_types = sa.alias(CONSUMER_TYPE_TBL, name="c") + cols = [ + consumer_types.c.id, + consumer_types.c.name, + consumer_types.c.updated_at, + consumer_types.c.created_at + ] + sel = sa.select(cols).where(consumer_types.c.name == name) + res = ctx.session.execute(sel).fetchone() + if not res: + raise exception.ConsumerTypeNotFound(name=name) + + return dict(res) + + +@db_api.placement_context_manager.writer +def _create_in_db(ctx, name): + db_obj = models.ConsumerType(name=name) + try: + db_obj.save(ctx.session) + return db_obj + except db_exc.DBDuplicateEntry: + raise exception.ConsumerTypeExists(name=name) + + +class ConsumerType(object): + + def __init__(self, context, id=None, name=None, + updated_at=None, created_at=None): + self._context = context + self.id = id + self.name = name + self.updated_at = updated_at + self.created_at = created_at + + @staticmethod + def _from_db_object(ctx, target, source): + target.id = source['id'] + target.name = source['name'] + target.created_at = source['created_at'] + target.updated_at = source['updated_at'] + + target._context = ctx + return target + + @classmethod + def get_by_id(cls, ctx, id): + res = _get_consumer_type_by_id(ctx, id) + return cls._from_db_object(ctx, cls(ctx), res) + + @classmethod + def get_by_name(cls, ctx, name): + res = _get_consumer_type_by_name(ctx, name) + return cls._from_db_object(ctx, cls(ctx), res) + + def create(self): + ct = _create_in_db(self._context, self.name) + return self._from_db_object(self._context, self, ct) diff --git a/placement/objects/usage.py b/placement/objects/usage.py index 3bdd3aeb3..1e1824490 100644 --- a/placement/objects/usage.py +++ b/placement/objects/usage.py @@ -10,18 +10,24 @@ # License for the specific language governing permissions and limitations # under the License. +from sqlalchemy import distinct from sqlalchemy import func from sqlalchemy import sql from placement.db.sqlalchemy import models from placement import db_api +from placement.objects import consumer_type as consumer_type_obj class Usage(object): - def __init__(self, resource_class=None, usage=0): + def __init__(self, resource_class=None, usage=0, consumer_type=None, + consumer_count=0): self.resource_class = resource_class self.usage = int(usage) + self.consumer_type = (consumer_type or + consumer_type_obj.NULL_CONSUMER_TYPE_ALIAS) + self.consumer_count = int(consumer_count) def get_all_by_resource_provider_uuid(context, rp_uuid): @@ -30,6 +36,14 @@ def get_all_by_resource_provider_uuid(context, rp_uuid): return [Usage(**db_item) for db_item in usage_list] +def get_by_consumer_type(context, project_id, user_id=None, + consumer_type=None): + """Get a list of Usage objects by consumer type.""" + usage_list = _get_by_consumer_type(context, project_id, user_id=user_id, + consumer_type=consumer_type) + return [Usage(**db_item) for db_item in usage_list] + + def get_all_by_project_user(context, project_id, user_id=None): """Get a list of Usage objects filtered by project and (optional) user.""" usage_list = _get_all_by_project_user(context, project_id, @@ -58,7 +72,8 @@ def _get_all_by_resource_provider_uuid(context, rp_uuid): @db_api.placement_context_manager.reader -def _get_all_by_project_user(context, project_id, user_id=None): +def _get_all_by_project_user(context, project_id, user_id=None, + consumer_type=False): query = (context.session.query(models.Allocation.resource_class_id, func.coalesce(func.sum(models.Allocation.used), 0)) .join(models.Consumer, @@ -71,7 +86,78 @@ def _get_all_by_project_user(context, project_id, user_id=None): models.Consumer.user_id == models.User.id) query = query.filter(models.User.external_id == user_id) query = query.group_by(models.Allocation.resource_class_id) + + if consumer_type: + # NOTE(melwitt): We have to count separately in order to get a count of + # unique consumers. If we count after grouping by resource class, we + # will count duplicate consumers for any unique consumer that consumes + # more than one resource class simultaneously (example: an instance + # consuming both VCPU and MEMORY_MB). + count_query = (context.session.query( + func.count(distinct(models.Allocation.consumer_id))) + .join(models.Consumer, + models.Allocation.consumer_id == models.Consumer.uuid) + .join(models.Project, + models.Consumer.project_id == models.Project.id) + .filter(models.Project.external_id == project_id)) + if user_id: + count_query = count_query.join( + models.User, models.Consumer.user_id == models.User.id) + count_query = count_query.filter( + models.User.external_id == user_id) + unique_consumer_count = count_query.scalar() + + result = [dict(resource_class=context.rc_cache.string_from_id(item[0]), + usage=item[1], + consumer_type="all", + consumer_count=unique_consumer_count) + for item in query.all()] + else: + result = [dict(resource_class=context.rc_cache.string_from_id(item[0]), + usage=item[1]) + for item in query.all()] + + return result + + +@db_api.placement_context_manager.reader +def _get_by_consumer_type(context, project_id, user_id=None, + consumer_type=None): + if consumer_type == 'all': + return _get_all_by_project_user(context, project_id, user_id, + consumer_type=True) + query = (context.session.query( + models.Allocation.resource_class_id, + func.coalesce(func.sum(models.Allocation.used), 0), + func.count(distinct(models.Allocation.consumer_id)), + models.ConsumerType.name) + .join(models.Consumer, + models.Allocation.consumer_id == models.Consumer.uuid) + .outerjoin(models.ConsumerType, + models.Consumer.consumer_type_id == + models.ConsumerType.id) + .join(models.Project, + models.Consumer.project_id == models.Project.id) + .filter(models.Project.external_id == project_id)) + if user_id: + query = query.join(models.User, + models.Consumer.user_id == models.User.id) + query = query.filter(models.User.external_id == user_id) + if consumer_type: + query = query.filter(models.ConsumerType.name == consumer_type) + # NOTE(melwitt): We have to count grouped by only consumer type first in + # order to get a count of unique consumers for a given consumer type. If we + # only count after grouping by resource class, we will count duplicate + # consumers for any unique consumer that consumes more than one resource + # class simultaneously (example: an instance consuming both VCPU and + # MEMORY_MB). + unique_consumer_counts = {item[3]: item[2] for item in + query.group_by(models.ConsumerType.name).all()} + query = query.group_by(models.Allocation.resource_class_id, + models.Consumer.consumer_type_id) result = [dict(resource_class=context.rc_cache.string_from_id(item[0]), - usage=item[1]) + usage=item[1], + consumer_count=unique_consumer_counts[item[3]], + consumer_type=item[3]) for item in query.all()] return result diff --git a/placement/tests/functional/db/test_allocation.py b/placement/tests/functional/db/test_allocation.py index 699bf0bcb..2e839e6ef 100644 --- a/placement/tests/functional/db/test_allocation.py +++ b/placement/tests/functional/db/test_allocation.py @@ -18,6 +18,7 @@ from oslo_utils.fixture import uuidsentinel from placement import exception from placement.objects import allocation as alloc_obj from placement.objects import consumer as consumer_obj +from placement.objects import consumer_type as ct_obj from placement.objects import inventory as inv_obj from placement.objects import usage as usage_obj from placement.tests.functional.db import test_base as tb @@ -93,10 +94,16 @@ class TestAllocation(tb.PlacementDbBaseTestCase): step_size=64, allocation_ratio=1.5) + # Create an INSTANCE consumer type + ct = ct_obj.ConsumerType(self.ctx, name='INSTANCE') + ct.create() + # Save consumer type id for later confirmation. + ct_id = ct.id + # Create a consumer representing the instance inst_consumer = consumer_obj.Consumer( self.ctx, uuid=uuidsentinel.instance, user=self.user_obj, - project=self.project_obj) + project=self.project_obj, consumer_type_id=ct_id) inst_consumer.create() # Now create an allocation that represents a move operation where the @@ -175,6 +182,10 @@ class TestAllocation(tb.PlacementDbBaseTestCase): self.assertEqual(2, len(consumer_allocs)) + # check the allocations have the expected INSTANCE consumer type + self.assertEqual(ct_id, consumer_allocs[0].consumer.consumer_type_id) + self.assertEqual(ct_id, consumer_allocs[1].consumer.consumer_type_id) + def test_get_all_by_resource_provider(self): rp, allocation = self._make_allocation(tb.DISK_INVENTORY, tb.DISK_ALLOCATION) diff --git a/placement/tests/functional/db/test_consumer_type.py b/placement/tests/functional/db/test_consumer_type.py new file mode 100644 index 000000000..cbc9da230 --- /dev/null +++ b/placement/tests/functional/db/test_consumer_type.py @@ -0,0 +1,47 @@ +# 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 placement import exception +from placement.objects import consumer_type as ct_obj +from placement.tests.functional.db import test_base as tb + + +class ConsumerTypeTestCase(tb.PlacementDbBaseTestCase): + + def test_get_by_name_and_id(self): + + ct = ct_obj.ConsumerType(self.context, name='MIGRATION') + ct.create() + + named_ct = ct_obj.ConsumerType.get_by_name(self.context, 'MIGRATION') + self.assertEqual(ct.id, named_ct.id) + + id_ct = ct_obj.ConsumerType.get_by_id(self.context, ct.id) + self.assertEqual(ct.name, id_ct.name) + + def test_id_not_found(self): + self.assertRaises( + exception.ConsumerTypeNotFound, ct_obj.ConsumerType.get_by_id, + self.context, 999999) + + def test_name_not_found(self): + self.assertRaises( + exception.ConsumerTypeNotFound, ct_obj.ConsumerType.get_by_name, + self.context, 'LOSTPONY') + + def test_duplicate_create(self): + ct = ct_obj.ConsumerType(self.context, name='MIGRATION') + ct.create() + + ct2 = ct_obj.ConsumerType(self.context, name='MIGRATION') + self.assertRaises(exception.ConsumerTypeExists, ct2.create) diff --git a/placement/tests/functional/db/test_migrations.py b/placement/tests/functional/db/test_migrations.py index af4b1df93..7358e16f4 100644 --- a/placement/tests/functional/db/test_migrations.py +++ b/placement/tests/functional/db/test_migrations.py @@ -33,6 +33,7 @@ from oslo_db.sqlalchemy import utils as db_utils from oslo_log import log as logging from oslo_utils.fixture import uuidsentinel as uuids from oslotest import base as test_base +from sqlalchemy import inspect import testtools from placement.db.sqlalchemy import migration @@ -226,6 +227,38 @@ class MigrationCheckersMixin(object): }).execute().inserted_primary_key[0] self.migration_api.upgrade('b5c396305c25') + def test_consumer_types_422ece571366(self): + # Upgrade to populate the schema. + self.migration_api.upgrade('422ece571366') + insp = inspect(self.engine) + # Test creation of consumer_types table + con = db_utils.get_table(self.engine, 'consumer_types') + col_names = [column.name for column in con.c] + self.assertIn('created_at', col_names) + self.assertIn('updated_at', col_names) + self.assertIn('id', col_names) + self.assertIn('name', col_names) + # check constraints + pkey = insp.get_pk_constraint("consumer_types") + self.assertEqual(['id'], pkey['constrained_columns']) + ukey = insp.get_unique_constraints("consumer_types") + self.assertEqual('uniq_consumer_types0name', ukey[0]['name']) + + def test_consumer_type_id_column_422ece571366(self): + # Upgrade to populate the schema. + self.migration_api.upgrade('422ece571366') + insp = inspect(self.engine) + # Test creation of consumer_types table + consumers = db_utils.get_table(self.engine, 'consumers') + col_names = [column.name for column in consumers.c] + self.assertIn('consumer_type_id', col_names) + # Check index and constraints + fkey = insp.get_foreign_keys("consumers") + self.assertEqual(['consumer_type_id'], fkey[0]['constrained_columns']) + ind = insp.get_indexes('consumers') + names = [r['name'] for r in ind] + self.assertIn('consumers_consumer_type_id_idx', names) + class PlacementOpportunisticFixture(object): def get_enginefacade(self): diff --git a/placement/tests/functional/db/test_usage.py b/placement/tests/functional/db/test_usage.py index ae95a4ba2..b437f0374 100644 --- a/placement/tests/functional/db/test_usage.py +++ b/placement/tests/functional/db/test_usage.py @@ -10,9 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + import os_resource_classes as orc from oslo_utils.fixture import uuidsentinel +from oslo_utils import uuidutils +from placement.objects import consumer as c_obj +from placement.objects import consumer_type as ct_obj from placement.objects import inventory as inv_obj from placement.objects import usage as usage_obj from placement.tests.functional.db import test_base as tb @@ -62,3 +67,123 @@ class UsageListTestCase(tb.PlacementDbBaseTestCase): usages = usage_obj.get_all_by_resource_provider_uuid( self.ctx, db_rp.uuid) self.assertEqual(2, len(usages)) + + def test_get_by_unspecified_consumer_type(self): + # This will add a consumer with a NULL consumer type and the default + # project and user external_ids + self._make_allocation(tb.DISK_INVENTORY, tb.DISK_ALLOCATION) + + # Verify we filter the project external_id correctly. Note: this will + # also work if filtering is broken (if it's not filtering at all) + usages = usage_obj.get_by_consumer_type( + self.ctx, self.project_obj.external_id) + self.assertEqual(1, len(usages)) + usage = usages[0] + self.assertEqual('unknown', usage.consumer_type) + self.assertEqual(1, usage.consumer_count) + self.assertEqual(orc.DISK_GB, usage.resource_class) + self.assertEqual(2, usage.usage) + # Verify we get nothing back if we filter on a different project + # external_id that does not exist (will not work if filtering is + # broken) + usages = usage_obj.get_by_consumer_type(self.ctx, 'BOGUS') + self.assertEqual(0, len(usages)) + + def test_get_by_specified_consumer_type(self): + ct = ct_obj.ConsumerType(self.ctx, name='INSTANCE') + ct.create() + consumer_id = uuidutils.generate_uuid() + c = c_obj.Consumer(self.ctx, uuid=consumer_id, + project=self.project_obj, user=self.user_obj, + consumer_type_id=ct.id) + c.create() + # This will add a consumer with the consumer type INSTANCE + # and the default project and user external_ids + da = copy.deepcopy(tb.DISK_ALLOCATION) + da['consumer_id'] = c.uuid + self._make_allocation(tb.DISK_INVENTORY, da) + + # Verify we filter the INSTANCE type correctly. Note: this will also + # work if filtering is broken (if it's not filtering at all) + usages = usage_obj.get_by_consumer_type( + self.ctx, self.project_obj.external_id, + consumer_type=ct.name) + self.assertEqual(1, len(usages)) + usage = usages[0] + self.assertEqual(ct.name, usage.consumer_type) + self.assertEqual(1, usage.consumer_count) + self.assertEqual(orc.DISK_GB, usage.resource_class) + self.assertEqual(2, usage.usage) + # Verify we get nothing back if we filter on a different consumer + # type that does not exist (will not work if filtering is broken) + usages = usage_obj.get_by_consumer_type( + self.ctx, self.project_obj.external_id, + consumer_type='BOGUS') + self.assertEqual(0, len(usages)) + + def test_get_by_specified_consumer_type_with_user(self): + ct = ct_obj.ConsumerType(self.ctx, name='INSTANCE') + ct.create() + consumer_id = uuidutils.generate_uuid() + c = c_obj.Consumer(self.ctx, uuid=consumer_id, + project=self.project_obj, user=self.user_obj, + consumer_type_id=ct.id) + c.create() + # This will add a consumer with the consumer type INSTANCE + # and the default project and user external_ids + da = copy.deepcopy(tb.DISK_ALLOCATION) + da['consumer_id'] = c.uuid + db_rp, _ = self._make_allocation(tb.DISK_INVENTORY, da) + + # Verify we filter the user external_id correctly. Note: this will also + # work if filtering is broken (if it's not filtering at all) + usages = usage_obj.get_by_consumer_type( + self.ctx, self.project_obj.external_id, + user_id=self.user_obj.external_id, + consumer_type=ct.name) + self.assertEqual(1, len(usages)) + usage = usages[0] + self.assertEqual(ct.name, usage.consumer_type) + self.assertEqual(1, usage.consumer_count) + self.assertEqual(orc.DISK_GB, usage.resource_class) + self.assertEqual(2, usage.usage) + # Verify we get nothing back if we filter on a different user + # external_id that does not exist (will not work if filtering is + # broken) + usages = usage_obj.get_by_consumer_type( + self.ctx, self.project_obj.external_id, + user_id='BOGUS', + consumer_type=ct.name) + self.assertEqual(0, len(usages)) + + def test_get_by_all_consumer_type(self): + # This will add a consumer with the default consumer type UNKNOWN + db_rp, _ = self._make_allocation(tb.DISK_INVENTORY, + tb.DISK_ALLOCATION) + # Make another allocation with a different consumer type + ct = ct_obj.ConsumerType(self.ctx, name='FOO') + ct.create() + consumer_id = uuidutils.generate_uuid() + c = c_obj.Consumer(self.ctx, uuid=consumer_id, + project=self.project_obj, user=self.user_obj, + consumer_type_id=ct.id) + c.create() + self.allocate_from_provider(db_rp, orc.DISK_GB, 2, consumer=c) + + # Verify we get usages back for both consumer types with 'all' + usages = usage_obj.get_by_consumer_type( + self.ctx, self.project_obj.external_id, consumer_type='all') + self.assertEqual(1, len(usages)) + usage = usages[0] + self.assertEqual('all', usage.consumer_type) + self.assertEqual(2, usage.consumer_count) + self.assertEqual(orc.DISK_GB, usage.resource_class) + self.assertEqual(4, usage.usage) + + def test_get_by_unused_consumer_type(self): + # This will add a consumer with the default consumer type UNKNOWN + self._make_allocation(tb.DISK_INVENTORY, tb.DISK_ALLOCATION) + + usages = usage_obj.get_by_consumer_type( + self.ctx, self.project_obj.external_id, consumer_type='EMPTY') + self.assertEqual(0, len(usages)) diff --git a/placement/tests/unit/objects/test_allocation.py b/placement/tests/unit/objects/test_allocation.py index 78ca39e7b..0f7cba6a8 100644 --- a/placement/tests/unit/objects/test_allocation.py +++ b/placement/tests/unit/objects/test_allocation.py @@ -48,6 +48,7 @@ _ALLOCATION_BY_CONSUMER_DB = { 'resource_class_id': _RESOURCE_CLASS_ID, 'consumer_uuid': uuids.fake_instance, 'consumer_id': 1, + 'consumer_type_id': 1, 'consumer_generation': 0, 'used': 8, 'user_id': 1,