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
orDetail.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 currentDetail
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 differentDetail
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 mappedDetail
will be used; otherwise, the code will supply aDetail.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 mappedDetail
field will be used.
Module documentation¶
The Message API Module¶
Handles all requests related to user facing messages.
- class API
API for handling user messages.
Cinder Messages describe the outcome of a user action using predefined fields that are members of objects defined in the cinder.message.message_field package. They are intended to be exposed to end users. Their primary purpose is to provide end users with a means of discovering what went wrong when an asynchronous action in the Volume REST API (for which they’ve already received a 2xx response) fails.
Messages contain an ‘expires_at’ field based on the creation time plus the value of the ‘message_ttl’ configuration option. They are periodically reaped by a task of the SchedulerManager class whose periodicity is given by the ‘message_reap_interval’ configuration option.
- cleanup_expired_messages(context)
- create(context, action, resource_type='VOLUME', resource_uuid=None, exception=None, detail=None, level='ERROR')
Create a message record with the specified information.
- Parameters:
context – current context object
action – a message_field.Action field describing what was taking place when this message was created
resource_type – a message_field.Resource field describing the resource this message applies to. Default is message_field.Resource.VOLUME
resource_uuid – the resource ID if this message applies to an existing resource. Default is None
exception – if an exception has occurred, you can pass it in and it will be translated into an appropriate message detail ID (possibly message_field.Detail.UNKNOWN_ERROR). The message in the exception itself is ignored in order not to expose sensitive information to end users. Default is None
detail – a message_field.Detail field describing the event the message is about. Default is None, in which case message_field.Detail.UNKNOWN_ERROR will be used for the message unless an exception in the message_field.EXCEPTION_DETAIL_MAPPINGS is passed; in that case the message_field.Detail field that’s mapped to the exception is used.
level – a string describing the severity of the message. Suggested values are ‘INFO’, ‘ERROR’, ‘WARNING’. Default is ‘ERROR’.
- create_from_request_context(context, exception=None, detail=None, level='ERROR')
Create a message record with the specified information.
- Parameters:
context – current context object which we must have populated with the message_action, message_resource_type and message_resource_id fields
exception – if an exception has occurred, you can pass it in and it will be translated into an appropriate message detail ID (possibly message_field.Detail.UNKNOWN_ERROR). The message in the exception itself is ignored in order not to expose sensitive information to end users. Default is None
detail – a message_field.Detail field describing the event the message is about. Default is None, in which case message_field.Detail.UNKNOWN_ERROR will be used for the message unless an exception in the message_field.EXCEPTION_DETAIL_MAPPINGS is passed; in that case the message_field.Detail field that’s mapped to the exception is used.
level – a string describing the severity of the message. Suggested values are ‘INFO’, ‘ERROR’, ‘WARNING’. Default is ‘ERROR’.
- delete(context, id)
Delete message with the specified id.
- get(context, id)
Return message with the specified id.
- get_all(context, filters=None, marker=None, limit=None, offset=None, sort_keys=None, sort_dirs=None)
Return all messages for the given context.
The Message Field Module¶
Message Resource, Action, Detail and user visible message.
Use Resource, Action and Detail’s combination to indicate the Event in the format of:
EVENT: VOLUME_RESOURCE_ACTION_DETAIL
Also, use exception-to-detail mapping to decrease the workload of classifying event in cinder’s task code.
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.)
Event ID and user visible message mapping.
Event IDs are used to look up the message to be displayed for an API Message object. All defined messages 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.
- class EventIds
Bases:
object
- ATTACH_READONLY_VOLUME = 'VOLUME_000003'
- IMAGE_FROM_VOLUME_OVER_QUOTA = 'VOLUME_000004'
- UNABLE_TO_ALLOCATE = 'VOLUME_000002'
- UNKNOWN_ERROR = 'VOLUME_000001'
- UNMANAGE_ENCRYPTED_VOLUME_UNSUPPORTED = 'VOLUME_000005'
- get_message_text(event_id)