Add consumer_types migration, database and object changes
In advance of a microversion 1.38, add the necessary database and object code to support saving consumer types when creating consumers and reporting those types when requesting usage information. This patch adds a new table called ``consumer_types`` and also add a new column called ``consumer_type_id`` that references the id in the consumer_types table. The consumer_type_id is nullable and records with a NULL consumer_type_id are considered as 'unknown'. In future changes we may wish to AttributeCache consumer types in the same way that traits and resource classes are cached. Usage object can now be retrieved by consumer type and include consumer type information when retrieved. Until 1.38 that info will not be used in results. Allocations are written and retrieved with a consumer type but the data is not yet presented in HTTP requests. Co-Authored-By: Chris Dent <cdent@anticdent.org> Co-Authored-By: melanie witt <melwittt@gmail.com> Story: 2005473 Task: 35683 Task: 36420 Change-Id: Ic74c3d1b8aa272e9c7d1070fe7658b928e693889
This commit is contained in:
parent
c4d785cc35
commit
b1f3dd39c3
@ -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
|
||||
)
|
@ -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)
|
||||
|
@ -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."
|
||||
|
@ -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']),
|
||||
|
@ -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
|
||||
|
114
placement/objects/consumer_type.py
Normal file
114
placement/objects/consumer_type.py
Normal file
@ -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)
|
@ -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
|
||||
|
@ -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)
|
||||
|
47
placement/tests/functional/db/test_consumer_type.py
Normal file
47
placement/tests/functional/db/test_consumer_type.py
Normal file
@ -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)
|
@ -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):
|
||||
|
@ -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))
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user