Implements node history: database
This patch provides basic data model change to support node history. Batch removal is not included in this patch. Change-Id: I5c7cebd585ee84b5b57bd4690d4074baf0d05699 Story: 2002980 Task: 22989
This commit is contained in:
parent
8ea1a438d3
commit
fbaad948d8
@ -821,3 +821,7 @@ class AgentInProgress(IronicException):
|
|||||||
class InsufficentMemory(IronicException):
|
class InsufficentMemory(IronicException):
|
||||||
_msg_fmt = _("Available memory at %(free)s, Insufficent as %(required)s "
|
_msg_fmt = _("Available memory at %(free)s, Insufficent as %(required)s "
|
||||||
"is required to proceed at this time.")
|
"is required to proceed at this time.")
|
||||||
|
|
||||||
|
|
||||||
|
class NodeHistoryNotFound(NotFound):
|
||||||
|
_msg_fmt = _("Node history record %(history)s could not be found.")
|
||||||
|
@ -377,6 +377,7 @@ RELEASE_MAPPING = {
|
|||||||
'Allocation': ['1.1'],
|
'Allocation': ['1.1'],
|
||||||
'BIOSSetting': ['1.1'],
|
'BIOSSetting': ['1.1'],
|
||||||
'Node': ['1.36', '1.35'],
|
'Node': ['1.36', '1.35'],
|
||||||
|
'NodeHistory': ['1.0'],
|
||||||
'Conductor': ['1.3'],
|
'Conductor': ['1.3'],
|
||||||
'Chassis': ['1.3'],
|
'Chassis': ['1.3'],
|
||||||
'Deployment': ['1.0'],
|
'Deployment': ['1.0'],
|
||||||
|
@ -1322,3 +1322,61 @@ class Connection(object, metaclass=abc.ABCMeta):
|
|||||||
:param names: List of names to filter by.
|
:param names: List of names to filter by.
|
||||||
:returns: A list of deploy templates.
|
:returns: A list of deploy templates.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def create_node_history(self, values):
|
||||||
|
"""Create a new history record.
|
||||||
|
|
||||||
|
:param values: Dict of values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def destroy_node_history_by_uuid(self, history_uuid):
|
||||||
|
"""Destroy a history record.
|
||||||
|
|
||||||
|
:param history_uuid: The uuid of a history record
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_node_history_by_id(self, history_id):
|
||||||
|
"""Return a node history representation.
|
||||||
|
|
||||||
|
:param history_id: The id of a history record.
|
||||||
|
:returns: A history.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_node_history_by_uuid(self, history_uuid):
|
||||||
|
"""Return a node history representation.
|
||||||
|
|
||||||
|
:param history_uuid: The uuid of a history record
|
||||||
|
:returns: A history.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_node_history_list(self, limit=None, marker=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
"""Return a list of node history records
|
||||||
|
|
||||||
|
:param limit: Maximum number of history records 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)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_node_history_by_node_id(self, node_id, limit=None, marker=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
"""List all the history records for a given node.
|
||||||
|
|
||||||
|
:param node_id: The integer node ID.
|
||||||
|
:param limit: Maximum number of history records 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 histories.
|
||||||
|
"""
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""add_node_history_table
|
||||||
|
|
||||||
|
Revision ID: 9ef41f07cb58
|
||||||
|
Revises: c1846a214450
|
||||||
|
Create Date: 2020-12-20 17:45:57.278649
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9ef41f07cb58'
|
||||||
|
down_revision = 'c1846a214450'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table('node_history',
|
||||||
|
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),
|
||||||
|
sa.Column('uuid', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('conductor', sa.String(length=255),
|
||||||
|
nullable=True),
|
||||||
|
sa.Column('event_type', sa.String(length=255),
|
||||||
|
nullable=True),
|
||||||
|
sa.Column('severity', sa.String(length=255),
|
||||||
|
nullable=True),
|
||||||
|
sa.Column('event', sa.Text(), nullable=True),
|
||||||
|
sa.Column('user', sa.String(length=32), nullable=True),
|
||||||
|
sa.Column('node_id', sa.Integer(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('uuid', name='uniq_history0uuid'),
|
||||||
|
sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
|
||||||
|
sa.Index('history_node_id_idx', 'node_id'),
|
||||||
|
sa.Index('history_uuid_idx', 'uuid'),
|
||||||
|
sa.Index('history_conductor_idx', 'conductor'),
|
||||||
|
mysql_ENGINE='InnoDB',
|
||||||
|
mysql_DEFAULT_CHARSET='UTF8')
|
@ -789,6 +789,11 @@ class Connection(api.Connection):
|
|||||||
models.Allocation).filter_by(node_id=node_id)
|
models.Allocation).filter_by(node_id=node_id)
|
||||||
allocation_query.delete()
|
allocation_query.delete()
|
||||||
|
|
||||||
|
# delete all history for this node
|
||||||
|
history_query = model_query(
|
||||||
|
models.NodeHistory).filter_by(node_id=node_id)
|
||||||
|
history_query.delete()
|
||||||
|
|
||||||
query.delete()
|
query.delete()
|
||||||
|
|
||||||
def update_node(self, node_id, values):
|
def update_node(self, node_id, values):
|
||||||
@ -2275,3 +2280,52 @@ class Connection(api.Connection):
|
|||||||
query = (_get_deploy_template_query_with_steps()
|
query = (_get_deploy_template_query_with_steps()
|
||||||
.filter(models.DeployTemplate.name.in_(names)))
|
.filter(models.DeployTemplate.name.in_(names)))
|
||||||
return query.all()
|
return query.all()
|
||||||
|
|
||||||
|
@oslo_db_api.retry_on_deadlock
|
||||||
|
def create_node_history(self, values):
|
||||||
|
values['uuid'] = uuidutils.generate_uuid()
|
||||||
|
|
||||||
|
history = models.NodeHistory()
|
||||||
|
history.update(values)
|
||||||
|
with _session_for_write() as session:
|
||||||
|
try:
|
||||||
|
session.add(history)
|
||||||
|
session.flush()
|
||||||
|
except db_exc.DBDuplicateEntry:
|
||||||
|
raise exception.NodeHistoryAlreadyExists(uuid=values['uuid'])
|
||||||
|
return history
|
||||||
|
|
||||||
|
@oslo_db_api.retry_on_deadlock
|
||||||
|
def destroy_node_history_by_uuid(self, history_uuid):
|
||||||
|
with _session_for_write():
|
||||||
|
query = model_query(models.NodeHistory).filter_by(
|
||||||
|
uuid=history_uuid)
|
||||||
|
count = query.delete()
|
||||||
|
if count == 0:
|
||||||
|
raise exception.NodeHistoryNotFound(history=history_uuid)
|
||||||
|
|
||||||
|
def get_node_history_by_id(self, history_id):
|
||||||
|
query = model_query(models.NodeHistory).filter_by(id=history_id)
|
||||||
|
try:
|
||||||
|
return query.one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.NodeHistoryNotFound(history=history_id)
|
||||||
|
|
||||||
|
def get_node_history_by_uuid(self, history_uuid):
|
||||||
|
query = model_query(models.NodeHistory).filter_by(uuid=history_uuid)
|
||||||
|
try:
|
||||||
|
return query.one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.NodeHistoryNotFound(history=history_uuid)
|
||||||
|
|
||||||
|
def get_node_history_list(self, limit=None, marker=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
return _paginate_query(models.NodeHistory, limit, marker, sort_key,
|
||||||
|
sort_dir)
|
||||||
|
|
||||||
|
def get_node_history_by_node_id(self, node_id, limit=None, marker=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
query = model_query(models.NodeHistory)
|
||||||
|
query = query.filter_by(node_id=node_id)
|
||||||
|
return _paginate_query(models.NodeHistory, limit, marker,
|
||||||
|
sort_key, sort_dir, query)
|
||||||
|
@ -417,6 +417,26 @@ class DeployTemplateStep(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeHistory(Base):
|
||||||
|
"""Represents a history event of a bare metal node."""
|
||||||
|
|
||||||
|
__tablename__ = 'node_history'
|
||||||
|
__table_args__ = (
|
||||||
|
schema.UniqueConstraint('uuid', name='uniq_history0uuid'),
|
||||||
|
Index('history_node_id_idx', 'node_id'),
|
||||||
|
Index('history_uuid_idx', 'uuid'),
|
||||||
|
Index('history_conductor_idx', 'conductor'),
|
||||||
|
table_args())
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
uuid = Column(String(36), nullable=False)
|
||||||
|
conductor = Column(String(255), nullable=True)
|
||||||
|
event_type = Column(String(255), nullable=True)
|
||||||
|
severity = Column(String(255), nullable=True)
|
||||||
|
event = Column(Text, nullable=True)
|
||||||
|
user = Column(String(32), nullable=True)
|
||||||
|
node_id = Column(Integer, ForeignKey('nodes.id'), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ def register_all():
|
|||||||
__import__('ironic.objects.deploy_template')
|
__import__('ironic.objects.deploy_template')
|
||||||
__import__('ironic.objects.deployment')
|
__import__('ironic.objects.deployment')
|
||||||
__import__('ironic.objects.node')
|
__import__('ironic.objects.node')
|
||||||
|
__import__('ironic.objects.node_history')
|
||||||
__import__('ironic.objects.port')
|
__import__('ironic.objects.port')
|
||||||
__import__('ironic.objects.portgroup')
|
__import__('ironic.objects.portgroup')
|
||||||
__import__('ironic.objects.trait')
|
__import__('ironic.objects.trait')
|
||||||
|
184
ironic/objects/node_history.py
Normal file
184
ironic/objects/node_history.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_utils import strutils
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
from oslo_versionedobjects import base as object_base
|
||||||
|
|
||||||
|
from ironic.common import exception
|
||||||
|
from ironic.db import api as dbapi
|
||||||
|
from ironic.objects import base
|
||||||
|
from ironic.objects import fields as object_fields
|
||||||
|
|
||||||
|
|
||||||
|
@base.IronicObjectRegistry.register
|
||||||
|
class NodeHistory(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||||
|
# Version 1.0: Initial version
|
||||||
|
VERSION = '1.0'
|
||||||
|
|
||||||
|
dbapi = dbapi.get_instance()
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'id': object_fields.IntegerField(),
|
||||||
|
'uuid': object_fields.UUIDField(nullable=True),
|
||||||
|
'conductor': object_fields.StringField(nullable=True),
|
||||||
|
'event': object_fields.StringField(nullable=True),
|
||||||
|
'user': object_fields.StringField(nullable=True),
|
||||||
|
'node_id': object_fields.IntegerField(nullable=True),
|
||||||
|
'event_type': object_fields.StringField(nullable=True),
|
||||||
|
'severity': object_fields.StringField(nullable=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
|
||||||
|
# methods can be used in the future to replace current explicit RPC calls.
|
||||||
|
# Implications of calling new remote procedures should be thought through.
|
||||||
|
# @object_base.remotable_classmethod
|
||||||
|
@classmethod
|
||||||
|
def get(cls, context, history_ident):
|
||||||
|
"""Get a history based on its id or uuid.
|
||||||
|
|
||||||
|
:param history_ident: The id or uuid of a history.
|
||||||
|
:param context: Security context
|
||||||
|
:returns: A :class:`NodeHistory` object.
|
||||||
|
:raises: InvalidIdentity
|
||||||
|
|
||||||
|
"""
|
||||||
|
if strutils.is_int_like(history_ident):
|
||||||
|
return cls.get_by_id(context, history_ident)
|
||||||
|
elif uuidutils.is_uuid_like(history_ident):
|
||||||
|
return cls.get_by_uuid(context, history_ident)
|
||||||
|
else:
|
||||||
|
raise exception.InvalidIdentity(identity=history_ident)
|
||||||
|
|
||||||
|
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
|
||||||
|
# methods can be used in the future to replace current explicit RPC calls.
|
||||||
|
# Implications of calling new remote procedures should be thought through.
|
||||||
|
# @object_base.remotable_classmethod
|
||||||
|
@classmethod
|
||||||
|
def get_by_id(cls, context, history_id):
|
||||||
|
"""Get a NodeHistory object by its integer ID.
|
||||||
|
|
||||||
|
:param cls: the :class:`NodeHistory`
|
||||||
|
:param context: Security context
|
||||||
|
:param history_id: The ID of a history.
|
||||||
|
:returns: A :class:`NodeHistory` object.
|
||||||
|
:raises: NodeHistoryNotFound
|
||||||
|
|
||||||
|
"""
|
||||||
|
db_history = cls.dbapi.get_node_history_by_id(history_id)
|
||||||
|
history = cls._from_db_object(context, cls(), db_history)
|
||||||
|
return history
|
||||||
|
|
||||||
|
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
|
||||||
|
# methods can be used in the future to replace current explicit RPC calls.
|
||||||
|
# Implications of calling new remote procedures should be thought through.
|
||||||
|
# @object_base.remotable_classmethod
|
||||||
|
@classmethod
|
||||||
|
def get_by_uuid(cls, context, uuid):
|
||||||
|
"""Get a NodeHistory object by its UUID.
|
||||||
|
|
||||||
|
:param cls: the :class:`NodeHistory`
|
||||||
|
:param context: Security context
|
||||||
|
:param uuid: The UUID of a NodeHistory.
|
||||||
|
:returns: A :class:`NodeHistory` object.
|
||||||
|
:raises: NodeHistoryNotFound
|
||||||
|
|
||||||
|
"""
|
||||||
|
db_history = cls.dbapi.get_node_history_by_uuid(uuid)
|
||||||
|
history = cls._from_db_object(context, cls(), db_history)
|
||||||
|
return history
|
||||||
|
|
||||||
|
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
|
||||||
|
# methods can be used in the future to replace current explicit RPC calls.
|
||||||
|
# Implications of calling new remote procedures should be thought through.
|
||||||
|
# @object_base.remotable_classmethod
|
||||||
|
@classmethod
|
||||||
|
def list(cls, context, limit=None, marker=None, sort_key=None,
|
||||||
|
sort_dir=None):
|
||||||
|
"""Return a list of NodeHistory objects.
|
||||||
|
|
||||||
|
:param cls: the :class:`NodeHistory`
|
||||||
|
:param context: Security 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:`NodeHistory` object.
|
||||||
|
:raises: InvalidParameterValue
|
||||||
|
|
||||||
|
"""
|
||||||
|
db_histories = cls.dbapi.get_node_history_list(limit=limit,
|
||||||
|
marker=marker,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
return cls._from_db_object_list(context, db_histories)
|
||||||
|
|
||||||
|
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
|
||||||
|
# methods can be used in the future to replace current explicit RPC calls.
|
||||||
|
# Implications of calling new remote procedures should be thought through.
|
||||||
|
# @object_base.remotable_classmethod
|
||||||
|
@classmethod
|
||||||
|
def list_by_node_id(cls, context, node_id, limit=None, marker=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
"""Return a list of NodeHistory objects belongs to a given node ID.
|
||||||
|
|
||||||
|
:param cls: the :class:`NodeHistory`
|
||||||
|
:param context: Security context.
|
||||||
|
:param node_id: The ID of the node.
|
||||||
|
: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:`NodeHistory` object.
|
||||||
|
:raises: InvalidParameterValue
|
||||||
|
|
||||||
|
"""
|
||||||
|
db_histories = cls.dbapi.get_node_history_by_node_id(
|
||||||
|
node_id, limit=limit, marker=marker, sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
return cls._from_db_object_list(context, db_histories)
|
||||||
|
|
||||||
|
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
|
||||||
|
# methods can be used in the future to replace current explicit RPC calls.
|
||||||
|
# Implications of calling new remote procedures should be thought through.
|
||||||
|
# @object_base.remotable
|
||||||
|
def create(self, context=None):
|
||||||
|
"""Create a NodeHistory 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.: NodeHistory(context)
|
||||||
|
"""
|
||||||
|
values = self.do_version_changes_for_db()
|
||||||
|
db_history = self.dbapi.create_node_history(values)
|
||||||
|
self._from_db_object(self._context, self, db_history)
|
||||||
|
|
||||||
|
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
|
||||||
|
# methods can be used in the future to replace current explicit RPC calls.
|
||||||
|
# Implications of calling new remote procedures should be thought through.
|
||||||
|
# @object_base.remotable
|
||||||
|
def destroy(self, context=None):
|
||||||
|
"""Delete the NodeHistory 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.: NodeHistory(context)
|
||||||
|
:raises: NodeHistoryNotFound
|
||||||
|
"""
|
||||||
|
self.dbapi.destroy_node_history_by_uuid(self.uuid)
|
||||||
|
self.obj_reset_changes()
|
@ -1053,6 +1053,36 @@ class MigrationCheckersMixin(object):
|
|||||||
col_names = [column.name for column in ports.c]
|
col_names = [column.name for column in ports.c]
|
||||||
self.assertIn('name', col_names)
|
self.assertIn('name', col_names)
|
||||||
|
|
||||||
|
def _check_9ef41f07cb58(self, engine, data):
|
||||||
|
node_history = db_utils.get_table(engine, 'node_history')
|
||||||
|
col_names = [column.name for column in node_history.c]
|
||||||
|
|
||||||
|
expected_names = ['version', 'created_at', 'updated_at', 'id', 'uuid',
|
||||||
|
'conductor', 'event_type', 'severity', 'event',
|
||||||
|
'user', 'node_id']
|
||||||
|
self.assertEqual(sorted(expected_names), sorted(col_names))
|
||||||
|
|
||||||
|
self.assertIsInstance(node_history.c.created_at.type,
|
||||||
|
sqlalchemy.types.DateTime)
|
||||||
|
self.assertIsInstance(node_history.c.updated_at.type,
|
||||||
|
sqlalchemy.types.DateTime)
|
||||||
|
self.assertIsInstance(node_history.c.id.type,
|
||||||
|
sqlalchemy.types.Integer)
|
||||||
|
self.assertIsInstance(node_history.c.uuid.type,
|
||||||
|
sqlalchemy.types.String)
|
||||||
|
self.assertIsInstance(node_history.c.conductor.type,
|
||||||
|
sqlalchemy.types.String)
|
||||||
|
self.assertIsInstance(node_history.c.event_type.type,
|
||||||
|
sqlalchemy.types.String)
|
||||||
|
self.assertIsInstance(node_history.c.severity.type,
|
||||||
|
sqlalchemy.types.String)
|
||||||
|
self.assertIsInstance(node_history.c.event.type,
|
||||||
|
sqlalchemy.types.TEXT)
|
||||||
|
self.assertIsInstance(node_history.c.node_id.type,
|
||||||
|
sqlalchemy.types.Integer)
|
||||||
|
self.assertIsInstance(node_history.c.user.type,
|
||||||
|
sqlalchemy.types.String)
|
||||||
|
|
||||||
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')
|
||||||
|
93
ironic/tests/unit/db/test_node_history.py
Normal file
93
ironic/tests/unit/db/test_node_history.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
|
from ironic.common import exception
|
||||||
|
from ironic.tests.unit.db import base
|
||||||
|
from ironic.tests.unit.db import utils as db_utils
|
||||||
|
|
||||||
|
|
||||||
|
class DBNodeHistoryTestCase(base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DBNodeHistoryTestCase, self).setUp()
|
||||||
|
self.node = db_utils.create_test_node()
|
||||||
|
self.history = db_utils.create_test_history(
|
||||||
|
id=0, node_id=self.node.id, conductor='test-conductor',
|
||||||
|
user='fake-user', event='Something bad happened but fear not')
|
||||||
|
|
||||||
|
def test_destroy_node_history_by_uuid(self):
|
||||||
|
self.dbapi.destroy_node_history_by_uuid(self.history.uuid)
|
||||||
|
self.assertRaises(exception.NodeHistoryNotFound,
|
||||||
|
self.dbapi.get_node_history_by_id,
|
||||||
|
self.history.id)
|
||||||
|
self.assertRaises(exception.NodeHistoryNotFound,
|
||||||
|
self.dbapi.get_node_history_by_uuid,
|
||||||
|
self.history.uuid)
|
||||||
|
|
||||||
|
def test_get_history_by_id(self):
|
||||||
|
res = self.dbapi.get_node_history_by_id(self.history.id)
|
||||||
|
self.assertEqual(self.history.conductor, res.conductor)
|
||||||
|
self.assertEqual(self.history.user, res.user)
|
||||||
|
self.assertEqual(self.history.event, res.event)
|
||||||
|
|
||||||
|
def test_get_history_by_id_not_found(self):
|
||||||
|
self.assertRaises(exception.NodeHistoryNotFound,
|
||||||
|
self.dbapi.get_node_history_by_id, -1)
|
||||||
|
|
||||||
|
def test_get_history_by_uuid(self):
|
||||||
|
res = self.dbapi.get_node_history_by_uuid(self.history.uuid)
|
||||||
|
self.assertEqual(self.history.id, res.id)
|
||||||
|
|
||||||
|
def test_get_history_by_uuid_not_found(self):
|
||||||
|
self.assertRaises(exception.NodeHistoryNotFound,
|
||||||
|
self.dbapi.get_node_history_by_uuid,
|
||||||
|
'wrong-uuid')
|
||||||
|
|
||||||
|
def _prepare_history_entries(self):
|
||||||
|
uuids = [str(self.history.uuid)]
|
||||||
|
for i in range(1, 6):
|
||||||
|
history = db_utils.create_test_history(
|
||||||
|
id=i, uuid=uuidutils.generate_uuid(),
|
||||||
|
conductor='test-conductor', user='fake-user',
|
||||||
|
event='Something bad happened but fear not %s' % i,
|
||||||
|
severity='ERROR', event_type='test')
|
||||||
|
uuids.append(str(history.uuid))
|
||||||
|
return uuids
|
||||||
|
|
||||||
|
def test_get_node_history_list(self):
|
||||||
|
uuids = self._prepare_history_entries()
|
||||||
|
res = self.dbapi.get_node_history_list()
|
||||||
|
res_uuids = [r.uuid for r in res]
|
||||||
|
self.assertCountEqual(uuids, res_uuids)
|
||||||
|
|
||||||
|
def test_get_node_history_list_sorted(self):
|
||||||
|
self._prepare_history_entries()
|
||||||
|
|
||||||
|
res = self.dbapi.get_node_history_list(sort_key='created_at',
|
||||||
|
sort_dir='desc')
|
||||||
|
expected = sorted(res, key=lambda r: r.created_at, reverse=True)
|
||||||
|
self.assertEqual(res, expected)
|
||||||
|
self.assertIn('fear not 5', res[0].event)
|
||||||
|
|
||||||
|
def test_get_history_by_node_id_empty(self):
|
||||||
|
self.assertEqual([], self.dbapi.get_node_history_by_node_id(10))
|
||||||
|
|
||||||
|
def test_get_history_by_node_id(self):
|
||||||
|
res = self.dbapi.get_node_history_by_node_id(self.node.id)
|
||||||
|
self.assertEqual(self.history.uuid, res[0].uuid)
|
||||||
|
self.assertEqual(self.history.user, res[0].user)
|
||||||
|
self.assertEqual(self.history.conductor, res[0].conductor)
|
||||||
|
self.assertEqual(self.history.event, res[0].event)
|
||||||
|
self.assertEqual(self.history.event_type, res[0].event_type)
|
||||||
|
self.assertEqual(self.history.severity, res[0].severity)
|
@ -751,6 +751,15 @@ class DbNodeTestCase(base.DbTestCase):
|
|||||||
self.assertRaises(exception.AllocationNotFound,
|
self.assertRaises(exception.AllocationNotFound,
|
||||||
self.dbapi.get_allocation_by_id, allocation.id)
|
self.dbapi.get_allocation_by_id, allocation.id)
|
||||||
|
|
||||||
|
def test_history_get_destroyed_after_destroying_a_node_by_uuid(self):
|
||||||
|
node = utils.create_test_node()
|
||||||
|
|
||||||
|
history = utils.create_test_history(node_id=node.id)
|
||||||
|
|
||||||
|
self.dbapi.destroy_node(node.uuid)
|
||||||
|
self.assertRaises(exception.NodeHistoryNotFound,
|
||||||
|
self.dbapi.get_node_history_by_id, history.id)
|
||||||
|
|
||||||
def test_update_node(self):
|
def test_update_node(self):
|
||||||
node = utils.create_test_node()
|
node = utils.create_test_node()
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ 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 deploy_template
|
||||||
from ironic.objects import node
|
from ironic.objects import node
|
||||||
|
from ironic.objects import node_history
|
||||||
from ironic.objects import port
|
from ironic.objects import port
|
||||||
from ironic.objects import portgroup
|
from ironic.objects import portgroup
|
||||||
from ironic.objects import trait
|
from ironic.objects import trait
|
||||||
@ -690,3 +691,33 @@ def get_test_ibmc_info():
|
|||||||
"ibmc_password": "password",
|
"ibmc_password": "password",
|
||||||
"verify_ca": False,
|
"verify_ca": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_test_history(**kw):
|
||||||
|
return {
|
||||||
|
'id': kw.get('id', 345),
|
||||||
|
'version': kw.get('version', node_history.NodeHistory.VERSION),
|
||||||
|
'uuid': kw.get('uuid', '6f8a5d5c-0f2d-4b2c-a62a-a38e300e3f31'),
|
||||||
|
'node_id': kw.get('node_id', 123),
|
||||||
|
'event': kw.get('event', 'Something is wrong'),
|
||||||
|
'conductor': kw.get('conductor', 'host-1'),
|
||||||
|
'severity': kw.get('severity', 'ERROR'),
|
||||||
|
'event_type': kw.get('event_type', 'provisioning'),
|
||||||
|
'user': kw.get('user', 'fake-user'),
|
||||||
|
'created_at': kw.get('created_at'),
|
||||||
|
'updated_at': kw.get('updated_at'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_history(**kw):
|
||||||
|
"""Create test history entry in DB and return NodeHistory DB object.
|
||||||
|
|
||||||
|
:param kw: kwargs with overriding values for port's attributes.
|
||||||
|
:returns: Test NodeHistory DB object.
|
||||||
|
"""
|
||||||
|
history = get_test_history(**kw)
|
||||||
|
# Let DB generate ID if it isn't specified explicitly
|
||||||
|
if 'id' not in kw:
|
||||||
|
del history['id']
|
||||||
|
dbapi = db_api.get_instance()
|
||||||
|
return dbapi.create_node_history(history)
|
||||||
|
133
ironic/tests/unit/objects/test_node_history.py
Normal file
133
ironic/tests/unit/objects/test_node_history.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# 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 types
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from testtools.matchers import HasLength
|
||||||
|
|
||||||
|
from ironic.common import exception
|
||||||
|
from ironic import objects
|
||||||
|
from ironic.tests.unit.db import base as db_base
|
||||||
|
from ironic.tests.unit.db import utils as db_utils
|
||||||
|
from ironic.tests.unit.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
|
class TestNodeHistoryObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestNodeHistoryObject, self).setUp()
|
||||||
|
self.fake_history = db_utils.get_test_history()
|
||||||
|
|
||||||
|
def test_get_by_id(self):
|
||||||
|
with mock.patch.object(self.dbapi, 'get_node_history_by_id',
|
||||||
|
autospec=True) as mock_get:
|
||||||
|
id_ = self.fake_history['id']
|
||||||
|
mock_get.return_value = self.fake_history
|
||||||
|
|
||||||
|
history = objects.NodeHistory.get_by_id(self.context, id_)
|
||||||
|
|
||||||
|
mock_get.assert_called_once_with(id_)
|
||||||
|
self.assertIsInstance(history, objects.NodeHistory)
|
||||||
|
self.assertEqual(self.context, history._context)
|
||||||
|
|
||||||
|
def test_get_by_uuid(self):
|
||||||
|
uuid = self.fake_history['uuid']
|
||||||
|
with mock.patch.object(self.dbapi, 'get_node_history_by_uuid',
|
||||||
|
autospec=True) as mock_get:
|
||||||
|
mock_get.return_value = self.fake_history
|
||||||
|
|
||||||
|
history = objects.NodeHistory.get_by_uuid(self.context, uuid)
|
||||||
|
|
||||||
|
mock_get.assert_called_once_with(uuid)
|
||||||
|
self.assertIsInstance(history, objects.NodeHistory)
|
||||||
|
self.assertEqual(self.context, history._context)
|
||||||
|
|
||||||
|
@mock.patch('ironic.objects.NodeHistory.get_by_uuid',
|
||||||
|
spec_set=types.FunctionType)
|
||||||
|
@mock.patch('ironic.objects.NodeHistory.get_by_id',
|
||||||
|
spec_set=types.FunctionType)
|
||||||
|
def test_get(self, mock_get_by_id, mock_get_by_uuid):
|
||||||
|
id_ = self.fake_history['id']
|
||||||
|
uuid = self.fake_history['uuid']
|
||||||
|
|
||||||
|
objects.NodeHistory.get(self.context, id_)
|
||||||
|
mock_get_by_id.assert_called_once_with(self.context, id_)
|
||||||
|
self.assertFalse(mock_get_by_uuid.called)
|
||||||
|
|
||||||
|
objects.NodeHistory.get(self.context, uuid)
|
||||||
|
mock_get_by_uuid.assert_called_once_with(self.context, uuid)
|
||||||
|
|
||||||
|
# Invalid identifier (not ID or UUID)
|
||||||
|
self.assertRaises(exception.InvalidIdentity,
|
||||||
|
objects.NodeHistory.get,
|
||||||
|
self.context, 'not-valid-identifier')
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
with mock.patch.object(self.dbapi, 'get_node_history_list',
|
||||||
|
autospec=True) as mock_get_list:
|
||||||
|
mock_get_list.return_value = [self.fake_history]
|
||||||
|
history = objects.NodeHistory.list(
|
||||||
|
self.context, limit=4, sort_key='uuid', sort_dir='asc')
|
||||||
|
|
||||||
|
mock_get_list.assert_called_once_with(
|
||||||
|
limit=4, marker=None, sort_key='uuid', sort_dir='asc')
|
||||||
|
self.assertThat(history, HasLength(1))
|
||||||
|
self.assertIsInstance(history[0], objects.NodeHistory)
|
||||||
|
self.assertEqual(self.context, history[0]._context)
|
||||||
|
|
||||||
|
def test_list_none(self):
|
||||||
|
with mock.patch.object(self.dbapi, 'get_node_history_list',
|
||||||
|
autospec=True) as mock_get_list:
|
||||||
|
mock_get_list.return_value = []
|
||||||
|
history = objects.NodeHistory.list(
|
||||||
|
self.context, limit=4, sort_key='uuid', sort_dir='asc')
|
||||||
|
|
||||||
|
mock_get_list.assert_called_once_with(
|
||||||
|
limit=4, marker=None, sort_key='uuid', sort_dir='asc')
|
||||||
|
self.assertEqual([], history)
|
||||||
|
|
||||||
|
def test_list_by_node_id(self):
|
||||||
|
with mock.patch.object(self.dbapi, 'get_node_history_by_node_id',
|
||||||
|
autospec=True) as mock_get_list_by_node_id:
|
||||||
|
mock_get_list_by_node_id.return_value = [self.fake_history]
|
||||||
|
node_id = self.fake_history['node_id']
|
||||||
|
history = objects.NodeHistory.list_by_node_id(
|
||||||
|
self.context, node_id, limit=10, sort_dir='desc')
|
||||||
|
|
||||||
|
mock_get_list_by_node_id.assert_called_once_with(
|
||||||
|
node_id, limit=10, marker=None, sort_key=None, sort_dir='desc')
|
||||||
|
self.assertThat(history, HasLength(1))
|
||||||
|
self.assertIsInstance(history[0], objects.NodeHistory)
|
||||||
|
self.assertEqual(self.context, history[0]._context)
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
with mock.patch.object(self.dbapi, 'create_node_history',
|
||||||
|
autospec=True) as mock_db_create:
|
||||||
|
mock_db_create.return_value = self.fake_history
|
||||||
|
new_history = objects.NodeHistory(
|
||||||
|
self.context, **self.fake_history)
|
||||||
|
new_history.create()
|
||||||
|
|
||||||
|
mock_db_create.assert_called_once_with(self.fake_history)
|
||||||
|
|
||||||
|
def test_destroy(self):
|
||||||
|
uuid = self.fake_history['uuid']
|
||||||
|
with mock.patch.object(self.dbapi, 'get_node_history_by_uuid',
|
||||||
|
autospec=True) as mock_get:
|
||||||
|
mock_get.return_value = self.fake_history
|
||||||
|
with mock.patch.object(self.dbapi, 'destroy_node_history_by_uuid',
|
||||||
|
autospec=True) as mock_db_destroy:
|
||||||
|
history = objects.NodeHistory.get_by_uuid(self.context, uuid)
|
||||||
|
history.destroy()
|
||||||
|
|
||||||
|
mock_db_destroy.assert_called_once_with(uuid)
|
@ -720,6 +720,7 @@ expected_object_fingerprints = {
|
|||||||
'DeployTemplateCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'DeployTemplateCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||||
'DeployTemplateCRUDPayload': '1.0-200857e7e715f58a5b6d6b700ab73a3b',
|
'DeployTemplateCRUDPayload': '1.0-200857e7e715f58a5b6d6b700ab73a3b',
|
||||||
'Deployment': '1.0-ff10ae028c5968f1596131d85d7f5f9d',
|
'Deployment': '1.0-ff10ae028c5968f1596131d85d7f5f9d',
|
||||||
|
'NodeHistory': '1.0-9b576c6481071e7f7eac97317fa29418',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user