Merge "Deploy templates: data model, DB API & objects"

This commit is contained in:
Zuul 2019-02-19 00:48:59 +00:00 committed by Gerrit Code Review
commit 488df355ab
17 changed files with 1189 additions and 1 deletions

View File

@ -83,6 +83,9 @@ ONLINE_MIGRATIONS = (
NEW_MODELS = [ NEW_MODELS = [
# TODO(dtantsur): remove in Train # TODO(dtantsur): remove in Train
'Allocation', 'Allocation',
# TODO(mgoddard): remove in Train
'DeployTemplate',
'DeployTemplateStep',
] ]

View File

@ -807,3 +807,15 @@ class AllocationAlreadyExists(Conflict):
class AllocationFailed(IronicException): class AllocationFailed(IronicException):
_msg_fmt = _("Failed to process allocation %(uuid)s: %(error)s.") _msg_fmt = _("Failed to process allocation %(uuid)s: %(error)s.")
class DeployTemplateDuplicateName(Conflict):
_msg_fmt = _("A deploy template with name %(name)s already exists.")
class DeployTemplateAlreadyExists(Conflict):
_msg_fmt = _("A deploy template with UUID %(uuid)s already exists.")
class DeployTemplateNotFound(NotFound):
_msg_fmt = _("Deploy template %(template)s could not be found.")

View File

@ -138,6 +138,7 @@ RELEASE_MAPPING = {
'Node': ['1.32', '1.31', '1.30', '1.29', '1.28'], 'Node': ['1.32', '1.31', '1.30', '1.29', '1.28'],
'Conductor': ['1.3'], 'Conductor': ['1.3'],
'Chassis': ['1.3'], 'Chassis': ['1.3'],
'DeployTemplate': ['1.0'],
'Port': ['1.9'], 'Port': ['1.9'],
'Portgroup': ['1.4'], 'Portgroup': ['1.4'],
'Trait': ['1.0'], 'Trait': ['1.0'],

View File

@ -1165,3 +1165,99 @@ class Connection(object):
:param allocation_id: Allocation ID :param allocation_id: Allocation ID
:raises: AllocationNotFound :raises: AllocationNotFound
""" """
@abc.abstractmethod
def create_deploy_template(self, values, version):
"""Create a deployment template.
:param values: A dict describing the deployment template. For example:
::
{
'uuid': uuidutils.generate_uuid(),
'name': 'CUSTOM_DT1',
}
:param version: the version of the object.DeployTemplate.
:raises: DeployTemplateDuplicateName if a deploy template with the same
name exists.
:raises: DeployTemplateAlreadyExists if a deploy template with the same
UUID exists.
:returns: A deploy template.
"""
@abc.abstractmethod
def update_deploy_template(self, template_id, values):
"""Update a deployment template.
:param template_id: ID of the deployment template to update.
:param values: A dict describing the deployment template. For example:
::
{
'uuid': uuidutils.generate_uuid(),
'name': 'CUSTOM_DT1',
}
:raises: DeployTemplateDuplicateName if a deploy template with the same
name exists.
:raises: DeployTemplateNotFound if the deploy template does not exist.
:returns: A deploy template.
"""
@abc.abstractmethod
def destroy_deploy_template(self, template_id):
"""Destroy a deployment template.
:param template_id: ID of the deployment template to destroy.
:raises: DeployTemplateNotFound if the deploy template does not exist.
"""
@abc.abstractmethod
def get_deploy_template_by_id(self, template_id):
"""Retrieve a deployment template by ID.
:param template_id: ID of the deployment template to retrieve.
:raises: DeployTemplateNotFound if the deploy template does not exist.
:returns: A deploy template.
"""
@abc.abstractmethod
def get_deploy_template_by_uuid(self, template_uuid):
"""Retrieve a deployment template by UUID.
:param template_uuid: UUID of the deployment template to retrieve.
:raises: DeployTemplateNotFound if the deploy template does not exist.
:returns: A deploy template.
"""
@abc.abstractmethod
def get_deploy_template_by_name(self, template_name):
"""Retrieve a deployment template by name.
:param template_name: name of the deployment template to retrieve.
:raises: DeployTemplateNotFound if the deploy template does not exist.
:returns: A deploy template.
"""
@abc.abstractmethod
def get_deploy_template_list(self, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Retrieve a list of deployment templates.
:param limit: Maximum number of deploy templates 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 deploy templates.
"""
@abc.abstractmethod
def get_deploy_template_list_by_names(self, names):
"""Return a list of deployment templates with one of a list of names.
:param names: List of names to filter by.
:returns: A list of deploy templates.
"""

View File

@ -0,0 +1,67 @@
# 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.
"""Create deploy_templates and deploy_template_steps tables.
Revision ID: 2aac7e0872f6
Revises: 28c44432c9c3
Create Date: 2018-12-27 11:49:15.029650
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2aac7e0872f6'
down_revision = '28c44432c9c3'
def upgrade():
op.create_table(
'deploy_templates',
sa.Column('version', sa.String(length=15), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False,
autoincrement=True),
sa.Column('uuid', sa.String(length=36)),
sa.Column('name', sa.String(length=255), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_deploytemplates0uuid'),
sa.UniqueConstraint('name', name='uniq_deploytemplates0name'),
mysql_ENGINE='InnoDB',
mysql_DEFAULT_CHARSET='UTF8'
)
op.create_table(
'deploy_template_steps',
sa.Column('version', sa.String(length=15), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False,
autoincrement=True),
sa.Column('deploy_template_id', sa.Integer(), nullable=False,
autoincrement=False),
sa.Column('interface', sa.String(length=255), nullable=False),
sa.Column('step', sa.String(length=255), nullable=False),
sa.Column('args', sa.Text, nullable=False),
sa.Column('priority', sa.Integer, nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.ForeignKeyConstraint(['deploy_template_id'],
['deploy_templates.id']),
sa.Index('deploy_template_id', 'deploy_template_id'),
sa.Index('deploy_template_steps_interface_idx', 'interface'),
sa.Index('deploy_template_steps_step_idx', 'step'),
mysql_ENGINE='InnoDB',
mysql_DEFAULT_CHARSET='UTF8'
)

View File

@ -85,6 +85,14 @@ def _get_node_query_with_all():
.options(joinedload('traits'))) .options(joinedload('traits')))
def _get_deploy_template_query_with_steps():
"""Return a query object for the DeployTemplate joined with steps.
:returns: a query object.
"""
return model_query(models.DeployTemplate).options(joinedload('steps'))
def model_query(model, *args, **kwargs): def model_query(model, *args, **kwargs):
"""Query helper for simpler session usage. """Query helper for simpler session usage.
@ -218,6 +226,42 @@ def _filter_active_conductors(query, interval=None):
return query return query
def _zip_matching(a, b, key):
"""Zip two unsorted lists, yielding matching items or None.
Each zipped item is a tuple taking one of three forms:
(a[i], b[j]) if a[i] and b[j] are equal.
(a[i], None) if a[i] is less than b[j] or b is empty.
(None, b[j]) if a[i] is greater than b[j] or a is empty.
Note that the returned list may be longer than either of the two
lists.
Adapted from https://stackoverflow.com/a/11426702.
:param a: the first list.
:param b: the second list.
:param key: a function that generates a key used to compare items.
"""
a = collections.deque(sorted(a, key=key))
b = collections.deque(sorted(b, key=key))
while a and b:
k_a = key(a[0])
k_b = key(b[0])
if k_a == k_b:
yield a.popleft(), b.popleft()
elif k_a < k_b:
yield a.popleft(), None
else:
yield None, b.popleft()
# Consume any remaining items in each deque.
for i in a:
yield i, None
for i in b:
yield None, i
@profiler.trace_cls("db_api") @profiler.trace_cls("db_api")
class Connection(api.Connection): class Connection(api.Connection):
"""SqlAlchemy connection.""" """SqlAlchemy connection."""
@ -1710,3 +1754,155 @@ class Connection(api.Connection):
node_query.update({'allocation_id': None, 'instance_uuid': None}) node_query.update({'allocation_id': None, 'instance_uuid': None})
query.delete() query.delete()
@staticmethod
def _get_deploy_template_steps(steps, deploy_template_id=None):
results = []
for values in steps:
step = models.DeployTemplateStep()
step.update(values)
if deploy_template_id:
step['deploy_template_id'] = deploy_template_id
results.append(step)
return results
@oslo_db_api.retry_on_deadlock
def create_deploy_template(self, values, version):
steps = values.get('steps', [])
values['steps'] = self._get_deploy_template_steps(steps)
template = models.DeployTemplate()
template.update(values)
with _session_for_write() as session:
try:
session.add(template)
session.flush()
except db_exc.DBDuplicateEntry as e:
if 'name' in e.columns:
raise exception.DeployTemplateDuplicateName(
name=values['name'])
raise exception.DeployTemplateAlreadyExists(
uuid=values['uuid'])
return template
def _update_deploy_template_steps(self, session, template_id, steps):
"""Update the steps for a deploy template.
:param session: DB session object.
:param template_id: deploy template ID.
:param steps: list of steps that should exist for the deploy template.
"""
def _step_key(step):
"""Compare two deploy template steps."""
return step.interface, step.step, step.args, step.priority
# List all existing steps for the template.
query = (model_query(models.DeployTemplateStep)
.filter_by(deploy_template_id=template_id))
current_steps = query.all()
# List the new steps for the template.
new_steps = self._get_deploy_template_steps(steps, template_id)
# The following is an efficient way to ensure that the steps in the
# database match those that have been requested. We compare the current
# and requested steps in a single pass using the _zip_matching
# function.
steps_to_create = []
step_ids_to_delete = []
for current_step, new_step in _zip_matching(current_steps, new_steps,
_step_key):
if current_step is None:
# No matching current step found for this new step - create.
steps_to_create.append(new_step)
elif new_step is None:
# No matching new step found for this current step - delete.
step_ids_to_delete.append(current_step.id)
# else: steps match, no work required.
# Delete and create steps in bulk as necessary.
if step_ids_to_delete:
((model_query(models.DeployTemplateStep)
.filter(models.DeployTemplateStep.id.in_(step_ids_to_delete)))
.delete(synchronize_session=False))
if steps_to_create:
session.bulk_save_objects(steps_to_create)
@oslo_db_api.retry_on_deadlock
def update_deploy_template(self, template_id, values):
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing deploy template.")
raise exception.InvalidParameterValue(err=msg)
try:
with _session_for_write() as session:
# NOTE(mgoddard): Don't issue a joined query for the update as
# this does not work with PostgreSQL.
query = model_query(models.DeployTemplate)
query = add_identity_filter(query, template_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
raise exception.DeployTemplateNotFound(
template=template_id)
# First, update non-step columns.
steps = None
if 'steps' in values:
steps = values.pop('steps')
ref.update(values)
# If necessary, update steps.
if steps is not None:
self._update_deploy_template_steps(session, ref.id, steps)
# Return the updated template joined with all relevant fields.
query = _get_deploy_template_query_with_steps()
query = add_identity_filter(query, template_id)
return query.one()
except db_exc.DBDuplicateEntry as e:
if 'name' in e.columns:
raise exception.DeployTemplateDuplicateName(
name=values['name'])
raise
@oslo_db_api.retry_on_deadlock
def destroy_deploy_template(self, template_id):
with _session_for_write():
model_query(models.DeployTemplateStep).filter_by(
deploy_template_id=template_id).delete()
count = model_query(models.DeployTemplate).filter_by(
id=template_id).delete()
if count == 0:
raise exception.DeployTemplateNotFound(template=template_id)
def _get_deploy_template(self, field, value):
"""Helper method for retrieving a deploy template."""
query = (_get_deploy_template_query_with_steps()
.filter_by(**{field: value}))
try:
return query.one()
except NoResultFound:
raise exception.DeployTemplateNotFound(template=value)
def get_deploy_template_by_id(self, template_id):
return self._get_deploy_template('id', template_id)
def get_deploy_template_by_uuid(self, template_uuid):
return self._get_deploy_template('uuid', template_uuid)
def get_deploy_template_by_name(self, template_name):
return self._get_deploy_template('name', template_name)
def get_deploy_template_list(self, limit=None, marker=None,
sort_key=None, sort_dir=None):
query = _get_deploy_template_query_with_steps()
return _paginate_query(models.DeployTemplate, limit, marker,
sort_key, sort_dir, query)
def get_deploy_template_list_by_names(self, names):
query = (_get_deploy_template_query_with_steps()
.filter(models.DeployTemplate.name.in_(names)))
return query.all()

View File

@ -350,6 +350,45 @@ class Allocation(Base):
nullable=True) nullable=True)
class DeployTemplate(Base):
"""Represents a deployment template."""
__tablename__ = 'deploy_templates'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_deploytemplates0uuid'),
schema.UniqueConstraint('name', name='uniq_deploytemplates0name'),
table_args())
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
name = Column(String(255), nullable=False)
class DeployTemplateStep(Base):
"""Represents a deployment step in a deployment template."""
__tablename__ = 'deploy_template_steps'
__table_args__ = (
Index('deploy_template_id', 'deploy_template_id'),
Index('deploy_template_steps_interface_idx', 'interface'),
Index('deploy_template_steps_step_idx', 'step'),
table_args())
id = Column(Integer, primary_key=True)
deploy_template_id = Column(Integer, ForeignKey('deploy_templates.id'),
nullable=False)
interface = Column(String(255), nullable=False)
step = Column(String(255), nullable=False)
args = Column(db_types.JsonEncodedDict, nullable=False)
priority = Column(Integer, nullable=False)
deploy_template = orm.relationship(
"DeployTemplate",
backref='steps',
primaryjoin=(
'and_(DeployTemplateStep.deploy_template_id == '
'DeployTemplate.id)'),
foreign_keys=deploy_template_id
)
def get_class(model_name): def get_class(model_name):
"""Returns the model class with the specified name. """Returns the model class with the specified name.

View File

@ -28,6 +28,7 @@ def register_all():
__import__('ironic.objects.bios') __import__('ironic.objects.bios')
__import__('ironic.objects.chassis') __import__('ironic.objects.chassis')
__import__('ironic.objects.conductor') __import__('ironic.objects.conductor')
__import__('ironic.objects.deploy_template')
__import__('ironic.objects.node') __import__('ironic.objects.node')
__import__('ironic.objects.port') __import__('ironic.objects.port')
__import__('ironic.objects.portgroup') __import__('ironic.objects.portgroup')

View File

@ -0,0 +1,240 @@
# 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_versionedobjects import base as object_base
from ironic.db import api as db_api
from ironic.objects import base
from ironic.objects import fields as object_fields
@base.IronicObjectRegistry.register
class DeployTemplate(base.IronicObject, object_base.VersionedObjectDictCompat):
# Version 1.0: Initial version
VERSION = '1.0'
dbapi = db_api.get_instance()
fields = {
'id': object_fields.IntegerField(),
'uuid': object_fields.UUIDField(nullable=False),
'name': object_fields.StringField(nullable=False),
'steps': object_fields.ListOfFlexibleDictsField(nullable=False),
}
# NOTE(mgoddard): 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 DeployTemplate 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.: DeployTemplate(context).
:raises: DeployTemplateDuplicateName if a deploy template with the same
name exists.
:raises: DeployTemplateAlreadyExists if a deploy template with the same
UUID exists.
"""
values = self.do_version_changes_for_db()
db_template = self.dbapi.create_deploy_template(
values, values['version'])
self._from_db_object(self._context, self, db_template)
# NOTE(mgoddard): 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 DeployTemplate.
Column-wise updates will be made 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.: DeployTemplate(context)
:raises: DeployTemplateDuplicateName if a deploy template with the same
name exists.
:raises: DeployTemplateNotFound if the deploy template does not exist.
"""
updates = self.do_version_changes_for_db()
db_template = self.dbapi.update_deploy_template(self.uuid, updates)
self._from_db_object(self._context, self, db_template)
# NOTE(mgoddard): 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
def destroy(self):
"""Delete the DeployTemplate 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.: DeployTemplate(context).
:raises: DeployTemplateNotFound if the deploy template no longer
appears in the database.
"""
self.dbapi.destroy_deploy_template(self.id)
self.obj_reset_changes()
# NOTE(mgoddard): 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, template_id):
"""Find a deploy template based on its integer ID.
: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.: DeployTemplate(context).
:param template_id: The ID of a deploy template.
:raises: DeployTemplateNotFound if the deploy template no longer
appears in the database.
:returns: a :class:`DeployTemplate` object.
"""
db_template = cls.dbapi.get_deploy_template_by_id(template_id)
template = cls._from_db_object(context, cls(), db_template)
return template
# NOTE(mgoddard): 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 deploy template based on its UUID.
: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.: DeployTemplate(context).
:param uuid: The UUID of a deploy template.
:raises: DeployTemplateNotFound if the deploy template no longer
appears in the database.
:returns: a :class:`DeployTemplate` object.
"""
db_template = cls.dbapi.get_deploy_template_by_uuid(uuid)
template = cls._from_db_object(context, cls(), db_template)
return template
# NOTE(mgoddard): 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 a deploy template based on its name.
: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.: DeployTemplate(context).
:param name: The name of a deploy template.
:raises: DeployTemplateNotFound if the deploy template no longer
appears in the database.
:returns: a :class:`DeployTemplate` object.
"""
db_template = cls.dbapi.get_deploy_template_by_name(name)
template = cls._from_db_object(context, cls(), db_template)
return template
# NOTE(mgoddard): 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, limit=None, marker=None, sort_key=None,
sort_dir=None):
"""Return a list of DeployTemplate objects.
: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.: DeployTemplate(context).
: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:`DeployTemplate` objects.
"""
db_templates = cls.dbapi.get_deploy_template_list(
limit=limit, marker=marker, sort_key=sort_key, sort_dir=sort_dir)
return cls._from_db_object_list(context, db_templates)
# NOTE(mgoddard): 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_by_names(cls, context, names):
"""Return a list of DeployTemplate objects matching a set of names.
: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.: DeployTemplate(context).
:param names: a list of names to filter by.
:returns: a list of :class:`DeployTemplate` objects.
"""
db_templates = cls.dbapi.get_deploy_template_list_by_names(names)
return cls._from_db_object_list(context, db_templates)
def refresh(self, context=None):
"""Loads updates for this deploy template.
Loads a deploy template with the same uuid from the database and
checks for updated attributes. Updates are applied from
the loaded template 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.: Port(context)
:raises: DeployTemplateNotFound if the deploy template no longer
appears in the database.
"""
current = self.get_by_uuid(self._context, uuid=self.uuid)
self.obj_refresh(current)
self.obj_reset_changes()

