summaryrefslogtreecommitdiff
path: root/ironic
diff options
context:
space:
mode:
authorKaifeng Wang <kaifeng.w@gmail.com>2020-12-20 21:16:15 +0800
committerJulia Kreger <juliaashleykreger@gmail.com>2021-09-09 09:35:09 -0700
commitfbaad948d870ffd18995f5494016798c8d3c9206 (patch)
tree617383da0c17b84194de5accba3f1340c84fa8c8 /ironic
parent8ea1a438d3f8715622798e01a709ea0dfab89f58 (diff)
downloadironic-fbaad948d870ffd18995f5494016798c8d3c9206.tar.gz
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
Diffstat (limited to 'ironic')
-rw-r--r--ironic/common/exception.py4
-rw-r--r--ironic/common/release_mappings.py1
-rw-r--r--ironic/db/api.py58
-rw-r--r--ironic/db/sqlalchemy/alembic/versions/9ef41f07cb58_add_node_history_table.py52
-rw-r--r--ironic/db/sqlalchemy/api.py54
-rw-r--r--ironic/db/sqlalchemy/models.py20
-rw-r--r--ironic/objects/__init__.py1
-rw-r--r--ironic/objects/node_history.py184
-rw-r--r--ironic/tests/unit/db/sqlalchemy/test_migrations.py30
-rw-r--r--ironic/tests/unit/db/test_node_history.py93
-rw-r--r--ironic/tests/unit/db/test_nodes.py9
-rw-r--r--ironic/tests/unit/db/utils.py31
-rw-r--r--ironic/tests/unit/objects/test_node_history.py133
-rw-r--r--ironic/tests/unit/objects/test_objects.py1
14 files changed, 671 insertions, 0 deletions
diff --git a/ironic/common/exception.py b/ironic/common/exception.py
index 75c7b02c5..c4c42aa93 100644
--- a/ironic/common/exception.py
+++ b/ironic/common/exception.py
@@ -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.")
diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py
index ee7634aab..cd86488df 100644
--- a/ironic/common/release_mappings.py
+++ b/ironic/common/release_mappings.py
@@ -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'],
diff --git a/ironic/db/api.py b/ironic/db/api.py
index 697654d38..5b71d32bc 100644
--- a/ironic/db/api.py
+++ b/ironic/db/api.py
@@ -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.
+ """
diff --git a/ironic/db/sqlalchemy/alembic/versions/9ef41f07cb58_add_node_history_table.py b/ironic/db/sqlalchemy/alembic/versions/9ef41f07cb58_add_node_history_table.py
new file mode 100644
index 000000000..9f5b855ed
--- /dev/null
+++ b/ironic/db/sqlalchemy/alembic/versions/9ef41f07cb58_add_node_history_table.py
@@ -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')
diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py
index 1de3add32..716c422dd 100644
--- a/ironic/db/sqlalchemy/api.py
+++ b/ironic/db/sqlalchemy/api.py
@@ -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)
diff --git a/ironic/db/sqlalchemy/models.py b/ironic/db/sqlalchemy/models.py
index 6a1c73d62..8f3f6a564 100644
--- a/ironic/db/sqlalchemy/models.py
+++ b/ironic/db/sqlalchemy/models.py
@@ -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.
diff --git a/ironic/objects/__init__.py b/ironic/objects/__init__.py
index 7f199c6aa..e8de08d5a 100644
--- a/ironic/objects/__init__.py
+++ b/ironic/objects/__init__.py
@@ -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')
diff --git a/ironic/objects/node_history.py b/ironic/objects/node_history.py
new file mode 100644
index 000000000..abccd5184
--- /dev/null
+++ b/ironic/objects/node_history.py
@@ -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()
diff --git a/ironic/tests/unit/db/sqlalchemy/test_migrations.py b/ironic/tests/unit/db/sqlalchemy/test_migrations.py
index 0381107e9..47b10eec8 100644
--- a/ironic/tests/unit/db/sqlalchemy/test_migrations.py
+++ b/ironic/tests/unit/db/sqlalchemy/test_migrations.py
@@ -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')
diff --git a/ironic/tests/unit/db/test_node_history.py b/ironic/tests/unit/db/test_node_history.py
new file mode 100644
index 000000000..9e554cd9c
--- /dev/null
+++ b/ironic/tests/unit/db/test_node_history.py
@@ -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)
diff --git a/ironic/tests/unit/db/test_nodes.py b/ironic/tests/unit/db/test_nodes.py
index 92e315eb1..eb5200f4e 100644
--- a/ironic/tests/unit/db/test_nodes.py
+++ b/ironic/tests/unit/db/test_nodes.py
@@ -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()
diff --git a/ironic/tests/unit/db/utils.py b/ironic/tests/unit/db/utils.py
index c0b060eef..0e60b1fa5 100644
--- a/ironic/tests/unit/db/utils.py
+++ b/ironic/tests/unit/db/utils.py
@@ -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)
diff --git a/ironic/tests/unit/objects/test_node_history.py b/ironic/tests/unit/objects/test_node_history.py
new file mode 100644
index 000000000..780fe7067
--- /dev/null
+++ b/ironic/tests/unit/objects/test_node_history.py
@@ -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)
diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py
index 7eefb3c59..7c58cf4aa 100644
--- a/ironic/tests/unit/objects/test_objects.py
+++ b/ironic/tests/unit/objects/test_objects.py
@@ -720,6 +720,7 @@ expected_object_fingerprints = {
'DeployTemplateCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'DeployTemplateCRUDPayload': '1.0-200857e7e715f58a5b6d6b700ab73a3b',
'Deployment': '1.0-ff10ae028c5968f1596131d85d7f5f9d',
+ 'NodeHistory': '1.0-9b576c6481071e7f7eac97317fa29418',
}