Allocation API: database and RPC

This change adds the database models and API, as well as RPC objects
for the allocation API. Also the node database API is extended with
query by power state and list of UUIDs.

There is one discrepancy from the initially approved spec: since we
do not have to separately update traits in an allocation, the planned
allocation_traits table was replaced by a simple field.

Change-Id: I6af132e2bfa6e4f7b93bd20f22a668790a22a30e
Story: #2004341
Task: #28367
This commit is contained in:
Dmitry Tantsur 2018-12-10 16:50:42 +01:00
parent c10ee94b92
commit a4717d9958
18 changed files with 1235 additions and 6 deletions

View File

@ -81,6 +81,8 @@ ONLINE_MIGRATIONS = (
# These are the models added in supported releases. We skip the version check
# for them since the tables do not exist when it happens.
NEW_MODELS = [
# TODO(dtantsur): remove in Train
'Allocation',
]

View File

@ -791,3 +791,15 @@ class AgentConnectionFailed(IronicException):
class NodeProtected(HTTPForbidden):
_msg_fmt = _("Node %(node)s is protected and cannot be undeployed, "
"rebuilt or deleted")
class AllocationNotFound(NotFound):
_msg_fmt = _("Allocation %(allocation)s could not be found.")
class AllocationDuplicateName(Conflict):
_msg_fmt = _("An allocation with name %(name)s already exists.")
class AllocationAlreadyExists(Conflict):
_msg_fmt = _("An allocation with UUID %(uuid)s already exists.")

View File

@ -134,7 +134,8 @@ RELEASE_MAPPING = {
'api': '1.50',
'rpc': '1.47',
'objects': {
'Node': ['1.30', '1.29', '1.28'],
'Allocation': ['1.0'],
'Node': ['1.31', '1.30', '1.29', '1.28'],
'Conductor': ['1.3'],
'Chassis': ['1.3'],
'Port': ['1.8'],

View File

@ -1079,3 +1079,82 @@ class Connection(object):
:returns: A list of BIOSSetting objects.
:raises: NodeNotFound if the node is not found.
"""
@abc.abstractmethod
def get_allocation_by_id(self, allocation_id):
"""Return an allocation representation.
:param allocation_id: The id of an allocation.
:returns: An allocation.
:raises: AllocationNotFound
"""
@abc.abstractmethod
def get_allocation_by_uuid(self, allocation_uuid):
"""Return an allocation representation.
:param allocation_uuid: The uuid of an allocation.
:returns: An allocation.
:raises: AllocationNotFound
"""
@abc.abstractmethod
def get_allocation_by_name(self, name):
"""Return an allocation representation.
:param name: The logical name of an allocation.
:returns: An allocation.
:raises: AllocationNotFound
"""
@abc.abstractmethod
def get_allocation_list(self, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of allocations.
:param filters: Filters to apply. Defaults to None.
:node_uuid: uuid of node
:state: allocation state
:resource_class: requested resource class
:param limit: Maximum number of allocations to return.
:param marker: The last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc)
:returns: A list of allocations.
"""
@abc.abstractmethod
def create_allocation(self, values):
"""Create a new allocation.
:param values: Dict of values to create an allocation with
:returns: An allocation
:raises: AllocationDuplicateName
:raises: AllocationAlreadyExists
"""
@abc.abstractmethod
def update_allocation(self, allocation_id, values, update_node=True):
"""Update properties of an allocation.
:param allocation_id: Allocation ID
:param values: Dict of values to update.
:param update_node: If True and node_id is updated, update the node
with instance_uuid and traits from the allocation
:returns: An allocation.
:raises: AllocationNotFound
:raises: AllocationDuplicateName
:raises: InstanceAssociated
:raises: NodeAssociated
"""
@abc.abstractmethod
def destroy_allocation(self, allocation_id):
"""Destroy an allocation.
:param allocation_id: Allocation ID
:raises: AllocationNotFound
"""

View File

@ -0,0 +1,56 @@
# 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 Allocations table
Revision ID: dd67b91a1981
Revises: f190f9d00a11
Create Date: 2018-12-10 15:24:30.555995
"""
from alembic import op
from oslo_db.sqlalchemy import types
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'dd67b91a1981'
down_revision = 'f190f9d00a11'
def upgrade():
op.create_table(
'allocations',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('version', sa.String(length=15), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('node_id', sa.Integer(), nullable=True),
sa.Column('state', sa.String(length=15), nullable=False),
sa.Column('last_error', sa.Text(), nullable=True),
sa.Column('resource_class', sa.String(length=80), nullable=True),
sa.Column('traits', types.JsonEncodedList(), nullable=True),
sa.Column('candidate_nodes', types.JsonEncodedList(), nullable=True),
sa.Column('extra', types.JsonEncodedDict(), nullable=True),
sa.Column('conductor_affinity', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['conductor_affinity'], ['conductors.id'], ),
sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name', name='uniq_allocations0name'),
sa.UniqueConstraint('uuid', name='uniq_allocations0uuid')
)
op.add_column('nodes', sa.Column('allocation_id', sa.Integer(),
nullable=True))
op.create_foreign_key(None, 'nodes', 'allocations',
['allocation_id'], ['id'])

View File

@ -224,7 +224,8 @@ class Connection(api.Connection):
'chassis_uuid', 'associated', 'reserved',
'reserved_by_any_of', 'provisioned_before',
'inspection_started_before', 'fault',
'conductor_group', 'owner'}
'conductor_group', 'owner',
'uuid_in', 'with_power_state'}
unsupported_filters = set(filters).difference(supported_filters)
if unsupported_filters:
msg = _("SqlAlchemy API does not support "
@ -263,9 +264,38 @@ class Connection(api.Connection):
- (datetime.timedelta(
seconds=filters['inspection_started_before'])))
query = query.filter(models.Node.inspection_started_at < limit)
if 'uuid_in' in filters:
query = query.filter(models.Node.uuid.in_(filters['uuid_in']))
if 'with_power_state' in filters:
if filters['with_power_state']:
query = query.filter(models.Node.power_state != sql.null())
else:
query = query.filter(models.Node.power_state == sql.null())
return query
def _add_allocations_filters(self, query, filters):
if filters is None:
filters = dict()
supported_filters = {'state', 'resource_class', 'node_uuid'}
unsupported_filters = set(filters).difference(supported_filters)
if unsupported_filters:
msg = _("SqlAlchemy API does not support "
"filtering by %s") % ', '.join(unsupported_filters)
raise ValueError(msg)
try:
node_uuid = filters.pop('node_uuid')
except KeyError:
pass
else:
node_obj = self.get_node_by_uuid(node_uuid)
filters['node_id'] = node_obj.id
if filters:
query = query.filter_by(**filters)
return query
def get_nodeinfo_list(self, columns=None, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
# list-ify columns default values because it is bad form
@ -452,6 +482,11 @@ class Connection(api.Connection):
models.BIOSSetting).filter_by(node_id=node_id)
bios_settings_query.delete()
# delete all allocations for this node
allocation_query = model_query(
models.Allocation).filter_by(node_id=node_id)
allocation_query.delete()
query.delete()
def update_node(self, node_id, values):
@ -1482,3 +1517,173 @@ class Connection(api.Connection):
.filter_by(node_id=node_id)
.all())
return result
def get_allocation_by_id(self, allocation_id):
"""Return an allocation representation.
:param allocation_id: The id of an allocation.
:returns: An allocation.
:raises: AllocationNotFound
"""
query = model_query(models.Allocation).filter_by(id=allocation_id)
try:
return query.one()
except NoResultFound:
raise exception.AllocationNotFound(allocation=allocation_id)
def get_allocation_by_uuid(self, allocation_uuid):
"""Return an allocation representation.
:param allocation_uuid: The uuid of an allocation.
:returns: An allocation.
:raises: AllocationNotFound
"""
query = model_query(models.Allocation).filter_by(uuid=allocation_uuid)
try:
return query.one()
except NoResultFound:
raise exception.AllocationNotFound(allocation=allocation_uuid)
def get_allocation_by_name(self, name):
"""Return an allocation representation.
:param name: The logical name of an allocation.
:returns: An allocation.
:raises: AllocationNotFound
"""
query = model_query(models.Allocation).filter_by(name=name)
try:
return query.one()
except NoResultFound:
raise exception.AllocationNotFound(allocation=name)
def get_allocation_list(self, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of allocations.
:param filters: Filters to apply. Defaults to None.
:node_uuid: uuid of node
:state: allocation state
:resource_class: requested resource class
:param limit: Maximum number of allocations to return.
:param marker: The last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc)
:returns: A list of allocations.
"""
query = self._add_allocations_filters(model_query(models.Allocation),
filters)
return _paginate_query(models.Allocation, limit, marker,
sort_key, sort_dir, query)
@oslo_db_api.retry_on_deadlock
def create_allocation(self, values):
"""Create a new allocation.
:param values: Dict of values to create an allocation with
:returns: An allocation
:raises: AllocationDuplicateName
:raises: AllocationAlreadyExists
"""
if not values.get('uuid'):
values['uuid'] = uuidutils.generate_uuid()
allocation = models.Allocation()
allocation.update(values)
with _session_for_write() as session:
try:
session.add(allocation)
session.flush()
except db_exc.DBDuplicateEntry as exc:
if 'name' in exc.columns:
raise exception.AllocationDuplicateName(
name=values['name'])
else:
raise exception.AllocationAlreadyExists(
uuid=values['uuid'])
return allocation
@oslo_db_api.retry_on_deadlock
def update_allocation(self, allocation_id, values, update_node=True):
"""Update properties of an allocation.
:param allocation_id: Allocation ID
:param values: Dict of values to update.
:param update_node: If True and node_id is updated, update the node
with instance_uuid and traits from the allocation
:returns: An allocation.
:raises: AllocationNotFound
:raises: AllocationDuplicateName
:raises: InstanceAssociated
:raises: NodeAssociated
"""
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing allocation.")
raise exception.InvalidParameterValue(err=msg)
# These values are used in exception handling. They should always be
# initialized, but set them to None just in case.
instance_uuid = node_uuid = None
with _session_for_write() as session:
try:
query = model_query(models.Allocation, session=session)
query = add_identity_filter(query, allocation_id)
ref = query.one()
ref.update(values)
instance_uuid = ref.uuid
if 'node_id' in values and update_node:
node = model_query(models.Node, session=session).filter_by(
id=ref.node_id).with_lockmode('update').one()
node_uuid = node.uuid
if node.instance_uuid and node.instance_uuid != ref.uuid:
raise exception.NodeAssociated(
node=node.uuid, instance=node.instance_uuid)
iinfo = node.instance_info.copy()
iinfo['traits'] = ref.traits or []
node.update({'allocation_id': ref.id,
'instance_uuid': instance_uuid,
'instance_info': iinfo})
session.flush()
except NoResultFound:
raise exception.AllocationNotFound(allocation=allocation_id)
except db_exc.DBDuplicateEntry as exc:
if 'name' in exc.columns:
raise exception.AllocationDuplicateName(
name=values['name'])
elif 'instance_uuid' in exc.columns:
# Case when the referenced node is associated with an
# instance already.
raise exception.InstanceAssociated(
instance_uuid=instance_uuid, node=node_uuid)
else:
raise
return ref
@oslo_db_api.retry_on_deadlock
def destroy_allocation(self, allocation_id):
"""Destroy an allocation.
:param allocation_id: Allocation ID or UUID
:raises: AllocationNotFound
"""
with _session_for_write() as session:
query = model_query(models.Allocation)
query = add_identity_filter(query, allocation_id)
try:
ref = query.one()
except NoResultFound:
raise exception.AllocationNotFound(allocation=allocation_id)
allocation_id = ref['id']
node_query = model_query(models.Node, session=session).filter_by(
allocation_id=allocation_id)
node_query.update({'allocation_id': None, 'instance_uuid': None})
query.delete()

View File

@ -180,6 +180,9 @@ class Node(Base):
server_default=false())
protected_reason = Column(Text, nullable=True)
owner = Column(String(255), nullable=True)
allocation_id = Column(Integer, ForeignKey('allocations.id'),
nullable=True)
bios_interface = Column(String(255), nullable=True)
boot_interface = Column(String(255), nullable=True)
console_interface = Column(String(255), nullable=True)
@ -322,6 +325,29 @@ class BIOSSetting(Base):
value = Column(Text, nullable=True)
class Allocation(Base):
"""Represents an allocation of a node for deployment."""
__tablename__ = 'allocations'
__table_args__ = (
schema.UniqueConstraint('name', name='uniq_allocations0name'),
schema.UniqueConstraint('uuid', name='uniq_allocations0uuid'),
table_args())
id = Column(Integer, primary_key=True)
uuid = Column(String(36), nullable=False)
name = Column(String(255), nullable=True)
node_id = Column(Integer, ForeignKey('nodes.id'), nullable=True)
state = Column(String(15), nullable=False)
last_error = Column(Text, nullable=True)
resource_class = Column(String(80), nullable=True)
traits = Column(db_types.JsonEncodedList)
candidate_nodes = Column(db_types.JsonEncodedList)
extra = Column(db_types.JsonEncodedDict)
# The last conductor to handle this allocation (internal field).
conductor_affinity = Column(Integer, ForeignKey('conductors.id'),
nullable=True)
def get_class(model_name):
"""Returns the model class with the specified name.

View File

@ -24,6 +24,7 @@ def register_all():
# NOTE(danms): You must make sure your object gets imported in this
# function in order for it to be registered by services that may
# need to receive it via RPC.
__import__('ironic.objects.allocation')
__import__('ironic.objects.bios')
__import__('ironic.objects.chassis')
__import__('ironic.objects.conductor')

View File

@ -0,0 +1,300 @@
# 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_utils import strutils
from oslo_utils import uuidutils
from oslo_versionedobjects import base as object_base
from ironic.common import exception
from ironic.common import utils
from ironic.db import api as dbapi
from ironic.objects import base
from ironic.objects import fields as object_fields
from ironic.objects import notification
@base.IronicObjectRegistry.register
class Allocation(base.IronicObject, object_base.VersionedObjectDictCompat):
# Version 1.0: Initial version
VERSION = '1.0'
dbapi = dbapi.get_instance()
fields = {
'id': object_fields.IntegerField(),
'uuid': object_fields.UUIDField(nullable=True),
'name': object_fields.StringField(nullable=True),
'node_id': object_fields.IntegerField(nullable=True),
'state': object_fields.StringField(nullable=True),
'last_error': object_fields.StringField(nullable=True),
'resource_class': object_fields.StringField(nullable=True),
'traits': object_fields.ListOfStringsField(nullable=True),
'candidate_nodes': object_fields.ListOfStringsField(nullable=True),
'extra': object_fields.FlexibleDictField(nullable=True),
'conductor_affinity': object_fields.IntegerField(nullable=True),
}
def _convert_to_version(self, target_version,
remove_unavailable_fields=True):
"""Convert to the target version.
Convert the object to the target version. The target version may be
the same, older, or newer than the version of the object. This is
used for DB interactions as well as for serialization/deserialization.
:param target_version: the desired version of the object
:param remove_unavailable_fields: True to remove fields that are
unavailable in the target version; set this to True when
(de)serializing. False to set the unavailable fields to appropriate
values; set this to False for DB interactions.
"""
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
# methods can be used in the future to replace current explicit RPC calls.
# Implications of calling new remote procedures should be thought through.
# @object_base.remotable_classmethod
@classmethod
def get(cls, context, allocation_ident):
"""Find a allocation based on its id, uuid, name or address.
:param allocation_ident: The id, uuid, name or address of a allocation.
:param context: Security context
:returns: A :class:`Allocation` object.
:raises: InvalidIdentity
"""
if strutils.is_int_like(allocation_ident):
return cls.get_by_id(context, allocation_ident)
elif uuidutils.is_uuid_like(allocation_ident):
return cls.get_by_uuid(context, allocation_ident)
elif utils.is_valid_logical_name(allocation_ident):
return cls.get_by_name(context, allocation_ident)
else:
raise exception.InvalidIdentity(identity=allocation_ident)
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
# methods can be used in the future to replace current explicit RPC calls.
# Implications of calling new remote procedures should be thought through.
# @object_base.remotable_classmethod
@classmethod
def get_by_id(cls, context, allocation_id):
"""Find a allocation by its integer ID and return a Allocation object.
:param cls: the :class:`Allocation`
:param context: Security context
:param allocation_id: The ID of a allocation.
:returns: A :class:`Allocation` object.
:raises: AllocationNotFound
"""
db_allocation = cls.dbapi.get_allocation_by_id(allocation_id)
allocation = cls._from_db_object(context, cls(), db_allocation)
return allocation
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
# methods can be used in the future to replace current explicit RPC calls.
# Implications of calling new remote procedures should be thought through.
# @object_base.remotable_classmethod
@classmethod
def get_by_uuid(cls, context, uuid):
"""Find a allocation by UUID and return a :class:`Allocation` object.
:param cls: the :class:`Allocation`
:param context: Security context
:param uuid: The UUID of a allocation.
:returns: A :class:`Allocation` object.
:raises: AllocationNotFound
"""
db_allocation = cls.dbapi.get_allocation_by_uuid(uuid)
allocation = cls._from_db_object(context, cls(), db_allocation)
return allocation
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
# methods can be used in the future to replace current explicit RPC calls.
# Implications of calling new remote procedures should be thought through.
# @object_base.remotable_classmethod
@classmethod
def get_by_name(cls, context, name):
"""Find allocation based on name and return a :class:`Allocation` object.
:param cls: the :class:`Allocation`
:param context: Security context
:param name: The name of a allocation.
:returns: A :class:`Allocation` object.
:raises: AllocationNotFound
"""
db_allocation = cls.dbapi.get_allocation_by_name(name)
allocation = cls._from_db_object(context, cls(), db_allocation)
return allocation
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
# methods can be used in the future to replace current explicit RPC calls.
# Implications of calling new remote procedures should be thought through.
# @object_base.remotable_classmethod
@classmethod
def list(cls, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of Allocation objects.
:param cls: the :class:`Allocation`
:param context: Security context.
:param filters: Filters to apply.
:param limit: Maximum number of resources to return in a single result.
:param marker: Pagination marker for large data sets.
:param sort_key: Column to sort results by.
:param sort_dir: Direction to sort. "asc" or "desc".
:returns: A list of :class:`Allocation` object.
:raises: InvalidParameterValue
"""
db_allocations = cls.dbapi.get_allocation_list(filters=filters,
limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
return cls._from_db_object_list(context, db_allocations)
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
# methods can be used in the future to replace current explicit RPC calls.
# Implications of calling new remote procedures should be thought through.
# @object_base.remotable
def create(self, context=None):
"""Create a Allocation record in the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Allocation(context)
:raises: AllocationDuplicateName, AllocationAlreadyExists
"""
values = self.do_version_changes_for_db()
db_allocation = self.dbapi.create_allocation(values)
self._from_db_object(self._context, self, db_allocation)
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
# methods can be used in the future to replace current explicit RPC calls.
# Implications of calling new remote procedures should be thought through.
# @object_base.remotable
def destroy(self, context=None):
"""Delete the Allocation from the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Allocation(context)
:raises: AllocationNotFound
"""
self.dbapi.destroy_allocation(self.uuid)
self.obj_reset_changes()
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
# methods can be used in the future to replace current explicit RPC calls.
# Implications of calling new remote procedures should be thought through.
# @object_base.remotable
def save(self, context=None):
"""Save updates to this Allocation.
Updates will be made column by column based on the result
of self.what_changed().
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Allocation(context)
:raises: AllocationNotFound, AllocationDuplicateName
"""
updates = self.do_version_changes_for_db()
updated_allocation = self.dbapi.update_allocation(self.uuid, updates)
self._from_db_object(self._context, self, updated_allocation)
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
# methods can be used in the future to replace current explicit RPC calls.
# Implications of calling new remote procedures should be thought through.
# @object_base.remotable
def refresh(self, context=None):
"""Loads updates for this Allocation.
Loads a allocation with the same uuid from the database and
checks for updated attributes. Updates are applied from
the loaded allocation column by column, if there are any updates.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Allocation(context)
:raises: AllocationNotFound
"""
current = self.get_by_uuid(self._context, uuid=self.uuid)
self.obj_refresh(current)
self.obj_reset_changes()
@base.IronicObjectRegistry.register
class AllocationCRUDNotification(notification.NotificationBase):
"""Notification when ironic creates, updates or deletes a allocation."""
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'payload': object_fields.ObjectField('AllocationCRUDPayload')
}
@base.IronicObjectRegistry.register
class AllocationCRUDPayload(notification.NotificationPayloadBase):
# Version 1.0: Initial version
VERSION = '1.0'
SCHEMA = {
'candidate_nodes': ('allocation', 'candidate_nodes'),
'created_at': ('allocation', 'created_at'),
'extra': ('allocation', 'extra'),
'last_error': ('allocation', 'last_error'),
'name': ('allocation', 'name'),
'resource_class': ('allocation', 'resource_class'),
'state': ('allocation', 'state'),
'traits': ('allocation', 'traits'),
'updated_at': ('allocation', 'updated_at'),
'uuid': ('allocation', 'uuid')
}
fields = {
'uuid': object_fields.UUIDField(nullable=True),
'name': object_fields.StringField(nullable=True),
'node_uuid': object_fields.StringField(nullable=True),
'state': object_fields.StringField(nullable=True),
'last_error': object_fields.StringField(nullable=True),
'resource_class': object_fields.StringField(nullable=True),
'traits': object_fields.ListOfStringsField(nullable=True),
'candidate_nodes': object_fields.ListOfStringsField(nullable=True),
'extra': object_fields.FlexibleDictField(nullable=True),
'created_at': object_fields.DateTimeField(nullable=True),
'updated_at': object_fields.DateTimeField(nullable=True),
}
def __init__(self, allocation, node_uuid):
super(AllocationCRUDPayload, self).__init__(node_uuid=node_uuid)
self.populate_schema(allocation=allocation)

View File

@ -67,7 +67,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
# Version 1.28: Add automated_clean field
# Version 1.29: Add protected and protected_reason fields
# Version 1.30: Add owner field
VERSION = '1.30'
# Version 1.31: Add allocation_id field
VERSION = '1.31'
dbapi = db_api.get_instance()
@ -136,6 +137,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
'automated_clean': objects.fields.BooleanField(nullable=True),
'protected': objects.fields.BooleanField(),
'protected_reason': object_fields.StringField(nullable=True),
'allocation_id': object_fields.IntegerField(nullable=True),
'bios_interface': object_fields.StringField(nullable=True),
'boot_interface': object_fields.StringField(nullable=True),
@ -585,6 +587,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
should be set to False (or removed).
Version 1.30: owner was added. For versions prior to this, it should be
set to None or removed.
Version 1.31: allocation_id was added. For versions prior to this, it
should be set to None (or removed).
:param target_version: the desired version of the object
:param remove_unavailable_fields: True to remove fields that are
@ -597,7 +601,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
# Convert the different fields depending on version
fields = [('rescue_interface', 22), ('traits', 23),
('bios_interface', 24), ('automated_clean', 28),
('protected_reason', 29), ('owner', 30)]
('protected_reason', 29), ('owner', 30),
('allocation_id', 31)]
for name, minor in fields:
self._adjust_field_to_version(name, None, target_version,
1, minor, remove_unavailable_fields)

View File

@ -100,6 +100,7 @@ def node_post_data(**kw):
node.pop('chassis_id')
node.pop('tags')
node.pop('traits')
node.pop('allocation_id')
# NOTE(jroll): pop out fields that were introduced in later API versions,
# unless explicitly requested. Otherwise, these will cause tests using

View File

@ -791,6 +791,57 @@ class MigrationCheckersMixin(object):
col_names = [column.name for column in nodes.c]
self.assertIn('owner', col_names)
def _pre_upgrade_dd67b91a1981(self, engine):
data = {
'node_uuid': uuidutils.generate_uuid(),
}
nodes = db_utils.get_table(engine, 'nodes')
nodes.insert().execute({'uuid': data['node_uuid']})
return data
def _check_dd67b91a1981(self, engine, data):
nodes = db_utils.get_table(engine, 'nodes')
col_names = [column.name for column in nodes.c]
self.assertIn('allocation_id', col_names)
node = nodes.select(
nodes.c.uuid == data['node_uuid']).execute().first()
self.assertIsNone(node['allocation_id'])
allocations = db_utils.get_table(engine, 'allocations')
col_names = [column.name for column in allocations.c]
expected_names = ['id', 'uuid', 'node_id', 'created_at', 'updated_at',
'name', 'version', 'state', 'last_error',
'resource_class', 'traits', 'candidate_nodes',
'extra', 'conductor_affinity']
self.assertEqual(sorted(expected_names), sorted(col_names))
self.assertIsInstance(allocations.c.created_at.type,
sqlalchemy.types.DateTime)
self.assertIsInstance(allocations.c.updated_at.type,
sqlalchemy.types.DateTime)
self.assertIsInstance(allocations.c.id.type,
sqlalchemy.types.Integer)
self.assertIsInstance(allocations.c.uuid.type,
sqlalchemy.types.String)
self.assertIsInstance(allocations.c.node_id.type,
sqlalchemy.types.Integer)
self.assertIsInstance(allocations.c.state.type,
sqlalchemy.types.String)
self.assertIsInstance(allocations.c.last_error.type,
sqlalchemy.types.TEXT)
self.assertIsInstance(allocations.c.resource_class.type,
sqlalchemy.types.String)
self.assertIsInstance(allocations.c.traits.type,
sqlalchemy.types.TEXT)
self.assertIsInstance(allocations.c.candidate_nodes.type,
sqlalchemy.types.TEXT)
self.assertIsInstance(allocations.c.extra.type,
sqlalchemy.types.TEXT)
self.assertIsInstance(allocations.c.conductor_affinity.type,
sqlalchemy.types.Integer)
def test_upgrade_and_version(self):
with patch_with_engine(self.engine):
self.migration_api.upgrade('head')

View File

@ -0,0 +1,230 @@
# 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.
"""Tests for manipulating allocations via the DB API"""
from oslo_utils import uuidutils
from ironic.common import exception
from ironic.tests.unit.db import base
from ironic.tests.unit.db import utils as db_utils
class AllocationsTestCase(base.DbTestCase):
def setUp(self):
super(AllocationsTestCase, self).setUp()
self.node = db_utils.create_test_node()
self.allocation = db_utils.create_test_allocation(name='host1')
def _create_test_allocation_range(self, count, **kw):
"""Create the specified number of test allocation entries in DB
It uses create_test_allocation method. And returns List of Allocation
DB objects.
:param count: Specifies the number of allocations to be created
:returns: List of Allocation DB objects
"""
return [db_utils.create_test_allocation(uuid=uuidutils.generate_uuid(),
name='allocation' + str(i),
**kw).uuid
for i in range(count)]
def test_get_allocation_by_id(self):
res = self.dbapi.get_allocation_by_id(self.allocation.id)
self.assertEqual(self.allocation.uuid, res.uuid)
def test_get_allocation_by_id_that_does_not_exist(self):
self.assertRaises(exception.AllocationNotFound,
self.dbapi.get_allocation_by_id, 99)
def test_get_allocation_by_uuid(self):
res = self.dbapi.get_allocation_by_uuid(self.allocation.uuid)
self.assertEqual(self.allocation.id, res.id)
def test_get_allocation_by_uuid_that_does_not_exist(self):
self.assertRaises(exception.AllocationNotFound,
self.dbapi.get_allocation_by_uuid,
'EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE')
def test_get_allocation_by_name(self):
res = self.dbapi.get_allocation_by_name(self.allocation.name)
self.assertEqual(self.allocation.id, res.id)
def test_get_allocation_by_name_that_does_not_exist(self):
self.assertRaises(exception.AllocationNotFound,
self.dbapi.get_allocation_by_name, 'testfail')
def test_get_allocation_list(self):
uuids = self._create_test_allocation_range(6)
# Also add the uuid for the allocation created in setUp()
uuids.append(self.allocation.uuid)
res = self.dbapi.get_allocation_list()
self.assertEqual(set(uuids), {r.uuid for r in res})
def test_get_allocation_list_sorted(self):
uuids = self._create_test_allocation_range(6)
# Also add the uuid for the allocation created in setUp()
uuids.append(self.allocation.uuid)
res = self.dbapi.get_allocation_list(sort_key='uuid')
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), res_uuids)
def test_get_allocation_list_filter_by_state(self):
self._create_test_allocation_range(6, state='error')
res = self.dbapi.get_allocation_list(filters={'state': 'allocating'})
self.assertEqual([self.allocation.uuid], [r.uuid for r in res])
res = self.dbapi.get_allocation_list(filters={'state': 'error'})
self.assertEqual(6, len(res))
def test_get_allocation_list_filter_by_node(self):
self._create_test_allocation_range(6)
self.dbapi.update_allocation(self.allocation.id,
{'node_id': self.node.id})
res = self.dbapi.get_allocation_list(
filters={'node_uuid': self.node.uuid})
self.assertEqual([self.allocation.uuid], [r.uuid for r in res])
def test_get_allocation_list_filter_by_rsc(self):
self._create_test_allocation_range(6)
self.dbapi.update_allocation(self.allocation.id,
{'resource_class': 'very-large'})
res = self.dbapi.get_allocation_list(
filters={'resource_class': 'very-large'})
self.assertEqual([self.allocation.uuid], [r.uuid for r in res])
def test_get_allocation_list_invalid_fields(self):
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.get_allocation_list, sort_key='foo')
self.assertRaises(ValueError,
self.dbapi.get_allocation_list,
filters={'foo': 42})
def test_destroy_allocation(self):
self.dbapi.destroy_allocation(self.allocation.id)
self.assertRaises(exception.AllocationNotFound,
self.dbapi.get_allocation_by_id, self.allocation.id)
def test_destroy_allocation_with_node(self):
self.dbapi.update_node(self.node.id,
{'allocation_id': self.allocation.id,
'instance_uuid': uuidutils.generate_uuid()})
self.dbapi.destroy_allocation(self.allocation.id)
self.assertRaises(exception.AllocationNotFound,
self.dbapi.get_allocation_by_id, self.allocation.id)
node = self.dbapi.get_node_by_id(self.node.id)
self.assertIsNone(node.allocation_id)
self.assertIsNone(node.instance_uuid)
def test_destroy_allocation_that_does_not_exist(self):
self.assertRaises(exception.AllocationNotFound,
self.dbapi.destroy_allocation, 99)
def test_destroy_allocation_uuid(self):
self.dbapi.destroy_allocation(self.allocation.uuid)
def test_update_allocation(self):
old_name = self.allocation.name
new_name = 'newname'
self.assertNotEqual(old_name, new_name)
res = self.dbapi.update_allocation(self.allocation.id,
{'name': new_name})
self.assertEqual(new_name, res.name)
def test_update_allocation_uuid(self):
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.update_allocation, self.allocation.id,
{'uuid': ''})
def test_update_allocation_not_found(self):
id_2 = 99
self.assertNotEqual(self.allocation.id, id_2)
self.assertRaises(exception.AllocationNotFound,
self.dbapi.update_allocation, id_2,
{'name': 'newname'})
def test_update_allocation_duplicated_name(self):
name1 = self.allocation.name
allocation2 = db_utils.create_test_allocation(
uuid=uuidutils.generate_uuid(), name='name2')
self.assertRaises(exception.AllocationDuplicateName,
self.dbapi.update_allocation, allocation2.id,
{'name': name1})
def test_update_allocation_with_node_id(self):
res = self.dbapi.update_allocation(self.allocation.id,
{'name': 'newname',
'traits': ['foo'],
'node_id': self.node.id})
self.assertEqual('newname', res.name)
self.assertEqual(['foo'], res.traits)
self.assertEqual(self.node.id, res.node_id)
node = self.dbapi.get_node_by_id(self.node.id)
self.assertEqual(res.id, node.allocation_id)
self.assertEqual(res.uuid, node.instance_uuid)
self.assertEqual(['foo'], node.instance_info['traits'])
def test_update_allocation_node_already_associated(self):
existing_uuid = uuidutils.generate_uuid()
self.dbapi.update_node(self.node.id, {'instance_uuid': existing_uuid})
self.assertRaises(exception.NodeAssociated,
self.dbapi.update_allocation, self.allocation.id,
{'node_id': self.node.id, 'traits': ['foo']})
# Make sure we do not see partial updates
allocation = self.dbapi.get_allocation_by_id(self.allocation.id)
self.assertEqual([], allocation.traits)
self.assertIsNone(allocation.node_id)
node = self.dbapi.get_node_by_id(self.node.id)
self.assertIsNone(node.allocation_id)
self.assertEqual(existing_uuid, node.instance_uuid)
self.assertNotIn('traits', node.instance_info)
def test_update_allocation_associated_with_another_node(self):
db_utils.create_test_node(uuid=uuidutils.generate_uuid(),
allocation_id=self.allocation.id,
instance_uuid=self.allocation.uuid)
self.assertRaises(exception.InstanceAssociated,
self.dbapi.update_allocation, self.allocation.id,
{'node_id': self.node.id, 'traits': ['foo']})
# Make sure we do not see partial updates
allocation = self.dbapi.get_allocation_by_id(self.allocation.id)
self.assertEqual([], allocation.traits)
self.assertIsNone(allocation.node_id)
node = self.dbapi.get_node_by_id(self.node.id)
self.assertIsNone(node.allocation_id)
self.assertIsNone(node.instance_uuid)
self.assertNotIn('traits', node.instance_info)
def test_create_allocation_duplicated_name(self):
self.assertRaises(exception.AllocationDuplicateName,
db_utils.create_test_allocation,
uuid=uuidutils.generate_uuid(),
name=self.allocation.name)
def test_create_allocation_duplicated_uuid(self):
self.assertRaises(exception.AllocationAlreadyExists,
db_utils.create_test_allocation,
uuid=self.allocation.uuid)