View File

@ -106,6 +106,10 @@ class FlexibleDictField(object_fields.AutoTypedField):
super(FlexibleDictField, self)._null(obj, attr) super(FlexibleDictField, self)._null(obj, attr)
class ListOfFlexibleDictsField(object_fields.AutoTypedField):
AUTO_TYPE = object_fields.List(FlexibleDict())
class EnumField(object_fields.EnumField): class EnumField(object_fields.EnumField):
pass pass

View File

@ -85,7 +85,7 @@ class ReleaseMappingsTestCase(base.TestCase):
self.assertIn('master', release_mappings.RELEASE_MAPPING) self.assertIn('master', release_mappings.RELEASE_MAPPING)
model_names = set((s.__name__ for s in models.Base.__subclasses__())) model_names = set((s.__name__ for s in models.Base.__subclasses__()))
exceptions = set(['NodeTag', 'ConductorHardwareInterfaces', exceptions = set(['NodeTag', 'ConductorHardwareInterfaces',
'NodeTrait', 'BIOSSetting']) 'NodeTrait', 'BIOSSetting', 'DeployTemplateStep'])
# NOTE(xek): As a rule, all models which can be changed between # NOTE(xek): As a rule, all models which can be changed between
# releases or are sent through RPC should have their counterpart # releases or are sent through RPC should have their counterpart
# versioned objects. # versioned objects.

