summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2021-09-21 12:56:17 +0000
committerGerrit Code Review <review@openstack.org>2021-09-21 12:56:17 +0000
commit33f09ad4d634d3940e2285be89e32ef926a83d31 (patch)
treec707baa9a4748ba694ce05b8860efffdf3ef3efa
parentd591ed76ad54c152949b883be2ce41eff3d31217 (diff)
parentfb9eae74124007ed1800e11ef9b8a4aac9e1a407 (diff)
downloadironic-33f09ad4d634d3940e2285be89e32ef926a83d31.tar.gz
Merge "API endpoints to get node history"
-rw-r--r--api-ref/source/baremetal-api-v1-nodes-history.inc76
-rw-r--r--api-ref/source/index.rst1
-rw-r--r--api-ref/source/parameters.yaml42
-rw-r--r--api-ref/source/samples/node-history-list-response.json16
-rw-r--r--doc/source/contributor/webapi-version-history.rst11
-rw-r--r--ironic/api/controllers/v1/node.py106
-rw-r--r--ironic/api/controllers/v1/utils.py5
-rw-r--r--ironic/api/controllers/v1/versions.py4
-rw-r--r--ironic/common/policy.py19
-rw-r--r--ironic/common/release_mappings.py2
-rw-r--r--ironic/db/sqlalchemy/api.py2
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_node.py129
-rw-r--r--ironic/tests/unit/api/test_acl.py12
-rw-r--r--ironic/tests/unit/api/test_rbac_legacy.yaml46
-rw-r--r--ironic/tests/unit/api/test_rbac_project_scoped.yaml92
-rw-r--r--ironic/tests/unit/api/test_rbac_system_scoped.yaml44
-rw-r--r--releasenotes/notes/add-node-event-history-99c6166607a90f3c.yaml8
17 files changed, 607 insertions, 8 deletions
diff --git a/api-ref/source/baremetal-api-v1-nodes-history.inc b/api-ref/source/baremetal-api-v1-nodes-history.inc
new file mode 100644
index 000000000..46cfa2ec4
--- /dev/null
+++ b/api-ref/source/baremetal-api-v1-nodes-history.inc
@@ -0,0 +1,76 @@
+.. -*- rst -*-
+
+================
+History of nodes
+================
+
+.. versionadded:: 1.78
+
+Identifying history of events from nodes is available via API version 1.78 via
+the ``v1/nodes/{node_ident}/history`` endpoint. In default policy
+configuration, only "System" scoped users as well as owners who are listed
+owners of associated nodes can list and retrieve nodes.
+
+List history entries for a node
+===============================
+
+.. rest_method:: GET /v1/nodes/{node_ident}/history
+
+Normal response code: 200
+
+Error codes: 400,401,403,404
+
+Request
+-------
+
+.. rest_parameters:: parameters.yaml
+
+ - node_ident: node_ident
+ - detail: detail
+ - marker: marker
+ - limit: limit
+
+Response
+--------
+
+.. rest_parameters:: parameters.yaml
+
+ - history: n_history
+
+**Example list of history events from a node:**
+
+.. literalinclude:: samples/node-history-list-response.json
+ :language: javascript
+
+Retrieve a specific history entry
+=================================
+
+.. rest_method:: GET /v1/nodes/{node_ident}/history/{history_event_uuid}
+
+Request
+-------
+
+.. rest_parameters:: parameters.yaml
+
+ - node_ident: node_ident
+ - history_event_uuid: history_event_ident
+
+Response
+--------
+
+.. rest_parameters:: parameters.yaml
+
+ - uuid: uuid
+ - created_at: created_at
+ - user: history_user_ident
+ - severity: history_severity
+ - event: history_event
+ - event_type: history_event_type
+ - conductor: hostname
+
+Deleting history entries for a node
+===================================
+
+Due to the nature of an immutable history record, records cannot be deleted
+via the REST API. Records and ultimately expired history records are managed
+by the conductor.
diff --git a/api-ref/source/index.rst b/api-ref/source/index.rst
index b8f19267a..50c6a6d14 100644
--- a/api-ref/source/index.rst
+++ b/api-ref/source/index.rst
@@ -27,6 +27,7 @@
.. include:: baremetal-api-v1-allocation.inc
.. include:: baremetal-api-v1-node-allocation.inc
.. include:: baremetal-api-v1-deploy-templates.inc
+.. include:: baremetal-api-v1-nodes-history.inc
.. NOTE(dtantsur): keep chassis close to the end since it's semi-deprecated
.. include:: baremetal-api-v1-chassis.inc
.. NOTE(dtantsur): keep misc last, since it covers internal API
diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml
index 266986c1b..b3eb28f82 100644
--- a/api-ref/source/parameters.yaml
+++ b/api-ref/source/parameters.yaml
@@ -74,6 +74,12 @@ driver_ident:
in: path
required: true
type: string
+history_event_ident:
+ description: |
+ The UUID of a history event.
+ in: path
+ required: true
+ type: string
hostname_ident:
description: |
The hostname of the conductor.
@@ -971,6 +977,36 @@ fault:
in: body
required: false
type: string
+history_event:
+ description: |
+ The event message body which has been logged related to the node for
+ this error.
+ in: body
+ required: true
+ type: string
+history_event_type:
+ description: |
+ Short descriptive string to indicate where the error occurred at to
+ enable API users/System Operators to be able to identify repeated
+ issues in a particular area of operation, such as 'deployment',
+ 'console', 'cleaning', 'monitoring'.
+ in: body
+ required: true
+ type: string
+history_severity:
+ description: |
+ Severity indicator for the event being returned. Typically this will
+ indicate if this was an Error or Informational entry.
+ in: body
+ required: true
+ type: string
+history_user_ident:
+ description: |
+ The UUID value representing the user whom appears to have caused
+ the recorded event.
+ in: body
+ required: true
+ type: string
hostname:
description: |
The hostname of this conductor.
@@ -1122,6 +1158,12 @@ n_description:
in: body
required: true
type: string
+n_history:
+ description: |
+ History events attached to this node.
+ in: body
+ required: true
+ type: array
n_ind_state:
description: |
The state of an indicator of the component of the node. Possible values
diff --git a/api-ref/source/samples/node-history-list-response.json b/api-ref/source/samples/node-history-list-response.json
new file mode 100644
index 000000000..632e6572b
--- /dev/null
+++ b/api-ref/source/samples/node-history-list-response.json
@@ -0,0 +1,16 @@
+{
+ "history": [
+ {
+ "uuid": "e5840e39-b4ba-4a93-8071-cff9aa2c9633",
+ "created_at": "2021-09-15T17:45:04.686541+00:00",
+ "severity": "ERROR",
+ "event": "Something is wrong",
+ "links": [
+ {
+ "href": "http://localhost/v1/nodes/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/history/e5840e39-b4ba-4a93-8071-cff9aa2c9633",
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+}
diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst
index c83cf2698..7176575f0 100644
--- a/doc/source/contributor/webapi-version-history.rst
+++ b/doc/source/contributor/webapi-version-history.rst
@@ -2,8 +2,17 @@
REST API Version History
========================
-1.77
+1.78 (Xena, ?)
----------------------
+Add endpoints to allow history events for nodes to be retrieved via
+the REST API.
+
+* ``GET /v1/nodes/{node_ident}/history/``
+* ``GET /v1/nodes/{node_ident}/history/{event_uuid}``
+
+1.77 (Xena, ?)
+----------------------
+
Add a fields selector to the the Drivers list:
* ``GET /v1/drivers?fields=``
Also add a fields selector to the the Driver detail:
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index bf3191efa..f3d017b6d 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -1840,6 +1840,107 @@ class NodeVIFController(rest.RestController):
vif_id=vif_id, topic=topic)
+class NodeHistoryController(rest.RestController):
+
+ detail_fields = ['uuid', 'created_at', 'severity', 'event_type',
+ 'event', 'conductor', 'user']
+
+ standard_fields = ['uuid', 'created_at', 'severity', 'event']
+
+ def __init__(self, node_ident):
+ super(NodeHistoryController).__init__()
+ self.node_ident = node_ident
+
+ def _history_event_convert_with_links(self, node_uuid, event,
+ detail=False):
+ """Add link and convert history event"""
+ url = api.request.public_url
+ if not detail:
+ fields = self.standard_fields
+ else:
+ fields = self.detail_fields
+
+ event_entry = api_utils.object_to_dict(
+ event,
+ link_resource='nodes',
+ fields=fields)
+ if not detail:
+ # The spec for this feature calls to truncate the event
+ # field if not detailed, which makes sense in some environments
+ # with many events, espescialy if the event text is particullarlly
+ # long.
+ entry_len = len(event_entry['event'])
+ if entry_len > 255:
+ event_entry['event'] = event_entry['event'][0:251] + '...'
+ else:
+ event_entry['event'] = event_entry['event'][0:entry_len]
+ # These records cannot be changed by the API consumer,
+ # and updated_at gets handed up from the db model
+ # regardless if we want it or not. As such, strip from
+ # the reply.
+ event_entry.pop('updated_at')
+ event_entry['links'] = [
+ link.make_link(
+ 'self', url,
+ 'nodes',
+ '%s/history/%s' % (node_uuid, event.uuid)
+ )
+ ]
+ return event_entry
+
+ @METRICS.timer('NodeHistoryController.get_all')
+ @method.expose()
+ @args.validate(details=args.boolean, marker=args.uuid, limit=args.integer)
+ def get_all(self, **kwargs):
+ """List node history."""
+ node = api_utils.check_node_policy_and_retrieve(
+ 'baremetal:node:history:get', self.node_ident)
+
+ if kwargs.get('detail'):
+ detail = True
+ fields = self.detail_fields
+ else:
+ detail = False
+ fields = self.standard_fields
+
+ marker_obj = None
+ marker = kwargs.get('marker')
+ if marker:
+ marker_obj = objects.NodeHistory.get_by_uuid(api.request.context,
+ marker)
+ limit = kwargs.get('limit')
+
+ events = objects.NodeHistory.list_by_node_id(api.request.context,
+ node.id,
+ marker=marker_obj,
+ limit=limit)
+
+ return collection.list_convert_with_links(
+ items=[
+ self._history_event_convert_with_links(
+ node.uuid, event, detail=detail) for event in events
+ ],
+ item_name='history',
+ fields=fields,
+ marker=marker_obj,
+ limit=limit,
+ )
+
+ @METRICS.timer('NodeHistoryController.get_one')
+ @method.expose()
+ @args.validate(event=args.uuid_or_name)
+ def get_one(self, event):
+ """Get a node history entry"""
+ node = api_utils.check_node_policy_and_retrieve(
+ 'baremetal:node:history:get', self.node_ident)
+ # TODO(TheJulia): Need to check policy to make sure if policy
+ # check fails, that the entry cannot be found.
+ event = objects.NodeHistory.get_by_uuid(api.request.context,
+ event)
+ return self._history_event_convert_with_links(
+ node.uuid, event, detail=True)
+
+
class NodesController(rest.RestController):
"""REST controller for Nodes."""
@@ -1885,6 +1986,7 @@ class NodesController(rest.RestController):
'traits': NodeTraitsController,
'bios': bios.NodeBiosController,
'allocation': allocation.NodeAllocationController,
+ 'history': NodeHistoryController,
}
@pecan.expose()
@@ -1906,7 +2008,9 @@ class NodesController(rest.RestController):
or (remainder[0] == 'bios'
and not api_utils.allow_bios_interface())
or (remainder[0] == 'allocation'
- and not api_utils.allow_allocations())):
+ and not api_utils.allow_allocations())
+ or (remainder[0] == 'history'
+ and not api_utils.allow_node_history())):
pecan.abort(http_client.NOT_FOUND)
if remainder[0] == 'traits' and not api_utils.allow_traits():
# NOTE(mgoddard): Returning here will ensure we exhibit the
diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py
index 62a57da6c..04525ff65 100644
--- a/ironic/api/controllers/v1/utils.py
+++ b/ironic/api/controllers/v1/utils.py
@@ -1334,6 +1334,11 @@ def allow_reset_interfaces():
return api.request.version.minor >= versions.MINOR_45_RESET_INTERFACES
+def allow_node_history():
+ """Check if node history access is permitted by API version."""
+ return api.request.version.minor >= versions.MINOR_78_NODE_HISTORY
+
+
def get_request_return_fields(fields, detail, default_fields,
check_detail_version=allow_detail_query,
check_fields_version=None):
diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py
index 0489b1dbe..14330a734 100644
--- a/ironic/api/controllers/v1/versions.py
+++ b/ironic/api/controllers/v1/versions.py
@@ -115,6 +115,7 @@ BASE_VERSION = 1
# v1.75: Add boot_mode, secure_boot fields to node object.
# v1.76: Add support for changing boot_mode and secure_boot state
# v1.77: Add fields selector to drivers list and driver detail.
+# v1.78: Add node history endpoint
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@@ -194,6 +195,7 @@ MINOR_74_BIOS_REGISTRY = 74
MINOR_75_NODE_BOOT_MODE = 75
MINOR_76_NODE_CHANGE_BOOT_MODE = 76
MINOR_77_DRIVER_FIELDS_SELECTOR = 77
+MINOR_78_NODE_HISTORY = 78
# When adding another version, update:
# - MINOR_MAX_VERSION
@@ -201,7 +203,7 @@ MINOR_77_DRIVER_FIELDS_SELECTOR = 77
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
-MINOR_MAX_VERSION = MINOR_77_DRIVER_FIELDS_SELECTOR
+MINOR_MAX_VERSION = MINOR_78_NODE_HISTORY
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
diff --git a/ironic/common/policy.py b/ironic/common/policy.py
index 29040c32b..94378fb07 100644
--- a/ironic/common/policy.py
+++ b/ironic/common/policy.py
@@ -420,7 +420,6 @@ deprecated_bios_disable_cleaning = policy.DeprecatedRule(
deprecated_since=versionutils.deprecated.WALLABY
)
-
node_policies = [
policy.DocumentedRuleDefault(
name='baremetal:node:create',
@@ -911,6 +910,24 @@ node_policies = [
],
deprecated_rule=deprecated_bios_disable_cleaning
),
+ policy.DocumentedRuleDefault(
+ name='baremetal:node:history:get',
+ check_str=SYSTEM_OR_OWNER_READER,
+ scope_types=['system', 'project'],
+ description='Filter to allow operators to retreive history records '
+ 'for a node.',
+ operations=[
+ {'path': '/nodes/{node_ident}/history', 'method': 'GET'},
+ {'path': '/nodes/{node_ident}/history/{event_ident}',
+ 'method': 'GET'}
+ ],
+ # This rule fallsback to deprecated_node_get in order to provide a
+ # mechanism so the additional policies only engage in an updated
+ # operating context.
+ deprecated_rule=deprecated_node_get
+ ),
+
+
]
deprecated_port_reason = """
diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py
index 2baad9af3..0a67cdd5d 100644
--- a/ironic/common/release_mappings.py
+++ b/ironic/common/release_mappings.py
@@ -371,7 +371,7 @@ RELEASE_MAPPING = {
}
},
'master': {
- 'api': '1.77',
+ 'api': '1.78',
'rpc': '1.55',
'objects': {
'Allocation': ['1.1'],
diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py
index 8f654483f..cb72e1b49 100644
--- a/ironic/db/sqlalchemy/api.py
+++ b/ironic/db/sqlalchemy/api.py
@@ -2319,7 +2319,7 @@ class Connection(api.Connection):
raise exception.NodeHistoryNotFound(history=history_uuid)
def get_node_history_list(self, limit=None, marker=None,
- sort_key=None, sort_dir=None):
+ sort_key='created_at', sort_dir='asc'):
return _paginate_query(models.NodeHistory, limit, marker, sort_key,
sort_dir)
diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py
index 9b37a542f..61eebc6da 100644
--- a/ironic/tests/unit/api/controllers/v1/test_node.py
+++ b/ironic/tests/unit/api/controllers/v1/test_node.py
@@ -7720,3 +7720,132 @@ class TestTraits(test_api_base.BaseApiTest):
headers={api_base.Version.string: "1.36"},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
+
+
+class TestNodeHistory(test_api_base.BaseApiTest):
+
+ def setUp(self):
+ super(TestNodeHistory, self).setUp()
+ self.version = "1.78"
+ self.node = obj_utils.create_test_node(
+ self.context,
+ provision_state=states.AVAILABLE, name='node-54')
+ self.node.save()
+ self.node.obj_reset_changes()
+
+ def _add_history_entries(self):
+ self.event1 = objects.NodeHistory(node_id=self.node.id, event='meow',
+ conductor='cat-tree1',
+ user='peaches')
+ self.event1.create()
+ self.event2 = objects.NodeHistory(node_id=self.node.id, event='purr',
+ conductor='cat-tree2',
+ user='sage')
+ self.event2.create()
+ self.event3 = objects.NodeHistory(node_id=self.node.id,
+ event='g' + 'rrrr' * 64 + '!',
+ conductor='cat-tree3',
+ user='bella')
+ self.event3.create()
+
+ def test_get_all_history(self):
+ ret = self.get_json('/nodes/%s/history' % self.node.uuid,
+ headers={api_base.Version.string: self.version})
+ self.assertEqual({'history': []}, ret)
+
+ def test_get_all_old_version(self):
+ ret = self.get_json('/nodes/%s/history' % self.node.uuid,
+ headers={api_base.Version.string: "1.77"},
+ expect_errors=True)
+ self.assertEqual(http_client.NOT_FOUND, ret.status_code)
+
+ def test_get_all_history_returns_entries(self):
+ self._add_history_entries()
+ ret = self.get_json('/nodes/%s/history' % self.node.uuid,
+ headers={api_base.Version.string: self.version})
+ self.assertIn('history', ret)
+ entries = ret['history']
+ self.assertEqual(3, len(entries))
+ self.assertEqual('meow', entries[0]['event'])
+ self.assertEqual('purr', entries[1]['event'])
+ self.assertIn('grr', entries[2]['event'])
+ self.assertNotIn('r!', entries[2]['event'])
+ self.assertIn('...', entries[2]['event'])
+ for entry in [0, 1, 2]:
+ for field in ['conductor', 'user']:
+ self.assertNotIn(field, entries[entry])
+ self.assertIn('severity', entries[entry])
+
+ def test_get_all_history_returns_detail(self):
+ self._add_history_entries()
+ ret = self.get_json('/nodes/%s/history?detail=true' % self.node.uuid,
+ headers={api_base.Version.string: self.version})
+ self.assertIn('history', ret)
+ entries = ret['history']
+ self.assertEqual(3, len(entries))
+ self.assertEqual('meow', entries[0]['event'])
+ self.assertEqual('purr', entries[1]['event'])
+ self.assertIn('grr', entries[2]['event'])
+ self.assertIn('r!', entries[2]['event'])
+ for entry in [0, 1, 2]:
+ for field in ['conductor', 'user', 'severity', 'event_type']:
+ self.assertIn(field, entries[entry])
+
+ def test_get_history_item(self):
+ self._add_history_entries()
+ record = self.get_json('/nodes/%s/history/%s' % (self.node.uuid,
+ self.event1.uuid),
+ headers={api_base.Version.string: self.version})
+ self.assertEqual(8, len(record))
+ expected_keys = ['created_at', 'links', 'event',
+ 'event_type', 'severity', 'user', 'uuid']
+ for key in expected_keys:
+ self.assertIn(key, record)
+ self.assertNotIn('updated_at', record)
+ self.assertEqual('cat-tree1', record['conductor'])
+ self.assertEqual('meow', record['event'])
+ self.assertEqual('peaches', record['user'])
+ self.assertEqual(self.event1.uuid, record['uuid'])
+
+ def test_get_history_item_not_found(self):
+ self._add_history_entries()
+ ret = self.get_json('/nodes/%s/history/52949728-59fc-'
+ '4651-84c8-b0a16b469372' % self.node.uuid,
+ headers={api_base.Version.string: self.version},
+ expect_errors=True)
+ self.assertEqual(http_client.NOT_FOUND, ret.status_code)
+
+ def test_get_history_item_old_version(self):
+ ret = self.get_json('/nodes/%s/history/1234' % self.node.uuid,
+ headers={api_base.Version.string: "1.77"},
+ expect_errors=True)
+ self.assertEqual(http_client.NOT_FOUND, ret.status_code)
+
+ def test_get_all_pagination(self):
+ self._add_history_entries()
+ # First request, initial request with a limit of 1.
+ ret = self.get_json('/nodes/%s/history?limit=1' % self.node.uuid,
+ headers={api_base.Version.string: self.version})
+ self.assertIn('history', ret)
+ entries = ret['history']
+ self.assertEqual(1, len(entries))
+ result_uuid = entries[0]['uuid']
+ self.assertEqual(self.event1.uuid, result_uuid)
+ # Second request
+ ret = self.get_json('/nodes/%s/history?limit=1&marker=%s' %
+ (self.node.uuid, result_uuid),
+ headers={api_base.Version.string: self.version})
+ self.assertIn('history', ret)
+ entries = ret['history']
+ self.assertEqual(1, len(entries))
+ result_uuid = entries[0]['uuid']
+ self.assertEqual(self.event2.uuid, result_uuid)
+ # Third request
+ ret = self.get_json('/nodes/%s/history?limit=1&marker=%s' %
+ (self.node.uuid, result_uuid),
+ headers={api_base.Version.string: self.version})
+ self.assertIn('history', ret)
+ entries = ret['history']
+ self.assertEqual(1, len(entries))
+ result_uuid = entries[0]['uuid']
+ self.assertEqual(self.event3.uuid, result_uuid)
diff --git a/ironic/tests/unit/api/test_acl.py b/ironic/tests/unit/api/test_acl.py
index 508b18c3d..99921e988 100644
--- a/ironic/tests/unit/api/test_acl.py
+++ b/ironic/tests/unit/api/test_acl.py
@@ -275,7 +275,7 @@ class TestRBACModelBeforeScopesBase(TestACLBase):
value=fake_setting)
db_utils.create_test_node_trait(
node_id=fake_db_node['id'])
-
+ fake_history = db_utils.create_test_history(node_id=fake_db_node.id)
# dedicated node for portgroup addition test to avoid
# false positives with test runners.
db_utils.create_test_node(
@@ -298,6 +298,7 @@ class TestRBACModelBeforeScopesBase(TestACLBase):
'trait': fake_trait,
'volume_target_ident': fake_db_volume_target['uuid'],
'volume_connector_ident': fake_db_volume_connector['uuid'],
+ 'history_ident': fake_history['uuid'],
})
@@ -402,6 +403,8 @@ class TestRBACProjectScoped(TestACLBase):
node_id=owned_node['id'],
owner=owner_project_id,
resource_class="CUSTOM_TEST")
+ owned_node_history = db_utils.create_test_history(
+ node_id=owned_node.id)
# Leased nodes
fake_allocation_id = 61
@@ -428,6 +431,9 @@ class TestRBACProjectScoped(TestACLBase):
owner=lessee_project_id,
resource_class="CUSTOM_LEASED")
+ leased_node_history = db_utils.create_test_history(
+ node_id=leased_node.id)
+
# Random objects that shouldn't be project visible
other_port = db_utils.create_test_port(
uuid='abfd8dbb-1732-449a-b760-2224035c6b99',
@@ -460,7 +466,9 @@ class TestRBACProjectScoped(TestACLBase):
'other_portgroup_ident': other_pgroup['uuid'],
'driver_name': 'fake-driverz',
'owner_allocation': fake_owner_allocation['uuid'],
- 'lessee_allocation': fake_leased_allocation['uuid']})
+ 'lessee_allocation': fake_leased_allocation['uuid'],
+ 'owned_history_ident': owned_node_history['uuid'],
+ 'lessee_history_ident': leased_node_history['uuid']})
@ddt.file_data('test_rbac_project_scoped.yaml')
@ddt.unpack
diff --git a/ironic/tests/unit/api/test_rbac_legacy.yaml b/ironic/tests/unit/api/test_rbac_legacy.yaml
index a665d15fc..4185b5bda 100644
--- a/ironic/tests/unit/api/test_rbac_legacy.yaml
+++ b/ironic/tests/unit/api/test_rbac_legacy.yaml
@@ -2349,3 +2349,49 @@ chassis_chassis_id_delete_observer:
headers: *observer_headers
assert_status: 403
deprecated: true
+
+node_history_get_admin:
+ path: '/v1/nodes/{node_ident}/history'
+ method: get
+ headers: *admin_headers
+ assert_status: 200
+ deprecated: true
+ assert_list_length:
+ history: 1
+
+node_history_get_member:
+ path: '/v1/nodes/{node_ident}/history'
+ method: get
+ headers: *member_headers
+ assert_status: 404
+ deprecated: true
+
+node_history_get_observer:
+ path: '/v1/nodes/{node_ident}/history'
+ method: get
+ headers: *observer_headers
+ assert_status: 200
+ deprecated: true
+ assert_list_length:
+ history: 1
+
+node_history_get_entry_admin:
+ path: '/v1/nodes/{node_ident}/history/{history_ident}'
+ method: get
+ headers: *admin_headers
+ assert_status: 200
+ deprecated: true
+
+node_history_get_entry_member:
+ path: '/v1/nodes/{node_ident}/history/{history_ident}'
+ method: get
+ headers: *member_headers
+ assert_status: 404
+ deprecated: true
+
+node_history_get_entry_observer:
+ path: '/v1/nodes/{node_ident}/history/{history_ident}'
+ method: get
+ headers: *observer_headers
+ assert_status: 200
+ deprecated: true
diff --git a/ironic/tests/unit/api/test_rbac_project_scoped.yaml b/ironic/tests/unit/api/test_rbac_project_scoped.yaml
index 81e7f646f..f7f4cbed0 100644
--- a/ironic/tests/unit/api/test_rbac_project_scoped.yaml
+++ b/ironic/tests/unit/api/test_rbac_project_scoped.yaml
@@ -2629,3 +2629,95 @@ third_party_admin_cannot_create_chassis:
body:
description: 'test-chassis'
assert_status: 500
+
+# Node history entries
+
+node_history_get_admin:
+ path: '/v1/nodes/{owner_node_ident}/history'
+ method: get
+ headers: *owner_admin_headers
+ assert_status: 200
+ assert_list_length:
+ history: 1
+
+node_history_get_member:
+ path: '/v1/nodes/{owner_node_ident}/history'
+ method: get
+ headers: *owner_member_headers
+ assert_status: 200
+ assert_list_length:
+ history: 1
+
+node_history_get_reader:
+ path: '/v1/nodes/{owner_node_ident}/history'
+ method: get
+ headers: *owner_reader_headers
+ assert_status: 200
+ assert_list_length:
+ history: 1
+
+node_history_get_entry_admin:
+ path: '/v1/nodes/{owner_node_ident}/history/{owned_history_ident}'
+ method: get
+ headers: *owner_admin_headers
+ assert_status: 200
+
+node_history_get_entry_member:
+ path: '/v1/nodes/{owner_node_ident}/history/{owned_history_ident}'
+ method: get
+ headers: *owner_member_headers
+ assert_status: 200
+
+node_history_get_entry_reader:
+ path: '/v1/nodes/{owner_node_ident}/history/{owned_history_ident}'
+ method: get
+ headers: *owner_reader_headers
+ assert_status: 200
+
+lessee_node_history_get_admin:
+ path: '/v1/nodes/{node_ident}/history'
+ method: get
+ headers: *lessee_admin_headers
+ assert_status: 404
+
+lessee_node_history_get_member:
+ path: '/v1/nodes/{node_ident}/history'
+ method: get
+ headers: *lessee_member_headers
+ assert_status: 404
+
+lessee_node_history_get_reader:
+ path: '/v1/nodes/{node_ident}/history'
+ method: get
+ headers: *lessee_reader_headers
+ assert_status: 404
+
+lessee_node_history_get_entry_admin:
+ path: '/v1/nodes/{node_ident}/history/{lessee_history_ident}'
+ method: get
+ headers: *lessee_admin_headers
+ assert_status: 404
+
+lessee_history_get_entry_member:
+ path: '/v1/nodes/{node_ident}/history/{lessee_history_ident}'
+ method: get
+ headers: *lessee_member_headers
+ assert_status: 404
+
+lessee_node_history_get_entry_reader:
+ path: '/v1/nodes/{node_ident}/history/{lessee_history_ident}'
+ method: get
+ headers: *lessee_reader_headers
+ assert_status: 404
+
+third_party_admin_cannot_get_node_history:
+ path: '/v1/nodes/{owner_node_ident}'
+ method: get
+ headers: *third_party_admin_headers
+ assert_status: 404
+
+node_history_get_entry_admin:
+ path: '/v1/nodes/{owner_node_ident}/history/{owned_history_ident}'
+ method: get
+ headers: *third_party_admin_headers
+ assert_status: 404
diff --git a/ironic/tests/unit/api/test_rbac_system_scoped.yaml b/ironic/tests/unit/api/test_rbac_system_scoped.yaml
index 032001fb2..d74a5fcae 100644
--- a/ironic/tests/unit/api/test_rbac_system_scoped.yaml
+++ b/ironic/tests/unit/api/test_rbac_system_scoped.yaml
@@ -2079,3 +2079,47 @@ chassis_chassis_id_delete_reader:
method: delete
headers: *reader_headers
assert_status: 403
+
+# Node history entries
+
+node_history_get_admin:
+ path: '/v1/nodes/{node_ident}/history'
+ method: get
+ headers: *admin_headers
+ assert_status: 200
+ assert_list_length:
+ history: 1
+
+node_history_get_member:
+ path: '/v1/nodes/{node_ident}/history'
+ method: get
+ headers: *scoped_member_headers
+ assert_status: 200
+ assert_list_length:
+ history: 1
+
+node_history_get_reader:
+ path: '/v1/nodes/{node_ident}/history'
+ method: get
+ headers: *reader_headers
+ assert_status: 200
+ assert_list_length:
+ history: 1
+
+node_history_get_entry_admin:
+ path: '/v1/nodes/{node_ident}/history/{history_ident}'
+ method: get
+ headers: *admin_headers
+ assert_status: 200
+
+node_history_get_entry_member:
+ path: '/v1/nodes/{node_ident}/history/{history_ident}'
+ method: get
+ headers: *scoped_member_headers
+ assert_status: 200
+
+node_history_get_entry_reader:
+ path: '/v1/nodes/{node_ident}/history/{history_ident}'
+ method: get
+ headers: *reader_headers
+ assert_status: 200
diff --git a/releasenotes/notes/add-node-event-history-99c6166607a90f3c.yaml b/releasenotes/notes/add-node-event-history-99c6166607a90f3c.yaml
new file mode 100644
index 000000000..36f88bfce
--- /dev/null
+++ b/releasenotes/notes/add-node-event-history-99c6166607a90f3c.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Adds API version ``1.78`` which provides the capability to retrieve
+ node history events which may have been recorded in the process of
+ management of the node, which may be aid in troubleshooting or identifying
+ a problem area with a specific node or configuration which has been
+ supplied.