summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ironic/cmd/dbsync.py2
-rw-r--r--ironic/common/exception.py12
-rw-r--r--ironic/common/release_mappings.py3
-rw-r--r--ironic/db/api.py79
-rw-r--r--ironic/db/sqlalchemy/alembic/versions/dd67b91a1981_add_allocations_table.py56
-rw-r--r--ironic/db/sqlalchemy/api.py207
-rw-r--r--ironic/db/sqlalchemy/models.py26
-rw-r--r--ironic/objects/__init__.py1
-rw-r--r--ironic/objects/allocation.py300
-rw-r--r--ironic/objects/node.py9
-rw-r--r--ironic/tests/unit/api/utils.py1
-rw-r--r--ironic/tests/unit/db/sqlalchemy/test_migrations.py51
-rw-r--r--ironic/tests/unit/db/test_allocations.py230
-rw-r--r--ironic/tests/unit/db/test_nodes.py24
-rw-r--r--ironic/tests/unit/db/utils.py30
-rw-r--r--ironic/tests/unit/objects/test_allocation.py144
-rw-r--r--ironic/tests/unit/objects/test_node.py61
-rw-r--r--ironic/tests/unit/objects/test_objects.py5
18 files changed, 1235 insertions, 6 deletions
diff --git a/ironic/cmd/dbsync.py b/ironic/cmd/dbsync.py
index 52012a2c3..f554cbdfc 100644
--- a/ironic/cmd/dbsync.py
+++ b/ironic/cmd/dbsync.py
@@ -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',
]
diff --git a/ironic/common/exception.py b/ironic/common/exception.py
index f38caf04c..cba02d5a0 100644
--- a/ironic/common/exception.py
+++ b/ironic/common/exception.py
@@ -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.")
diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py
index 883a4910b..0486993ac 100644
--- a/ironic/common/release_mappings.py
+++ b/ironic/common/release_mappings.py
@@ -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'],
diff --git a/ironic/db/api.py b/ironic/db/api.py
index c98880c7f..e43dc55da 100644
--- a/ironic/db/api.py
+++ b/ironic/db/api.py
@@ -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
+ """
diff --git a/ironic/db/sqlalchemy/alembic/versions/dd67b91a1981_add_allocations_table.py b/ironic/db/sqlalchemy/alembic/versions/dd67b91a1981_add_allocations_table.py
new file mode 100644
index 000000000..55560dc68
--- /dev/null
+++ b/ironic/db/sqlalchemy/alembic/versions/dd67b91a1981_add_allocations_table.py
@@ -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'])
diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py
index 2c096f002..beaca9339 100644
--- a/ironic/db/sqlalchemy/api.py
+++ b/ironic/db/sqlalchemy/api.py
@@ -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()
diff --git a/ironic/db/sqlalchemy/models.py b/ironic/db/sqlalchemy/models.py
index 2a17dfa65..db76a9dbd 100644
--- a/ironic/db/sqlalchemy/models.py
+++ b/ironic/db/sqlalchemy/models.py
@@ -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.
diff --git a/ironic/objects/__init__.py b/ironic/objects/__init__.py
index 63c4d2b13..96ddc1c28 100644
--- a/ironic/objects/__init__.py
+++ b/ironic/objects/__init__.py
@@ -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')
diff --git a/ironic/objects/allocation.py b/ironic/objects/allocation.py
new file mode 100644
index 000000000..1bfe44a0a
--- /dev/null
+++ b/ironic/objects/allocation.py
@@ -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)
diff --git a/ironic/objects/node.py b/ironic/objects/node.py
index 73b918745..3fe530525 100644
--- a/ironic/objects/node.py
+++ b/ironic/objects/node.py
@@ -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)
diff --git a/ironic/tests/unit/api/utils.py b/ironic/tests/unit/api/utils.py
index 0635d0294..f2c11bda1 100644
--- a/ironic/tests/unit/api/utils.py
+++ b/ironic/tests/unit/api/utils.py
@@ -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
diff --git a/ironic/tests/unit/db/sqlalchemy/test_migrations.py b/ironic/tests/unit/db/sqlalchemy/test_migrations.py
index 10b409f20..9593f2605 100644
--- a/ironic/tests/unit/db/sqlalchemy/test_migrations.py
+++ b/ironic/tests/unit/db/sqlalchemy/test_migrations.py
@@ -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')
diff --git a/ironic/tests/unit/db/test_allocations.py b/ironic/tests/unit/db/test_allocations.py
new file mode 100644
index 000000000..451b087fa
--- /dev/null
+++ b/ironic/tests/unit/db/test_allocations.py
@@ -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)
diff --git a/ironic/tests/unit/db/test_nodes.py b/ironic/tests/unit/db/test_nodes.py
index 45b770370..f92baa7b1 100644
--- a/ironic/tests/unit/db/test_nodes.py
+++ b/ironic/tests/unit/db/test_nodes.py
@@ -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()
diff --git a/ironic/tests/unit/db/utils.py b/ironic/tests/unit/db/utils.py
index a0bc1fb38..f3f838000 100644
--- a/ironic/tests/unit/db/utils.py
+++ b/ironic/tests/unit/db/utils.py
@@ -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)
diff --git a/ironic/tests/unit/objects/test_allocation.py b/ironic/tests/unit/objects/test_allocation.py
new file mode 100644
index 000000000..baa7d8d71
--- /dev/null
+++ b/ironic/tests/unit/objects/test_allocation.py
@@ -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)
diff --git a/ironic/tests/unit/objects/test_node.py b/ironic/tests/unit/objects/test_node.py
index 536e86ac0..6d25cad24 100644
--- a/ironic/tests/unit/objects/test_node.py
+++ b/ironic/tests/unit/objects/test_node.py
@@ -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):
diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py
index 4f409053c..bd8808506 100644
--- a/ironic/tests/unit/objects/test_objects.py
+++ b/ironic/tests/unit/objects/test_objects.py
@@ -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',
}