View File

@ -858,6 +858,102 @@ class MigrationCheckersMixin(object):
self.assertIsInstance(nodes_tbl.c.description.type, self.assertIsInstance(nodes_tbl.c.description.type,
sqlalchemy.types.TEXT) sqlalchemy.types.TEXT)
def _check_2aac7e0872f6(self, engine, data):
# Deploy templates.
deploy_templates = db_utils.get_table(engine, 'deploy_templates')
col_names = [column.name for column in deploy_templates.c]
expected = ['created_at', 'updated_at', 'version',
'id', 'uuid', 'name']
self.assertEqual(sorted(expected), sorted(col_names))
self.assertIsInstance(deploy_templates.c.created_at.type,
sqlalchemy.types.DateTime)
self.assertIsInstance(deploy_templates.c.updated_at.type,
sqlalchemy.types.DateTime)
self.assertIsInstance(deploy_templates.c.version.type,
sqlalchemy.types.String)
self.assertIsInstance(deploy_templates.c.id.type,
sqlalchemy.types.Integer)
self.assertIsInstance(deploy_templates.c.uuid.type,
sqlalchemy.types.String)
self.assertIsInstance(deploy_templates.c.name.type,
sqlalchemy.types.String)
# Insert a deploy template.
uuid = uuidutils.generate_uuid()
name = 'CUSTOM_DT1'
template = {'name': name, 'uuid': uuid}
deploy_templates.insert().execute(template)
# Query by UUID.
result = deploy_templates.select(
deploy_templates.c.uuid == uuid).execute().first()
template_id = result['id']
self.assertEqual(name, result['name'])
# Query by name.
result = deploy_templates.select(
deploy_templates.c.name == name).execute().first()
self.assertEqual(template_id, result['id'])
# Query by ID.
result = deploy_templates.select(
deploy_templates.c.id == template_id).execute().first()
self.assertEqual(uuid, result['uuid'])
self.assertEqual(name, result['name'])
# UUID is unique.
template = {'name': 'CUSTOM_DT2', 'uuid': uuid}
self.assertRaises(db_exc.DBDuplicateEntry,
deploy_templates.insert().execute, template)
# Name is unique.
template = {'name': name, 'uuid': uuidutils.generate_uuid()}
self.assertRaises(db_exc.DBDuplicateEntry,
deploy_templates.insert().execute, template)
# Deploy template steps.
deploy_template_steps = db_utils.get_table(engine,
'deploy_template_steps')
col_names = [column.name for column in deploy_template_steps.c]
expected = ['created_at', 'updated_at', 'version',
'id', 'deploy_template_id', 'interface', 'step', 'args',
'priority']
self.assertEqual(sorted(expected), sorted(col_names))
self.assertIsInstance(deploy_template_steps.c.created_at.type,
sqlalchemy.types.DateTime)
self.assertIsInstance(deploy_template_steps.c.updated_at.type,
sqlalchemy.types.DateTime)
self.assertIsInstance(deploy_template_steps.c.version.type,
sqlalchemy.types.String)
self.assertIsInstance(deploy_template_steps.c.id.type,
sqlalchemy.types.Integer)
self.assertIsInstance(deploy_template_steps.c.deploy_template_id.type,
sqlalchemy.types.Integer)
self.assertIsInstance(deploy_template_steps.c.interface.type,
sqlalchemy.types.String)
self.assertIsInstance(deploy_template_steps.c.step.type,
sqlalchemy.types.String)
self.assertIsInstance(deploy_template_steps.c.args.type,
sqlalchemy.types.Text)
self.assertIsInstance(deploy_template_steps.c.priority.type,
sqlalchemy.types.Integer)
# Insert a deploy template step.
interface = 'raid'
step_name = 'create_configuration'
args = '{"logical_disks": []}'
priority = 10
step = {'deploy_template_id': template_id, 'interface': interface,
'step': step_name, 'args': args, 'priority': priority}
deploy_template_steps.insert().execute(step)
# Query by deploy template ID.
result = deploy_template_steps.select(
deploy_template_steps.c.deploy_template_id ==
template_id).execute().first()
self.assertEqual(template_id, result['deploy_template_id'])
self.assertEqual(interface, result['interface'])
self.assertEqual(step_name, result['step'])
self.assertEqual(args, result['args'])
self.assertEqual(priority, result['priority'])
# Insert another step for the same template.
deploy_template_steps.insert().execute(step)
def test_upgrade_and_version(self): def test_upgrade_and_version(self):
with patch_with_engine(self.engine): with patch_with_engine(self.engine):
self.migration_api.upgrade('head') self.migration_api.upgrade('head')