View File

@ -302,7 +302,8 @@ class DbNodeTestCase(base.DbTestCase):
maintenance=True,
fault='boom',
resource_class='foo',
conductor_group='group1')
conductor_group='group1',
power_state='power on')
res = self.dbapi.get_node_list(filters={'chassis_uuid': ch1['uuid']})
self.assertEqual([node1.id], [r.id for r in res])
@ -355,6 +356,18 @@ class DbNodeTestCase(base.DbTestCase):
res = self.dbapi.get_node_list(filters={'uuid': node1.uuid})
self.assertEqual([node1.id], [r.id for r in res])
uuids = [uuidutils.generate_uuid(),
node1.uuid,
uuidutils.generate_uuid()]
res = self.dbapi.get_node_list(filters={'uuid_in': uuids})
self.assertEqual([node1.id], [r.id for r in res])
res = self.dbapi.get_node_list(filters={'with_power_state': True})
self.assertEqual([node2.id], [r.id for r in res])
res = self.dbapi.get_node_list(filters={'with_power_state': False})
self.assertEqual([node1.id], [r.id for r in res])
# ensure unknown filters explode
filters = {'bad_filter': 'foo'}
self.assertRaisesRegex(ValueError,
@ -519,6 +532,15 @@ class DbNodeTestCase(base.DbTestCase):
self.assertRaises(exception.NodeNotFound,
self.dbapi.node_trait_exists, node.id, trait.trait)
def test_allocations_get_destroyed_after_destroying_a_node_by_uuid(self):
node = utils.create_test_node()
allocation = utils.create_test_allocation(node_id=node.id)
self.dbapi.destroy_node(node.uuid)
self.assertRaises(exception.AllocationNotFound,
self.dbapi.get_allocation_by_id, allocation.id)
def test_update_node(self):
node = utils.create_test_node()

View File

@ -16,10 +16,12 @@
from oslo_utils import timeutils
from oslo_utils import uuidutils
from ironic.common import states
from ironic.db import api as db_api
from ironic.drivers import base as drivers_base
from ironic.objects import allocation
from ironic.objects import bios
from ironic.objects import chassis
from ironic.objects import conductor
@ -219,6 +221,7 @@ def get_test_node(**kw):
'protected_reason': kw.get('protected_reason', None),
'conductor': kw.get('conductor'),
'owner': kw.get('owner', None),
'allocation_id': kw.get('allocation_id'),
}
for iface in drivers_base.ALL_INTERFACES:
@ -588,3 +591,30 @@ def get_test_bios_setting_setting_list():
{'name': 'hyperthread', 'value': 'enabled'},
{'name': 'numlock', 'value': 'off'}
]
def get_test_allocation(**kw):
return {
'candidate_nodes': kw.get('candidate_nodes', []),
'conductor_affinity': kw.get('conductor_affinity'),
'created_at': kw.get('created_at'),
'extra': kw.get('extra', {}),
'id': kw.get('id', 42),
'last_error': kw.get('last_error'),
'name': kw.get('name'),
'node_id': kw.get('node_id'),
'resource_class': kw.get('resource_class', 'baremetal'),
'state': kw.get('state', 'allocating'),
'traits': kw.get('traits', []),
'updated_at': kw.get('updated_at'),
'uuid': kw.get('uuid', uuidutils.generate_uuid()),
'version': kw.get('version', allocation.Allocation.VERSION),
}
def create_test_allocation(**kw):
allocation = get_test_allocation(**kw)
if 'id' not in kw:
del allocation['id']
dbapi = db_api.get_instance()
return dbapi.create_allocation(allocation)

