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):
|
||||
_msg_fmt = _("Available memory at %(free)s, Insufficent as %(required)s "
|
||||
"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'],
|
||||
'BIOSSetting': ['1.1'],
|
||||
'Node': ['1.36', '1.35'],
|
||||
'NodeHistory': ['1.0'],
|
||||
'Conductor': ['1.3'],
|
||||
'Chassis': ['1.3'],
|
||||
'Deployment': ['1.0'],
|
||||
|
@ -1322,3 +1322,61 @@ class Connection(object, metaclass=abc.ABCMeta):
|
||||
:param names: List of names to filter by.
|
||||
: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)
|
||||
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()
|
||||
|
||||
def update_node(self, node_id, values):
|
||||
@ -2275,3 +2280,52 @@ class Connection(api.Connection):
|
||||
query = (_get_deploy_template_query_with_steps()
|
||||
.filter(models.DeployTemplate.name.in_(names)))
|
||||
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):
|
||||
"""Returns the model class with the specified name.
|
||||
|
||||
|
@ -31,6 +31,7 @@ def register_all():
|
||||
__import__('ironic.objects.deploy_template')
|
||||
__import__('ironic.objects.deployment')
|
||||
__import__('ironic.objects.node')
|
||||
__import__('ironic.objects.node_history')
|
||||
__import__('ironic.objects.port')
|
||||
__import__('ironic.objects.portgroup')
|
||||
__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]
|
||||
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):
|
||||
with patch_with_engine(self.engine):
|
||||
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.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):
|
||||
node = utils.create_test_node()
|
||||
|
||||
|
@ -27,6 +27,7 @@ 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 node_history
|
||||
from ironic.objects import port
|
||||
from ironic.objects import portgroup
|
||||
from ironic.objects import trait
|
||||
@ -690,3 +691,33 @@ def get_test_ibmc_info():
|
||||
"ibmc_password": "password",
|
||||
"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',
|
||||
'DeployTemplateCRUDPayload': '1.0-200857e7e715f58a5b6d6b700ab73a3b',
|
||||
'Deployment': '1.0-ff10ae028c5968f1596131d85d7f5f9d',
|
||||
'NodeHistory': '1.0-9b576c6481071e7f7eac97317fa29418',
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user