View File

@ -0,0 +1,194 @@
# 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 DeployTemplates via the DB API"""
from oslo_db import exception as db_exc
from oslo_utils import uuidutils
import six
from ironic.common import exception
from ironic.tests.unit.db import base
from ironic.tests.unit.db import utils as db_utils
class DbDeployTemplateTestCase(base.DbTestCase):
def setUp(self):
super(DbDeployTemplateTestCase, self).setUp()
self.template = db_utils.create_test_deploy_template()
def test_create(self):
self.assertEqual('CUSTOM_DT1', self.template.name)
self.assertEqual(1, len(self.template.steps))
step = self.template.steps[0]
self.assertEqual(self.template.id, step.deploy_template_id)
self.assertEqual('raid', step.interface)
self.assertEqual('create_configuration', step.step)
self.assertEqual({'logical_disks': []}, step.args)
self.assertEqual(10, step.priority)
def test_create_no_steps(self):
uuid = uuidutils.generate_uuid()
template = db_utils.create_test_deploy_template(
uuid=uuid, name='CUSTOM_DT2', steps=[])
self.assertEqual([], template.steps)
def test_create_duplicate_uuid(self):
self.assertRaises(exception.DeployTemplateAlreadyExists,
db_utils.create_test_deploy_template,
uuid=self.template.uuid, name='CUSTOM_DT2')
def test_create_duplicate_name(self):
uuid = uuidutils.generate_uuid()
self.assertRaises(exception.DeployTemplateDuplicateName,
db_utils.create_test_deploy_template,
uuid=uuid, name=self.template.name)
def test_create_invalid_step_no_interface(self):
uuid = uuidutils.generate_uuid()
template = db_utils.get_test_deploy_template(uuid=uuid,
name='CUSTOM_DT2')
del template['steps'][0]['interface']
self.assertRaises(db_exc.DBError,
self.dbapi.create_deploy_template,
template, None)
def test_update_name(self):
values = {'name': 'CUSTOM_DT2'}
template = self.dbapi.update_deploy_template(self.template.id, values)
self.assertEqual('CUSTOM_DT2', template.name)
def test_update_steps_replace(self):
step = {'interface': 'bios', 'step': 'apply_configuration',
'args': {}, 'priority': 50}
values = {'steps': [step]}
template = self.dbapi.update_deploy_template(self.template.id, values)
self.assertEqual(1, len(template.steps))
step = template.steps[0]
self.assertEqual('bios', step.interface)
self.assertEqual('apply_configuration', step.step)
self.assertEqual({}, step.args)
self.assertEqual(50, step.priority)
def test_update_steps_add(self):
step = {'interface': 'bios', 'step': 'apply_configuration',
'args': {}, 'priority': 50}
values = {'steps': [self.template.steps[0], step]}
template = self.dbapi.update_deploy_template(self.template.id, values)
self.assertEqual(2, len(template.steps))
step0 = template.steps[0]
self.assertEqual(self.template.steps[0].id, step0.id)
self.assertEqual('raid', step0.interface)
self.assertEqual('create_configuration', step0.step)
self.assertEqual({'logical_disks': []}, step0.args)
self.assertEqual(10, step0.priority)
step1 = template.steps[1]
self.assertNotEqual(self.template.steps[0].id, step1.id)
self.assertEqual('bios', step1.interface)
self.assertEqual('apply_configuration', step1.step)
self.assertEqual({}, step1.args)
self.assertEqual(50, step1.priority)
def test_update_steps_remove_all(self):
values = {'steps': []}
template = self.dbapi.update_deploy_template(self.template.id, values)
self.assertEqual([], template.steps)
def test_update_duplicate_name(self):
uuid = uuidutils.generate_uuid()
template2 = db_utils.create_test_deploy_template(uuid=uuid,
name='CUSTOM_DT2')
values = {'name': self.template.name}
self.assertRaises(exception.DeployTemplateDuplicateName,
self.dbapi.update_deploy_template, template2.id,
values)
def test_update_not_found(self):
self.assertRaises(exception.DeployTemplateNotFound,
self.dbapi.update_deploy_template, 123, {})
def test_update_uuid_not_allowed(self):
uuid = uuidutils.generate_uuid()
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.update_deploy_template,
self.template.id, {'uuid': uuid})
def test_destroy(self):
self.dbapi.destroy_deploy_template(self.template.id)
# Attempt to retrieve the template to verify it is gone.
self.assertRaises(exception.DeployTemplateNotFound,
self.dbapi.get_deploy_template_by_id,
self.template.id)
# Ensure that the destroy_deploy_template returns the
# expected exception.
self.assertRaises(exception.DeployTemplateNotFound,
self.dbapi.destroy_deploy_template,
self.template.id)
def test_get_deploy_template_by_id(self):
res = self.dbapi.get_deploy_template_by_id(self.template.id)
self.assertEqual(self.template.id, res.id)
self.assertEqual(self.template.name, res.name)
self.assertEqual(1, len(res.steps))
self.assertEqual(self.template.id, res.steps[0].deploy_template_id)
self.assertRaises(exception.DeployTemplateNotFound,
self.dbapi.get_deploy_template_by_id, -1)
def test_get_deploy_template_by_uuid(self):
res = self.dbapi.get_deploy_template_by_uuid(self.template.uuid)
self.assertEqual(self.template.id, res.id)
self.assertRaises(exception.DeployTemplateNotFound,
self.dbapi.get_deploy_template_by_uuid, -1)
def test_get_deploy_template_by_name(self):
res = self.dbapi.get_deploy_template_by_name(self.template.name)
self.assertEqual(self.template.id, res.id)
self.assertRaises(exception.DeployTemplateNotFound,
self.dbapi.get_deploy_template_by_name, 'bogus')
def _template_list_preparation(self):
uuids = [six.text_type(self.template.uuid)]
for i in range(1, 3):
template = db_utils.create_test_deploy_template(
uuid=uuidutils.generate_uuid(),
name='CUSTOM_DT%d' % (i + 1))
uuids.append(six.text_type(template.uuid))
return uuids
def test_get_deploy_template_list(self):
uuids = self._template_list_preparation()
res = self.dbapi.get_deploy_template_list()
res_uuids = [r.uuid for r in res]
six.assertCountEqual(self, uuids, res_uuids)
def test_get_deploy_template_list_sorted(self):
uuids = self._template_list_preparation()
res = self.dbapi.get_deploy_template_list(sort_key='uuid')
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), res_uuids)
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.get_deploy_template_list, sort_key='foo')
def test_get_deploy_template_list_by_names(self):
self._template_list_preparation()
names = ['CUSTOM_DT2', 'CUSTOM_DT3']
res = self.dbapi.get_deploy_template_list_by_names(names=names)
res_names = [r.name for r in res]
six.assertCountEqual(self, names, res_names)
def test_get_deploy_template_list_by_names_no_match(self):
self._template_list_preparation()
names = ['CUSTOM_FOO']
res = self.dbapi.get_deploy_template_list_by_names(names=names)
self.assertEqual([], res)

