cinder/doc/source/contributor/user_messages.rst
Brian Rosmaita 09c1111904 Document behavior of message.create
Add docstrings to clarify the behavior of message.create() when both
an exception and detail are passed as arguments.  Updates the
user_messages section of the contributor docs to reflect the changes
introduced in Pike by implementation of the "Explicit user messages"
spec.  Adds tests to prevent regressions.

Closes-bug: #1822000
Change-Id: Ia0425dc9c241d0c101057fbc534e68e7e5fdc317
2019-04-04 10:27:12 -04:00

253 lines
9.4 KiB
ReStructuredText

User Messages
=============
General information
~~~~~~~~~~~~~~~~~~~
User messages are a way to inform users about the state of asynchronous
operations. One example would be notifying the user of why a volume
provisioning request failed. End users can request these messages via the
Volume v3 REST API under the ``/messages`` resource. The REST API allows
only GET and DELETE verbs for this resource.
Internally, you use the ``cinder.message.api`` to work with messages. In
order to prevent leakage of sensitive information or breaking the volume
service abstraction layer, free-form messages are *not* allowed. Instead, all
messages must be defined using a combination of pre-defined fields in the
``cinder.message.message_field`` module.
The message ultimately displayed to end users is combined from an ``Action``
field and a ``Detail`` field.
* The ``Action`` field describes what was taking place when the message
was created, for example, ``Action.COPY_IMAGE_TO_VOLUME``.
* The ``Detail`` field is used to provide more information, for example,
``Detail.NOT_ENOUGH_SPACE_FOR_IMAGE`` or ``Detail.QUOTA_EXCEED``.
Example
~~~~~~~
Example message generation::
from cinder import context
from cinder.message import api as message_api
from cinder.message import message_field
self.message_api = message_api.API()
context = context.RequestContext()
volume_id = 'f292cc0c-54a7-4b3b-8174-d2ff82d87008'
self.message_api.create(
context,
message_field.Action.UNMANAGE_VOLUME,
resource_uuid=volume_id,
detail=message_field.Detail.UNMANAGE_ENC_NOT_SUPPORTED)
Will produce roughly the following::
GET /v3/6c430ede-9476-4128-8838-8d3929ced223/messages
{
"messages": [
{
"id": "5429fffa-5c76-4d68-a671-37a8e24f37cf",
"event_id": "VOLUME_VOLUME_006_008",
"user_message": "unmanage volume: Unmanaging encrypted volumes is not supported.",
"message_level": "ERROR",
"resource_type": "VOLUME",
"resource_uuid": "f292cc0c-54a7-4b3b-8174-d2ff82d87008",
"created_at": 2018-08-27T09:49:58-05:00,
"guaranteed_until": 2018-09-27T09:49:58-05:00,
"request_id": "req-936666d2-4c8f-4e41-9ac9-237b43f8b848",
}
]
}
Adding user messages
~~~~~~~~~~~~~~~~~~~~
If you are creating a message in the code but find that the predefined fields
are insufficient, just add what you need to ``cinder.message.message_field``.
The key thing to keep in mind is that all defined fields should be appropriate
for any API user to see and not contain any sensitive information. A good
rule-of-thumb is to be very general in error messages unless the issue is due
to a bad user action, then be specific.
As a convenience to developers, the ``Detail`` class contains a
``EXCEPTION_DETAIL_MAPPINGS`` dict. This maps ``Detail`` fields to particular
Cinder exceptions, and allows you to create messages in a context where you've
caught an Exception that could be any of several possibilities. Instead of
having to sort through them where you've caught the exception, you can call
``message_api.create`` and pass it both the exception and a general detail
field like ``Detail.SOMETHING_BAD_HAPPENED`` (that's not a real field, but
you get the idea). If the passed exception is in the mapping, the resulting
message will have the mapped ``Detail`` field instead of the generic one.
Usage patterns
~~~~~~~~~~~~~~
These are taken from the Cinder code. The exact code may have changed
by the time you read this, but the general idea should hold.
No exception in context
-----------------------
From cinder/compute/nova.py::
def extend_volume(self, context, server_ids, volume_id):
api_version = '2.51'
events = [self._get_volume_extended_event(server_id, volume_id)
for server_id in server_ids]
result = self._send_events(context, events, api_version=api_version)
if not result:
self.message_api.create(
context,
message_field.Action.EXTEND_VOLUME,
resource_uuid=volume_id,
detail=message_field.Detail.NOTIFY_COMPUTE_SERVICE_FAILED)
return result
* You must always pass the context object and an action.
* We're working with an existing volume, so pass its ID as the
``resource_uuid``.
* You need to fill in some detail, or else the code will supply an
``UNKNOWN_ERROR``, which isn't very helpful.
Cinder exception in context
---------------------------
From cinder/scheduler/manager.py::
except exception.NoValidBackend as ex:
QUOTAS.rollback(context, reservations,
project_id=volume.project_id)
_extend_volume_set_error(self, context, ex, request_spec)
self.message_api.create(
context,
message_field.Action.EXTEND_VOLUME,
resource_uuid=volume.id,
exception=ex)
* You must always pass the context object and an action.
* Since we have it available, pass the volume ID as the resource_uuid.
* It's a Cinder exception. Check to see if it's in the mapping.
* If it's there, we can pass it, and the detail will be supplied
by the code.
* It it's not, consider adding it and mapping it to an existing
``Detail`` field. If there's no current ``Detail`` field for that
exception, go ahead and add that, too.
* On the other hand, maybe it's in the mapping, but you have more
information in this code context than is available in the mapped
``Detail`` field. In that case, you may want to use a different
``Detail`` field (creating it if necessary).
* Remember, if you pass *both* a mapped exception *and* a detail, the
passed detail will be ignored and the mapped ``Detail`` field will be
used instead.
General Exception in context
----------------------------
Not passing the Exception to message_api.create()
+++++++++++++++++++++++++++++++++++++++++++++++++
From cinder/volume/manager.py::
try:
self.driver.extend_volume(volume, new_size)
except exception.TargetUpdateFailed:
# We just want to log this but continue on with quota commit
LOG.warning('Volume extended but failed to update target.')
except Exception:
LOG.exception("Extend volume failed.",
resource=volume)
self.message_api.create(
context,
message_field.Action.EXTEND_VOLUME,
resource_uuid=volume.id,
detail=message_field.Detail.DRIVER_FAILED_EXTEND)
* Pass the context object and an action; pass a ``resource_uuid`` since we
have it.
* We're not passing the exception, so the ``detail`` we pass is guaranteed
to be used.
Passing the Exception to message_api.create()
+++++++++++++++++++++++++++++++++++++++++++++
From cinder/volume/manager.py::
try:
if volume_metadata.get('readonly') == 'True' and mode != 'ro':
raise exception.InvalidVolumeAttachMode(mode=mode,
volume_id=volume.id)
utils.require_driver_initialized(self.driver)
LOG.info('Attaching volume %(volume_id)s to instance '
'%(instance)s at mountpoint %(mount)s on host '
'%(host)s.',
{'volume_id': volume_id, 'instance': instance_uuid,
'mount': mountpoint, 'host': host_name_sanitized},
resource=volume)
self.driver.attach_volume(context,
volume,
instance_uuid,
host_name_sanitized,
mountpoint)
except Exception as excep:
with excutils.save_and_reraise_exception():
self.message_api.create(
context,
message_field.Action.ATTACH_VOLUME,
resource_uuid=volume_id,
exception=excep)
attachment.attach_status = (
fields.VolumeAttachStatus.ERROR_ATTACHING)
attachment.save()
* Pass the context object and an action; pass a resource_uuid since we
have it.
* We're passing an exception, which could be a Cinder
``InvalidVolumeAttachMode``, which is in the mapping. In that case, the
mapped ``Detail`` will be used;
otherwise, the code will supply a ``Detail.UNKNOWN_ERROR``.
This is appropriate if we really have no idea what happened. If it's
possible to provide more information, we can pass a different, generic
``Detail`` field (creating it if necessary). The passed detail would be
used for any exception that's *not* in the mapping. If it's a mapped
exception, then the mapped ``Detail`` field will be used.
Module documentation
~~~~~~~~~~~~~~~~~~~~
The Message API Module
----------------------
.. automodule:: cinder.message.api
:noindex:
:members:
:undoc-members:
The Message Field Module
------------------------
.. automodule:: cinder.message.message_field
:noindex:
The Defined Messages Module
---------------------------
This module is DEPRECATED and is currently only used by
``cinder.api.v3.messages`` to handle pre-Pike message database objects.
(Editorial comment:: With the default ``message_ttl`` of 2592000 seconds
(30 days), it's probably safe to remove this module during the Train
development cycle.)
.. automodule:: cinder.message.defined_messages
:noindex:
:members:
:undoc-members:
:show-inheritance: