Deploy templates: data model, DB API & objects
Adds deploy_templates and deploy_template_steps tables to the database, provides a DB API for these tables, and a DeployTemplate versioned object. Change-Id: I5b8b59bbea1594b1220438050b80f1c603dbc346 Story: 1722275 Task: 28674
This commit is contained in:
parent
0d19732089
commit
b137af30b9
@ -83,6 +83,9 @@ ONLINE_MIGRATIONS = (
|
||||
NEW_MODELS = [
|
||||
# TODO(dtantsur): remove in Train
|
||||
'Allocation',
|
||||
# TODO(mgoddard): remove in Train
|
||||
'DeployTemplate',
|
||||
'DeployTemplateStep',
|
||||
]
|
||||
|
||||
|
||||
|
@ -807,3 +807,15 @@ class AllocationAlreadyExists(Conflict):
|
||||
|
||||
class AllocationFailed(IronicException):
|
||||
_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.")
|
||||
|
@ -138,6 +138,7 @@ RELEASE_MAPPING = {
|
||||
'Node': ['1.32', '1.31', '1.30', '1.29', '1.28'],
|
||||
'Conductor': ['1.3'],
|
||||
'Chassis': ['1.3'],
|
||||
'DeployTemplate': ['1.0'],
|
||||
'Port': ['1.9'],
|
||||
'Portgroup': ['1.4'],
|
||||
'Trait': ['1.0'],
|
||||
|
@ -1165,3 +1165,99 @@ class Connection(object):
|
||||
:param allocation_id: Allocation ID
|
||||
: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.
|
||||
"""
|
||||
|
@ -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'
|
||||
)
|
@ -85,6 +85,14 @@ def _get_node_query_with_all():
|
||||
.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):
|
||||
"""Query helper for simpler session usage.
|
||||
|
||||
@ -218,6 +226,42 @@ def _filter_active_conductors(query, interval=None):
|
||||
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")
|
||||
class Connection(api.Connection):
|
||||
"""SqlAlchemy connection."""
|
||||
@ -1710,3 +1754,155 @@ class Connection(api.Connection):
|
||||
node_query.update({'allocation_id': None, 'instance_uuid': None})
|
||||
|
||||
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()
|
||||
|
@ -350,6 +350,45 @@ class Allocation(Base):
|
||||
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):
|
||||
"""Returns the model class with the specified name.
|
||||
|
||||
|
@ -28,6 +28,7 @@ def register_all():
|
||||
__import__('ironic.objects.bios')
|
||||
__import__('ironic.objects.chassis')
|
||||
__import__('ironic.objects.conductor')
|
||||
__import__('ironic.objects.deploy_template')
|
||||
__import__('ironic.objects.node')
|
||||
__import__('ironic.objects.port')
|
||||
__import__('ironic.objects.portgroup')
|
||||
|
240
ironic/objects/deploy_template.py
Normal file
240
ironic/objects/deploy_template.py
Normal 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()
|
@ -106,6 +106,10 @@ class FlexibleDictField(object_fields.AutoTypedField):
|
||||
super(FlexibleDictField, self)._null(obj, attr)
|
||||
|
||||
|
||||
class ListOfFlexibleDictsField(object_fields.AutoTypedField):
|
||||
AUTO_TYPE = object_fields.List(FlexibleDict())
|
||||
|
||||
|
||||
class EnumField(object_fields.EnumField):
|
||||
pass
|
||||
|
||||
|
@ -85,7 +85,7 @@ class ReleaseMappingsTestCase(base.TestCase):
|
||||
self.assertIn('master', release_mappings.RELEASE_MAPPING)
|
||||
model_names = set((s.__name__ for s in models.Base.__subclasses__()))
|
||||
exceptions = set(['NodeTag', 'ConductorHardwareInterfaces',
|
||||
'NodeTrait', 'BIOSSetting'])
|
||||
'NodeTrait', 'BIOSSetting', 'DeployTemplateStep'])
|
||||
# NOTE(xek): As a rule, all models which can be changed between
|
||||
# releases or are sent through RPC should have their counterpart
|
||||
# versioned objects.
|
||||
|
@ -858,6 +858,102 @@ class MigrationCheckersMixin(object):
|
||||
self.assertIsInstance(nodes_tbl.c.description.type,
|
||||
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):
|
||||
with patch_with_engine(self.engine):
|
||||
self.migration_api.upgrade('head')
|
||||
|
194
ironic/tests/unit/db/test_deploy_templates.py
Normal file
194
ironic/tests/unit/db/test_deploy_templates.py
Normal 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)
|
@ -25,6 +25,7 @@ from ironic.objects import allocation
|
||||
from ironic.objects import bios
|
||||
from ironic.objects import chassis
|
||||
from ironic.objects import conductor
|
||||
from ironic.objects import deploy_template
|
||||
from ironic.objects import node
|
||||
from ironic.objects import port
|
||||
from ironic.objects import portgroup
|
||||
@ -620,3 +621,51 @@ def create_test_allocation(**kw):
|
||||
del allocation['id']
|
||||
dbapi = db_api.get_instance()
|
||||
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'])
|
||||
|
154
ironic/tests/unit/objects/test_deploy_template.py
Normal file
154
ironic/tests/unit/objects/test_deploy_template.py
Normal 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)
|
@ -717,6 +717,7 @@ expected_object_fingerprints = {
|
||||
'Allocation': '1.0-25ebf609743cd3f332a4f80fcb818102',
|
||||
'AllocationCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'AllocationCRUDPayload': '1.0-a82389d019f37cfe54b50049f73911b3',
|
||||
'DeployTemplate': '1.0-c20a91a34a5518e13b2a1bf5072eb119',
|
||||
}
|
||||
|
||||
|
||||
|
@ -296,6 +296,41 @@ def create_test_allocation(ctxt, **kw):
|
||||
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):
|
||||
"""Get the Payload classes with SCHEMAs defined.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user