View File

@ -25,6 +25,7 @@ from ironic.objects import allocation
from ironic.objects import bios from ironic.objects import bios
from ironic.objects import chassis from ironic.objects import chassis
from ironic.objects import conductor from ironic.objects import conductor
from ironic.objects import deploy_template
from ironic.objects import node from ironic.objects import node
from ironic.objects import port from ironic.objects import port
from ironic.objects import portgroup from ironic.objects import portgroup
@ -620,3 +621,51 @@ def create_test_allocation(**kw):
del allocation['id'] del allocation['id']
dbapi = db_api.get_instance() dbapi = db_api.get_instance()
return dbapi.create_allocation(allocation) return dbapi.create_allocation(allocation)
def get_test_deploy_template(**kw):
return {
'version': kw.get('version', deploy_template.DeployTemplate.VERSION),
'created_at': kw.get('created_at'),
'updated_at': kw.get('updated_at'),
'id': kw.get('id', 234),
'name': kw.get('name', u'CUSTOM_DT1'),
'uuid': kw.get('uuid', 'aa75a317-2929-47d4-b676-fd9bff578bf1'),
'steps': kw.get('steps', [get_test_deploy_template_step(
deploy_template_id=kw.get('id', 234))]),
}
def get_test_deploy_template_step(**kw):
return {
'created_at': kw.get('created_at'),
'updated_at': kw.get('updated_at'),
'id': kw.get('id', 345),
'deploy_template_id': kw.get('deploy_template_id', 234),
'interface': kw.get('interface', 'raid'),
'step': kw.get('step', 'create_configuration'),
'args': kw.get('args', {'logical_disks': []}),
'priority': kw.get('priority', 10),
}
def create_test_deploy_template(**kw):
"""Create a deployment template in the DB and return DeployTemplate model.
:param kw: kwargs with overriding values for the deploy template.
:returns: Test DeployTemplate DB object.
"""
template = get_test_deploy_template(**kw)
dbapi = db_api.get_instance()
# Let DB generate an ID if one isn't specified explicitly.
if 'id' not in kw:
del template['id']
if 'steps' not in kw:
for step in template['steps']:
del step['id']
del step['deploy_template_id']
else:
for kw_step, template_step in zip(kw['steps'], template['steps']):
if 'id' not in kw_step:
del template_step['id']
return dbapi.create_deploy_template(template, template['version'])

