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
This commit is contained in:
parent
20503d94e5
commit
fb9eae7412
76
api-ref/source/baremetal-api-v1-nodes-history.inc
Normal file
76
api-ref/source/baremetal-api-v1-nodes-history.inc
Normal file
@ -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.
|
@ -27,6 +27,7 @@
|
|||||||
.. include:: baremetal-api-v1-allocation.inc
|
.. include:: baremetal-api-v1-allocation.inc
|
||||||
.. include:: baremetal-api-v1-node-allocation.inc
|
.. include:: baremetal-api-v1-node-allocation.inc
|
||||||
.. include:: baremetal-api-v1-deploy-templates.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
|
.. NOTE(dtantsur): keep chassis close to the end since it's semi-deprecated
|
||||||
.. include:: baremetal-api-v1-chassis.inc
|
.. include:: baremetal-api-v1-chassis.inc
|
||||||
.. NOTE(dtantsur): keep misc last, since it covers internal API
|
.. NOTE(dtantsur): keep misc last, since it covers internal API
|
||||||
|
@ -74,6 +74,12 @@ driver_ident:
|
|||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
history_event_ident:
|
||||||
|
description: |
|
||||||
|
The UUID of a history event.
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
hostname_ident:
|
hostname_ident:
|
||||||
description: |
|
description: |
|
||||||
The hostname of the conductor.
|
The hostname of the conductor.
|
||||||
@ -971,6 +977,36 @@ fault:
|
|||||||
in: body
|
in: body
|
||||||
required: false
|
required: false
|
||||||
type: string
|
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:
|
hostname:
|
||||||
description: |
|
description: |
|
||||||
The hostname of this conductor.
|
The hostname of this conductor.
|
||||||
@ -1122,6 +1158,12 @@ n_description:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
n_history:
|
||||||
|
description: |
|
||||||
|
History events attached to this node.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: array
|
||||||
n_ind_state:
|
n_ind_state:
|
||||||
description: |
|
description: |
|
||||||
The state of an indicator of the component of the node. Possible values
|
The state of an indicator of the component of the node. Possible values
|
||||||
|
16
api-ref/source/samples/node-history-list-response.json
Normal file
16
api-ref/source/samples/node-history-list-response.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -2,8 +2,17 @@
|
|||||||
REST API Version History
|
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:
|
Add a fields selector to the the Drivers list:
|
||||||
* ``GET /v1/drivers?fields=``
|
* ``GET /v1/drivers?fields=``
|
||||||
Also add a fields selector to the the Driver detail:
|
Also add a fields selector to the the Driver detail:
|
||||||
|
@ -1840,6 +1840,107 @@ class NodeVIFController(rest.RestController):
|
|||||||
vif_id=vif_id, topic=topic)
|
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):
|
class NodesController(rest.RestController):
|
||||||
"""REST controller for Nodes."""
|
"""REST controller for Nodes."""
|
||||||
|
|
||||||
@ -1885,6 +1986,7 @@ class NodesController(rest.RestController):
|
|||||||
'traits': NodeTraitsController,
|
'traits': NodeTraitsController,
|
||||||
'bios': bios.NodeBiosController,
|
'bios': bios.NodeBiosController,
|
||||||
'allocation': allocation.NodeAllocationController,
|
'allocation': allocation.NodeAllocationController,
|
||||||
|
'history': NodeHistoryController,
|
||||||
}
|
}
|
||||||
|
|
||||||
@pecan.expose()
|
@pecan.expose()
|
||||||
@ -1906,7 +2008,9 @@ class NodesController(rest.RestController):
|
|||||||
or (remainder[0] == 'bios'
|
or (remainder[0] == 'bios'
|
||||||
and not api_utils.allow_bios_interface())
|
and not api_utils.allow_bios_interface())
|
||||||
or (remainder[0] == 'allocation'
|
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)
|
pecan.abort(http_client.NOT_FOUND)
|
||||||
if remainder[0] == 'traits' and not api_utils.allow_traits():
|
if remainder[0] == 'traits' and not api_utils.allow_traits():
|
||||||
# NOTE(mgoddard): Returning here will ensure we exhibit the
|
# NOTE(mgoddard): Returning here will ensure we exhibit the
|
||||||
|
@ -1334,6 +1334,11 @@ def allow_reset_interfaces():
|
|||||||
return api.request.version.minor >= versions.MINOR_45_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,
|
def get_request_return_fields(fields, detail, default_fields,
|
||||||
check_detail_version=allow_detail_query,
|
check_detail_version=allow_detail_query,
|
||||||
check_fields_version=None):
|
check_fields_version=None):
|
||||||
|
@ -115,6 +115,7 @@ BASE_VERSION = 1
|
|||||||
# v1.75: Add boot_mode, secure_boot fields to node object.
|
# v1.75: Add boot_mode, secure_boot fields to node object.
|
||||||
# v1.76: Add support for changing boot_mode and secure_boot state
|
# v1.76: Add support for changing boot_mode and secure_boot state
|
||||||
# v1.77: Add fields selector to drivers list and driver detail.
|
# v1.77: Add fields selector to drivers list and driver detail.
|
||||||
|
# v1.78: Add node history endpoint
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -194,6 +195,7 @@ MINOR_74_BIOS_REGISTRY = 74
|
|||||||
MINOR_75_NODE_BOOT_MODE = 75
|
MINOR_75_NODE_BOOT_MODE = 75
|
||||||
MINOR_76_NODE_CHANGE_BOOT_MODE = 76
|
MINOR_76_NODE_CHANGE_BOOT_MODE = 76
|
||||||
MINOR_77_DRIVER_FIELDS_SELECTOR = 77
|
MINOR_77_DRIVER_FIELDS_SELECTOR = 77
|
||||||
|
MINOR_78_NODE_HISTORY = 78
|
||||||
|
|
||||||
# When adding another version, update:
|
# When adding another version, update:
|
||||||
# - MINOR_MAX_VERSION
|
# - MINOR_MAX_VERSION
|
||||||
@ -201,7 +203,7 @@ MINOR_77_DRIVER_FIELDS_SELECTOR = 77
|
|||||||
# explanation of what changed in the new version
|
# explanation of what changed in the new version
|
||||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
# - 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
|
# String representations of the minor and maximum versions
|
||||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||||
|
@ -420,7 +420,6 @@ deprecated_bios_disable_cleaning = policy.DeprecatedRule(
|
|||||||
deprecated_since=versionutils.deprecated.WALLABY
|
deprecated_since=versionutils.deprecated.WALLABY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
node_policies = [
|
node_policies = [
|
||||||
policy.DocumentedRuleDefault(
|
policy.DocumentedRuleDefault(
|
||||||
name='baremetal:node:create',
|
name='baremetal:node:create',
|
||||||
@ -911,6 +910,24 @@ node_policies = [
|
|||||||
],
|
],
|
||||||
deprecated_rule=deprecated_bios_disable_cleaning
|
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 = """
|
deprecated_port_reason = """
|
||||||
|
@ -371,7 +371,7 @@ RELEASE_MAPPING = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'master': {
|
'master': {
|
||||||
'api': '1.77',
|
'api': '1.78',
|
||||||
'rpc': '1.55',
|
'rpc': '1.55',
|
||||||
'objects': {
|
'objects': {
|
||||||
'Allocation': ['1.1'],
|
'Allocation': ['1.1'],
|
||||||
|
@ -2319,7 +2319,7 @@ class Connection(api.Connection):
|
|||||||
raise exception.NodeHistoryNotFound(history=history_uuid)
|
raise exception.NodeHistoryNotFound(history=history_uuid)
|
||||||
|
|
||||||
def get_node_history_list(self, limit=None, marker=None,
|
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,
|
return _paginate_query(models.NodeHistory, limit, marker, sort_key,
|
||||||
sort_dir)
|
sort_dir)
|
||||||
|
|
||||||
|
@ -7720,3 +7720,132 @@ class TestTraits(test_api_base.BaseApiTest):
|
|||||||
headers={api_base.Version.string: "1.36"},
|
headers={api_base.Version.string: "1.36"},
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
|
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)
|
||||||
|
@ -275,7 +275,7 @@ class TestRBACModelBeforeScopesBase(TestACLBase):
|
|||||||
value=fake_setting)
|
value=fake_setting)
|
||||||
db_utils.create_test_node_trait(
|
db_utils.create_test_node_trait(
|
||||||
node_id=fake_db_node['id'])
|
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
|
# dedicated node for portgroup addition test to avoid
|
||||||
# false positives with test runners.
|
# false positives with test runners.
|
||||||
db_utils.create_test_node(
|
db_utils.create_test_node(
|
||||||
@ -298,6 +298,7 @@ class TestRBACModelBeforeScopesBase(TestACLBase):
|
|||||||
'trait': fake_trait,
|
'trait': fake_trait,
|
||||||
'volume_target_ident': fake_db_volume_target['uuid'],
|
'volume_target_ident': fake_db_volume_target['uuid'],
|
||||||
'volume_connector_ident': fake_db_volume_connector['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'],
|
node_id=owned_node['id'],
|
||||||
owner=owner_project_id,
|
owner=owner_project_id,
|
||||||
resource_class="CUSTOM_TEST")
|
resource_class="CUSTOM_TEST")
|
||||||
|
owned_node_history = db_utils.create_test_history(
|
||||||
|
node_id=owned_node.id)
|
||||||
|
|
||||||
# Leased nodes
|
# Leased nodes
|
||||||
fake_allocation_id = 61
|
fake_allocation_id = 61
|
||||||
@ -428,6 +431,9 @@ class TestRBACProjectScoped(TestACLBase):
|
|||||||
owner=lessee_project_id,
|
owner=lessee_project_id,
|
||||||
resource_class="CUSTOM_LEASED")
|
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
|
# Random objects that shouldn't be project visible
|
||||||
other_port = db_utils.create_test_port(
|
other_port = db_utils.create_test_port(
|
||||||
uuid='abfd8dbb-1732-449a-b760-2224035c6b99',
|
uuid='abfd8dbb-1732-449a-b760-2224035c6b99',
|
||||||
@ -460,7 +466,9 @@ class TestRBACProjectScoped(TestACLBase):
|
|||||||
'other_portgroup_ident': other_pgroup['uuid'],
|
'other_portgroup_ident': other_pgroup['uuid'],
|
||||||
'driver_name': 'fake-driverz',
|
'driver_name': 'fake-driverz',
|
||||||
'owner_allocation': fake_owner_allocation['uuid'],
|
'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.file_data('test_rbac_project_scoped.yaml')
|
||||||
@ddt.unpack
|
@ddt.unpack
|
||||||
|
@ -2349,3 +2349,49 @@ chassis_chassis_id_delete_observer:
|
|||||||
headers: *observer_headers
|
headers: *observer_headers
|
||||||
assert_status: 403
|
assert_status: 403
|
||||||
deprecated: true
|
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
|
||||||
|
@ -2629,3 +2629,95 @@ third_party_admin_cannot_create_chassis:
|
|||||||
body:
|
body:
|
||||||
description: 'test-chassis'
|
description: 'test-chassis'
|
||||||
assert_status: 500
|
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
|
||||||
|
@ -2079,3 +2079,47 @@ chassis_chassis_id_delete_reader:
|
|||||||
method: delete
|
method: delete
|
||||||
headers: *reader_headers
|
headers: *reader_headers
|
||||||
assert_status: 403
|
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
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user