From fb9eae74124007ed1800e11ef9b8a4aac9e1a407 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Sat, 7 Aug 2021 21:31:51 -0700 Subject: [PATCH] API endpoints to get node history Adds API for retrieving node history events via a node. Includes pagination and limitation of the response set. Story: 2002980 Tas: 42961 Change-Id: I22a92fa6c30d721f6a5dd0670b2e0a9cf76ad7b1 --- .../source/baremetal-api-v1-nodes-history.inc | 76 +++++++++++ api-ref/source/index.rst | 1 + api-ref/source/parameters.yaml | 42 ++++++ .../samples/node-history-list-response.json | 16 +++ .../contributor/webapi-version-history.rst | 11 +- ironic/api/controllers/v1/node.py | 106 +++++++++++++- ironic/api/controllers/v1/utils.py | 5 + ironic/api/controllers/v1/versions.py | 4 +- ironic/common/policy.py | 19 ++- ironic/common/release_mappings.py | 2 +- ironic/db/sqlalchemy/api.py | 2 +- .../unit/api/controllers/v1/test_node.py | 129 ++++++++++++++++++ ironic/tests/unit/api/test_acl.py | 12 +- ironic/tests/unit/api/test_rbac_legacy.yaml | 46 +++++++ .../unit/api/test_rbac_project_scoped.yaml | 92 +++++++++++++ .../unit/api/test_rbac_system_scoped.yaml | 44 ++++++ ...d-node-event-history-99c6166607a90f3c.yaml | 8 ++ 17 files changed, 607 insertions(+), 8 deletions(-) create mode 100644 api-ref/source/baremetal-api-v1-nodes-history.inc create mode 100644 api-ref/source/samples/node-history-list-response.json create mode 100644 releasenotes/notes/add-node-event-history-99c6166607a90f3c.yaml 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 0000000000..46cfa2ec44 --- /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 b8f19267ac..50c6a6d14f 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 266986c1b9..b3eb28f828 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 0000000000..632e6572bd --- /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 c83cf2698c..7176575f06 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 bf3191efa9..f3d017b6dc 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 62a57da6c2..04525ff651 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 0489b1dbe4..14330a7340 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 29040c32bd..94378fb07b 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 2baad9af34..0a67cdd5da 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 8f654483f4..cb72e1b496 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 9b37a542f5..61eebc6dad 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 508b18c3d0..99921e9884 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 a665d15fc2..4185b5bda8 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 81e7f646f4..f7f4cbed09 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 032001fb21..d74a5fcaec 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 0000000000..36f88bfce8 --- /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.