View File

@ -0,0 +1,154 @@
# 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 mock
from ironic.common import context
from ironic.db import api as dbapi
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 TestDeployTemplateObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
def setUp(self):
super(TestDeployTemplateObject, self).setUp()
self.ctxt = context.get_admin_context()
self.fake_template = db_utils.get_test_deploy_template()
@mock.patch.object(dbapi.IMPL, 'create_deploy_template', autospec=True)
def test_create(self, mock_create):
template = objects.DeployTemplate(context=self.context,
**self.fake_template)
mock_create.return_value = db_utils.get_test_deploy_template()
template.create()
args, _kwargs = mock_create.call_args
self.assertEqual(objects.DeployTemplate.VERSION, args[0]['version'])
self.assertEqual(1, mock_create.call_count)
self.assertEqual(self.fake_template['name'], template.name)
self.assertEqual(self.fake_template['steps'], template.steps)
@mock.patch.object(dbapi.IMPL, 'update_deploy_template', autospec=True)
def test_save(self, mock_update):
template = objects.DeployTemplate(context=self.context,
**self.fake_template)
template.obj_reset_changes()
mock_update.return_value = db_utils.get_test_deploy_template(
name='CUSTOM_DT2')
template.name = 'CUSTOM_DT2'
template.save()
mock_update.assert_called_once_with(
self.fake_template['uuid'],
{'name': 'CUSTOM_DT2', 'version': objects.DeployTemplate.VERSION})
self.assertEqual('CUSTOM_DT2', template.name)
@mock.patch.object(dbapi.IMPL, 'destroy_deploy_template', autospec=True)
def test_destroy(self, mock_destroy):
template = objects.DeployTemplate(context=self.context,
id=self.fake_template['id'])
template.destroy()
mock_destroy.assert_called_once_with(self.fake_template['id'])
@mock.patch.object(dbapi.IMPL, 'get_deploy_template_by_id', autospec=True)
def test_get_by_id(self, mock_get):
mock_get.return_value = self.fake_template
template = objects.DeployTemplate.get_by_id(
self.context, self.fake_template['id'])
mock_get.assert_called_once_with(self.fake_template['id'])
self.assertEqual(self.fake_template['name'], template.name)
self.assertEqual(self.fake_template['uuid'], template.uuid)
self.assertEqual(self.fake_template['steps'], template.steps)
@mock.patch.object(dbapi.IMPL, 'get_deploy_template_by_uuid',
autospec=True)
def test_get_by_uuid(self, mock_get):
mock_get.return_value = self.fake_template
template = objects.DeployTemplate.get_by_uuid(
self.context, self.fake_template['uuid'])
mock_get.assert_called_once_with(self.fake_template['uuid'])
self.assertEqual(self.fake_template['name'], template.name)
self.assertEqual(self.fake_template['uuid'], template.uuid)
self.assertEqual(self.fake_template['steps'], template.steps)
@mock.patch.object(dbapi.IMPL, 'get_deploy_template_by_name',
autospec=True)
def test_get_by_name(self, mock_get):
mock_get.return_value = self.fake_template
template = objects.DeployTemplate.get_by_name(
self.context, self.fake_template['name'])
mock_get.assert_called_once_with(self.fake_template['name'])
self.assertEqual(self.fake_template['name'], template.name)
self.assertEqual(self.fake_template['uuid'], template.uuid)
self.assertEqual(self.fake_template['steps'], template.steps)
@mock.patch.object(dbapi.IMPL, 'get_deploy_template_list', autospec=True)
def test_list(self, mock_list):
mock_list.return_value = [self.fake_template]
templates = objects.DeployTemplate.list(self.context)
mock_list.assert_called_once_with(limit=None, marker=None,
sort_dir=None, sort_key=None)
self.assertEqual(1, len(templates))
self.assertEqual(self.fake_template['name'], templates[0].name)
self.assertEqual(self.fake_template['uuid'], templates[0].uuid)
self.assertEqual(self.fake_template['steps'], templates[0].steps)
@mock.patch.object(dbapi.IMPL, 'get_deploy_template_list_by_names',
autospec=True)
def test_list_by_names(self, mock_list):
mock_list.return_value = [self.fake_template]
names = [self.fake_template['name']]
templates = objects.DeployTemplate.list_by_names(self.context, names)
mock_list.assert_called_once_with(names)
self.assertEqual(1, len(templates))
self.assertEqual(self.fake_template['name'], templates[0].name)
self.assertEqual(self.fake_template['uuid'], templates[0].uuid)
self.assertEqual(self.fake_template['steps'], templates[0].steps)
@mock.patch.object(dbapi.IMPL, 'get_deploy_template_by_uuid',
autospec=True)
def test_refresh(self, mock_get):
uuid = self.fake_template['uuid']
mock_get.side_effect = [dict(self.fake_template),
dict(self.fake_template, name='CUSTOM_DT2')]
template = objects.DeployTemplate.get_by_uuid(self.context, uuid)
self.assertEqual(self.fake_template['name'], template.name)
template.refresh()
self.assertEqual('CUSTOM_DT2', template.name)
expected = [mock.call(uuid), mock.call(uuid)]
self.assertEqual(expected, mock_get.call_args_list)
self.assertEqual(self.context, template._context)