View File

@ -0,0 +1,144 @@
# 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.
import datetime
import mock
from testtools import matchers
from ironic.common import exception
from ironic import objects
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.objects import utils as obj_utils
class TestAllocationObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
def setUp(self):
super(TestAllocationObject, self).setUp()
self.fake_allocation = db_utils.get_test_allocation(name='host1')
def test_get_by_id(self):
allocation_id = self.fake_allocation['id']
with mock.patch.object(self.dbapi, 'get_allocation_by_id',
autospec=True) as mock_get_allocation:
mock_get_allocation.return_value = self.fake_allocation
allocation = objects.Allocation.get(self.context, allocation_id)
mock_get_allocation.assert_called_once_with(allocation_id)
self.assertEqual(self.context, allocation._context)
def test_get_by_uuid(self):
uuid = self.fake_allocation['uuid']
with mock.patch.object(self.dbapi, 'get_allocation_by_uuid',
autospec=True) as mock_get_allocation:
mock_get_allocation.return_value = self.fake_allocation
allocation = objects.Allocation.get(self.context, uuid)
mock_get_allocation.assert_called_once_with(uuid)
self.assertEqual(self.context, allocation._context)
def test_get_by_name(self):
name = self.fake_allocation['name']
with mock.patch.object(self.dbapi, 'get_allocation_by_name',
autospec=True) as mock_get_allocation:
mock_get_allocation.return_value = self.fake_allocation
allocation = objects.Allocation.get(self.context, name)
mock_get_allocation.assert_called_once_with(name)
self.assertEqual(self.context, allocation._context)
def test_get_bad_id_and_uuid_and_name(self):
self.assertRaises(exception.InvalidIdentity,
objects.Allocation.get,
self.context,
'not:a_name_or_uuid')
def test_create(self):
allocation = objects.Allocation(self.context, **self.fake_allocation)
with mock.patch.object(self.dbapi, 'create_allocation',
autospec=True) as mock_create_allocation:
mock_create_allocation.return_value = (
db_utils.get_test_allocation())
allocation.create()
args, _kwargs = mock_create_allocation.call_args
self.assertEqual(objects.Allocation.VERSION, args[0]['version'])
def test_save(self):
uuid = self.fake_allocation['uuid']
test_time = datetime.datetime(2000, 1, 1, 0, 0)
with mock.patch.object(self.dbapi, 'get_allocation_by_uuid',
autospec=True) as mock_get_allocation:
mock_get_allocation.return_value = self.fake_allocation
with mock.patch.object(self.dbapi, 'update_allocation',
autospec=True) as mock_update_allocation:
mock_update_allocation.return_value = (
db_utils.get_test_allocation(name='newname',
updated_at=test_time))
p = objects.Allocation.get_by_uuid(self.context, uuid)
p.name = 'newname'
p.save()
mock_get_allocation.assert_called_once_with(uuid)
mock_update_allocation.assert_called_once_with(
uuid, {'version': objects.Allocation.VERSION,
'name': 'newname'})
self.assertEqual(self.context, p._context)
res_updated_at = (p.updated_at).replace(tzinfo=None)
self.assertEqual(test_time, res_updated_at)
def test_refresh(self):
uuid = self.fake_allocation['uuid']
returns = [self.fake_allocation,
db_utils.get_test_allocation(name='newname')]
expected = [mock.call(uuid), mock.call(uuid)]
with mock.patch.object(self.dbapi, 'get_allocation_by_uuid',
side_effect=returns,
autospec=True) as mock_get_allocation:
p = objects.Allocation.get_by_uuid(self.context, uuid)
self.assertEqual(self.fake_allocation['name'], p.name)
p.refresh()
self.assertEqual('newname', p.name)
self.assertEqual(expected, mock_get_allocation.call_args_list)
self.assertEqual(self.context, p._context)
def test_save_after_refresh(self):
# Ensure that it's possible to do object.save() after object.refresh()
db_allocation = db_utils.create_test_allocation()
p = objects.Allocation.get_by_uuid(self.context, db_allocation.uuid)
p_copy = objects.Allocation.get_by_uuid(self.context,
db_allocation.uuid)
p.name = 'newname'
p.save()
p_copy.refresh()
p.copy = 'newname2'
# Ensure this passes and an exception is not generated
p_copy.save()
def test_list(self):
with mock.patch.object(self.dbapi, 'get_allocation_list',
autospec=True) as mock_get_list:
mock_get_list.return_value = [self.fake_allocation]
allocations = objects.Allocation.list(self.context)
self.assertThat(allocations, matchers.HasLength(1))
self.assertIsInstance(allocations[0], objects.Allocation)
self.assertEqual(self.context, allocations[0]._context)
def test_payload_schemas(self):
self._check_payload_schemas(objects.allocation,
objects.Allocation.fields)

