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:
Kaifeng Wang 2020-12-20 21:16:15 +08:00 committed by Julia Kreger
parent 8ea1a438d3
commit fbaad948d8
14 changed files with 671 additions and 0 deletions

View File

@ -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.")

View File

@ -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'],

View File

@ -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.
"""

View File

@ -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')

View File

@ -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)

View File

@ -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.

View File

@ -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')

View 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()

View File

@ -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')

View 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)

View File

@ -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()

View File

@ -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)

View 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)

View File

@ -720,6 +720,7 @@ expected_object_fingerprints = {
'DeployTemplateCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'DeployTemplateCRUDPayload': '1.0-200857e7e715f58a5b6d6b700ab73a3b',
'Deployment': '1.0-ff10ae028c5968f1596131d85d7f5f9d',
'NodeHistory': '1.0-9b576c6481071e7f7eac97317fa29418',
}