View File

@ -717,6 +717,7 @@ expected_object_fingerprints = {
'Allocation': '1.0-25ebf609743cd3f332a4f80fcb818102', 'Allocation': '1.0-25ebf609743cd3f332a4f80fcb818102',
'AllocationCRUDNotification': '1.0-59acc533c11d306f149846f922739c15', 'AllocationCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'AllocationCRUDPayload': '1.0-a82389d019f37cfe54b50049f73911b3', 'AllocationCRUDPayload': '1.0-a82389d019f37cfe54b50049f73911b3',
'DeployTemplate': '1.0-c20a91a34a5518e13b2a1bf5072eb119',
} }

View File

@ -296,6 +296,41 @@ def create_test_allocation(ctxt, **kw):
return allocation return allocation
def get_test_deploy_template(ctxt, **kw):
"""Return a DeployTemplate object with appropriate attributes.
NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB.
"""
db_template = db_utils.get_test_deploy_template(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del db_template['id']
if 'steps' not in kw:
for step in db_template['steps']:
del step['id']
del step['deploy_template_id']
else:
for kw_step, template_step in zip(kw['steps'], db_template['steps']):
if 'id' not in kw_step and 'id' in template_step:
del template_step['id']
template = objects.DeployTemplate(ctxt)
for key in db_template:
setattr(template, key, db_template[key])
return template
def create_test_deploy_template(ctxt, **kw):
"""Create and return a test deploy template object.
NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB.
"""
template = get_test_deploy_template(ctxt, **kw)
template.create()
return template
def get_payloads_with_schemas(from_module): def get_payloads_with_schemas(from_module):
"""Get the Payload classes with SCHEMAs defined. """Get the Payload classes with SCHEMAs defined.