View File

@ -888,6 +888,67 @@ class TestConvertToVersion(db_base.DbTestCase):
self.assertIsNone(node.owner)
self.assertEqual({}, node.obj_get_changes())
def test_allocation_id_supported_missing(self):
# allocation_id_interface not set, should be set to default.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
delattr(node, 'allocation_id')
node.obj_reset_changes()
node._convert_to_version("1.31")
self.assertIsNone(node.allocation_id)
self.assertEqual({'allocation_id': None},
node.obj_get_changes())
def test_allocation_id_supported_set(self):
# allocation_id set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.allocation_id = 42
node.obj_reset_changes()
node._convert_to_version("1.31")
self.assertEqual(42, node.allocation_id)
self.assertEqual({}, node.obj_get_changes())
def test_allocation_id_unsupported_missing(self):
# allocation_id not set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
delattr(node, 'allocation_id')
node.obj_reset_changes()
node._convert_to_version("1.30")
self.assertNotIn('allocation_id', node)
self.assertEqual({}, node.obj_get_changes())
def test_allocation_id_unsupported_set_remove(self):
# allocation_id set, should be removed.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.allocation_id = 42
node.obj_reset_changes()
node._convert_to_version("1.30")
self.assertNotIn('allocation_id', node)
self.assertEqual({}, node.obj_get_changes())
def test_allocation_id_unsupported_set_no_remove_non_default(self):
# allocation_id set, should be set to default.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.allocation_id = 42
node.obj_reset_changes()
node._convert_to_version("1.30", False)
self.assertIsNone(node.allocation_id)
self.assertEqual({'allocation_id': None},
node.obj_get_changes())
def test_allocation_id_unsupported_set_no_remove_default(self):
# allocation_id set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.allocation_id = None
node.obj_reset_changes()
node._convert_to_version("1.30", False)
self.assertIsNone(node.allocation_id)
self.assertEqual({}, node.obj_get_changes())
class TestNodePayloads(db_base.DbTestCase):

View File

@ -677,7 +677,7 @@ class TestObject(_LocalTest, _TestObject):
# version bump. It is an MD5 hash of the object fields and remotable methods.
# The fingerprint values should only be changed if there is a version bump.
expected_object_fingerprints = {
'Node': '1.30-8313460d6ea5457a527cd3d85e5ee3d8',
'Node': '1.31-1b77c11e94f971a71c76f5f44fb5b3f4',
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
'Port': '1.8-898a47921f4a1f53fcdddd4eeb179e0b',
@ -714,6 +714,9 @@ expected_object_fingerprints = {
'TraitList': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
'BIOSSetting': '1.0-fd4a791dc2139a7cc21cefbbaedfd9e7',
'BIOSSettingList': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
'Allocation': '1.0-25ebf609743cd3f332a4f80fcb818102',
'AllocationCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'AllocationCRUDPayload': '1.0-a82389d019f37cfe54b50049f73911b3',
}