From 53cfde43b88b169c0cf6c908fd8e792e7ea78701 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Sun, 24 Apr 2016 14:41:19 +0530 Subject: [PATCH] User messages API for error cases This patch implements basic user messages with the following APIs. GET /messages GET /messages/ DELETE /messages/ Implements : blueprint summarymessage Co-Authored-By: Alex Meade Co-Authored-By: Sheel Rana Change-Id: Id8a4a700c1159be24b15056f401a2ea77804d0a0 --- cinder/api/openstack/api_version_request.py | 3 +- .../openstack/rest_api_version_history.rst | 4 + cinder/api/v3/messages.py | 106 +++++++++++++ cinder/api/v3/router.py | 6 + cinder/api/v3/views/__init__.py | 0 cinder/api/v3/views/messages.py | 69 +++++++++ cinder/db/api.py | 22 +++ cinder/db/sqlalchemy/api.py | 70 +++++++++ .../versions/074_add_message_table.py | 40 +++++ cinder/db/sqlalchemy/models.py | 18 +++ cinder/exception.py | 8 + cinder/image/glance.py | 2 + cinder/message/__init__.py | 0 cinder/message/api.py | 75 ++++++++++ cinder/message/defined_messages.py | 44 ++++++ cinder/message/resource_types.py | 15 ++ cinder/opts.py | 2 + cinder/scheduler/flows/create_volume.py | 11 ++ cinder/tests/unit/api/v3/stubs.py | 42 ++++++ cinder/tests/unit/api/v3/test_messages.py | 140 ++++++++++++++++++ cinder/tests/unit/image/test_glance.py | 14 ++ cinder/tests/unit/message/__init__.py | 0 cinder/tests/unit/message/test_api.py | 94 ++++++++++++ cinder/tests/unit/policy.json | 6 +- cinder/tests/unit/scheduler/test_scheduler.py | 10 +- cinder/tests/unit/test_migrations.py | 27 ++++ cinder/tests/unit/test_volume.py | 92 ++++++------ cinder/volume/manager.py | 14 ++ doc/source/devref/index.rst | 1 + doc/source/devref/user_messages.rst | 73 +++++++++ etc/cinder/policy.json | 5 +- 31 files changed, 965 insertions(+), 48 deletions(-) create mode 100644 cinder/api/v3/messages.py create mode 100644 cinder/api/v3/views/__init__.py create mode 100644 cinder/api/v3/views/messages.py create mode 100644 cinder/db/sqlalchemy/migrate_repo/versions/074_add_message_table.py create mode 100644 cinder/message/__init__.py create mode 100644 cinder/message/api.py create mode 100644 cinder/message/defined_messages.py create mode 100644 cinder/message/resource_types.py create mode 100644 cinder/tests/unit/api/v3/stubs.py create mode 100644 cinder/tests/unit/api/v3/test_messages.py create mode 100644 cinder/tests/unit/message/__init__.py create mode 100644 cinder/tests/unit/message/test_api.py create mode 100644 doc/source/devref/user_messages.rst diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index 2eb949a484d..b3770a62c5d 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -49,6 +49,7 @@ REST_API_VERSION_HISTORY = """ * 3.1 - Adds visibility and protected to _volume_upload_image parameters. * 3.2 - Bootable filters in volume GET call no longer treats all values passed to it as true. + * 3.3 - Add user messages APIs. """ @@ -57,7 +58,7 @@ REST_API_VERSION_HISTORY = """ # minimum version of the API supported. # Explicitly using /v1 or /v2 enpoints will still work _MIN_API_VERSION = "3.0" -_MAX_API_VERSION = "3.2" +_MAX_API_VERSION = "3.3" _LEGACY_API_VERSION1 = "1.0" _LEGACY_API_VERSION2 = "2.0" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 87a87fe9434..24b2444017f 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -51,3 +51,7 @@ user documentation. bootable filter values. But for any other values passed for bootable filter, it will return "Invalid input received: bootable={filter value}' error. + +3.3 +--- + Added /messages API. diff --git a/cinder/api/v3/messages.py b/cinder/api/v3/messages.py new file mode 100644 index 00000000000..f8cb4cdaede --- /dev/null +++ b/cinder/api/v3/messages.py @@ -0,0 +1,106 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The messages API.""" + + +from oslo_config import cfg +from oslo_log import log as logging +import webob +from webob import exc + +from cinder.api.openstack import wsgi +from cinder.api.v3.views import messages as messages_view +from cinder import exception +from cinder.message import api as message_api +from cinder.message import defined_messages +import cinder.policy + +CONF = cfg.CONF + +LOG = logging.getLogger(__name__) +MESSAGES_BASE_MICRO_VERSION = '3.3' + + +def check_policy(context, action, target_obj=None): + target = { + 'project_id': context.project_id, + 'user_id': context.user_id, + } + target.update(target_obj or {}) + + _action = 'message:%s' % action + cinder.policy.enforce(context, _action, target) + + +class MessagesController(wsgi.Controller): + """The User Messages API controller for the OpenStack API.""" + + _view_builder_class = messages_view.ViewBuilder + + def __init__(self, ext_mgr): + self.message_api = message_api.API() + self.ext_mgr = ext_mgr + super(MessagesController, self).__init__() + + @wsgi.Controller.api_version(MESSAGES_BASE_MICRO_VERSION) + def show(self, req, id): + """Return the given message.""" + context = req.environ['cinder.context'] + + try: + message = self.message_api.get(context, id) + except exception.MessageNotFound as error: + raise exc.HTTPNotFound(explanation=error.msg) + + check_policy(context, 'get', message) + + # Fetches message text based on event id passed to it. + message['user_message'] = defined_messages.get_message_text( + message['event_id']) + + return self._view_builder.detail(req, message) + + @wsgi.Controller.api_version(MESSAGES_BASE_MICRO_VERSION) + def delete(self, req, id): + """Delete a message.""" + context = req.environ['cinder.context'] + + try: + message = self.message_api.get(context, id) + check_policy(context, 'delete', message) + self.message_api.delete(context, message) + except exception.MessageNotFound as error: + raise exc.HTTPNotFound(explanation=error.msg) + + return webob.Response(status_int=204) + + @wsgi.Controller.api_version(MESSAGES_BASE_MICRO_VERSION) + def index(self, req): + """Returns a list of messages, transformed through view builder.""" + context = req.environ['cinder.context'] + check_policy(context, 'get_all') + + messages = self.message_api.get_all(context) + + for message in messages: + # Fetches message text based on event id passed to it. + user_message = defined_messages.get_message_text( + message['event_id']) + message['user_message'] = user_message + + messages = self._view_builder.index(req, messages) + return messages + + +def create_resource(ext_mgr): + return wsgi.Resource(MessagesController(ext_mgr)) diff --git a/cinder/api/v3/router.py b/cinder/api/v3/router.py index 0da7ef7aaca..7e8ae4e6f4a 100644 --- a/cinder/api/v3/router.py +++ b/cinder/api/v3/router.py @@ -28,6 +28,7 @@ from cinder.api.v2 import snapshot_metadata from cinder.api.v2 import snapshots from cinder.api.v2 import types from cinder.api.v2 import volume_metadata +from cinder.api.v3 import messages from cinder.api.v3 import volumes from cinder.api import versions @@ -53,6 +54,11 @@ class APIRouter(cinder.api.openstack.APIRouter): collection={'detail': 'GET'}, member={'action': 'POST'}) + self.resources['messages'] = messages.create_resource(ext_mgr) + mapper.resource("message", "messages", + controller=self.resources['messages'], + collection={'detail': 'GET'}) + self.resources['types'] = types.create_resource() mapper.resource("type", "types", controller=self.resources['types'], diff --git a/cinder/api/v3/views/__init__.py b/cinder/api/v3/views/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/api/v3/views/messages.py b/cinder/api/v3/views/messages.py new file mode 100644 index 00000000000..5f95f5b28b4 --- /dev/null +++ b/cinder/api/v3/views/messages.py @@ -0,0 +1,69 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from cinder.api import common + + +class ViewBuilder(common.ViewBuilder): + """Model a server API response as a python dictionary.""" + + _collection_name = "messages" + + def index(self, request, messages, message_count=None): + """Show a list of messages.""" + return self._list_view(self.detail, request, messages, message_count) + + def detail(self, request, message): + """Detailed view of a single message.""" + message_ref = { + 'id': message.get('id'), + 'event_id': message.get('event_id'), + 'user_message': message.get('user_message'), + 'message_level': message.get('message_level'), + 'created_at': message.get('created_at'), + 'guaranteed_until': message.get('expires_at'), + 'request_id': message.get('request_id'), + 'links': self._get_links(request, message['id']), + } + + if message.get('resource_type'): + message_ref['resource_type'] = message.get('resource_type') + if message.get('resource_uuid'): + message_ref['resource_uuid'] = message.get('resource_uuid') + + return {'message': message_ref} + + def _list_view(self, func, request, messages, message_count=None, + coll_name=_collection_name): + """Provide a view for a list of messages. + + :param func: Function used to format the message data + :param request: API request + :param messages: List of messages in dictionary format + :param message_count: Length of the original list of messages + :param coll_name: Name of collection, used to generate the next link + for a pagination query + :returns: message data in dictionary format + """ + messages_list = [func(request, message)['message'] + for message in messages] + messages_links = self._get_collection_links(request, + messages, + coll_name, + message_count) + messages_dict = dict(messages=messages_list) + + if messages_links: + messages_dict['messages_links'] = messages_links + + return messages_dict diff --git a/cinder/db/api.py b/cinder/db/api.py index 05f6129c74d..d5980438601 100644 --- a/cinder/db/api.py +++ b/cinder/db/api.py @@ -1121,6 +1121,28 @@ def image_volume_cache_get_all_for_host(context, host): ################### +def message_get(context, message_id): + """Return a message with the specified ID.""" + return IMPL.message_get(context, message_id) + + +def message_get_all(context): + return IMPL.message_get_all(context) + + +def message_create(context, values): + """Creates a new message with the specified values.""" + return IMPL.message_create(context, values) + + +def message_destroy(context, message_id): + """Deletes message with the specified ID.""" + return IMPL.message_destroy(context, message_id) + + +################### + + def get_model_for_versioned_object(versioned_object): return IMPL.get_model_for_versioned_object(versioned_object) diff --git a/cinder/db/sqlalchemy/api.py b/cinder/db/sqlalchemy/api.py index eb610051baa..9a3c0b06010 100644 --- a/cinder/db/sqlalchemy/api.py +++ b/cinder/db/sqlalchemy/api.py @@ -4271,6 +4271,76 @@ def purge_deleted_rows(context, age_in_days): ############################### +def _translate_messages(messages): + return [_translate_message(message) for message in messages] + + +def _translate_message(message): + """Translate the Message model to a dict.""" + return { + 'id': message['id'], + 'project_id': message['project_id'], + 'request_id': message['request_id'], + 'resource_type': message['resource_type'], + 'resource_uuid': message.get('resource_uuid'), + 'event_id': message['event_id'], + 'message_level': message['message_level'], + 'created_at': message['created_at'], + 'expires_at': message.get('expires_at'), + } + + +@require_context +def message_get(context, message_id): + query = model_query(context, + models.Message, + read_deleted="no", + project_only="yes") + result = query.filter_by(id=message_id).first() + if not result: + raise exception.MessageNotFound(message_id=message_id) + return _translate_message(result) + + +@require_context +def message_get_all(context): + """Fetch all messages for the contexts project.""" + messages = models.Message + query = (model_query(context, + messages, + read_deleted="no", + project_only="yes")) + results = query.all() + return _translate_messages(results) + + +@require_context +def message_create(context, values): + message_ref = models.Message() + if not values.get('id'): + values['id'] = str(uuid.uuid4()) + message_ref.update(values) + + session = get_session() + with session.begin(): + session.add(message_ref) + + +@require_admin_context +def message_destroy(context, message): + session = get_session() + now = timeutils.utcnow() + with session.begin(): + (model_query(context, models.Message, session=session). + filter_by(id=message.get('id')). + update({'deleted': True, + 'deleted_at': now, + 'updated_at': literal_column('updated_at')})) + + +############################### + + @require_context def driver_initiator_data_update(context, initiator, namespace, updates): session = get_session() diff --git a/cinder/db/sqlalchemy/migrate_repo/versions/074_add_message_table.py b/cinder/db/sqlalchemy/migrate_repo/versions/074_add_message_table.py new file mode 100644 index 00000000000..434962cee97 --- /dev/null +++ b/cinder/db/sqlalchemy/migrate_repo/versions/074_add_message_table.py @@ -0,0 +1,40 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Boolean, Column, DateTime +from sqlalchemy import MetaData, String, Table + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + # New table + messages = Table( + 'messages', + meta, + Column('id', String(36), primary_key=True, nullable=False), + Column('project_id', String(36), nullable=False), + Column('request_id', String(255), nullable=False), + Column('resource_type', String(36)), + Column('resource_uuid', String(255), nullable=True), + Column('event_id', String(255), nullable=False), + Column('message_level', String(255), nullable=False), + Column('created_at', DateTime(timezone=False)), + Column('updated_at', DateTime(timezone=False)), + Column('deleted_at', DateTime(timezone=False)), + Column('deleted', Boolean), + Column('expires_at', DateTime(timezone=False)), + mysql_engine='InnoDB' + ) + + messages.create() diff --git a/cinder/db/sqlalchemy/models.py b/cinder/db/sqlalchemy/models.py index 06475056fdb..8b7324f6036 100644 --- a/cinder/db/sqlalchemy/models.py +++ b/cinder/db/sqlalchemy/models.py @@ -599,6 +599,24 @@ class DriverInitiatorData(BASE, models.TimestampMixin, models.ModelBase): value = Column(String(255)) +class Message(BASE, CinderBase): + """Represents a message""" + __tablename__ = 'messages' + id = Column(String(36), primary_key=True, nullable=False) + project_id = Column(String(36), nullable=False) + # Info/Error/Warning. + message_level = Column(String(255), nullable=False) + request_id = Column(String(255), nullable=True) + resource_type = Column(String(255)) + # The uuid of the related resource. + resource_uuid = Column(String(36), nullable=True) + # Operation specific event ID. + event_id = Column(String(255), nullable=False) + created_at = Column(DateTime, default=lambda: timeutils.utcnow()) + # After this time the message may no longer exist + expires_at = Column(DateTime, nullable=True) + + class ImageVolumeCacheEntry(BASE, models.ModelBase): """Represents an image volume cache entry""" __tablename__ = 'image_volume_cache_entries' diff --git a/cinder/exception.py b/cinder/exception.py index c117c9f062c..15a7ee41721 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -286,6 +286,10 @@ class VolumeNotFound(NotFound): message = _("Volume %(volume_id)s could not be found.") +class MessageNotFound(NotFound): + message = _("Message %(message_id)s could not be found.") + + class VolumeAttachmentNotFound(NotFound): message = _("Volume attachment could not be found with " "filter: %(filter)s .") @@ -532,6 +536,10 @@ class BackupLimitExceeded(QuotaError): message = _("Maximum number of backups allowed (%(allowed)d) exceeded") +class ImageLimitExceeded(QuotaError): + message = _("Image quota exceeded") + + class DuplicateSfVolumeNames(Duplicate): message = _("Detected more than one volume with name %(vol_name)s") diff --git a/cinder/image/glance.py b/cinder/image/glance.py index e8b92db12aa..55d83a66118 100644 --- a/cinder/image/glance.py +++ b/cinder/image/glance.py @@ -219,6 +219,8 @@ class GlanceClientWrapper(object): 'method': method, 'extra': extra}) time.sleep(1) + except glanceclient.exc.HTTPOverLimit as e: + raise exception.ImageLimitExceeded(e) class GlanceImageService(object): diff --git a/cinder/message/__init__.py b/cinder/message/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/message/api.py b/cinder/message/api.py new file mode 100644 index 00000000000..d631a550884 --- /dev/null +++ b/cinder/message/api.py @@ -0,0 +1,75 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Handles all requests related to user facing messages. +""" +import datetime + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import timeutils + +from cinder.db import base +from cinder.i18n import _LE, _LI +from cinder.message import defined_messages + + +messages_opts = [ + cfg.IntOpt('message_ttl', default=2592000, + help='message minimum life in seconds.')] + +CONF = cfg.CONF +CONF.register_opts(messages_opts) + +LOG = logging.getLogger(__name__) + + +class API(base.Base): + """API for handling user messages.""" + + def create(self, context, event_id, project_id, resource_type=None, + resource_uuid=None, level="ERROR"): + """Create a message with the specified information.""" + LOG.info(_LI("Creating message record for request_id = %s"), + context.request_id) + # Ensure valid event_id + defined_messages.get_message_text(event_id) + # Updates expiry time for message as per message_ttl config. + expires_at = (timeutils.utcnow() + datetime.timedelta( + seconds=CONF.message_ttl)) + + message_record = {'project_id': project_id, + 'request_id': context.request_id, + 'resource_type': resource_type, + 'resource_uuid': resource_uuid, + 'event_id': event_id, + 'message_level': level, + 'expires_at': expires_at} + try: + self.db.message_create(context, message_record) + except Exception: + LOG.exception(_LE("Failed to create message record " + "for request_id %s"), context.request_id) + + def get(self, context, id): + """Return message with the specified id.""" + return self.db.message_get(context, id) + + def get_all(self, context): + """Return all messages for the given context.""" + messages = self.db.message_get_all(context) + return messages + + def delete(self, context, id): + """Delete message with the specified id.""" + ctx = context.elevated() + return self.db.message_destroy(ctx, id) diff --git a/cinder/message/defined_messages.py b/cinder/message/defined_messages.py new file mode 100644 index 00000000000..9fac0b6c578 --- /dev/null +++ b/cinder/message/defined_messages.py @@ -0,0 +1,44 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""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. +""" + +from cinder.i18n import _ + +UNKNOWN_ERROR = '000001' +UNABLE_TO_ALLOCATE = '000002' +ATTACH_READONLY_VOLUME = '000003' +IMAGE_FROM_VOLUME_OVER_QUOTA = '000004' + +event_id_message_map = { + UNKNOWN_ERROR: _("An unknown error occurred."), + UNABLE_TO_ALLOCATE: _("No storage could be allocated for this volume " + "request. You may be able to try another size or" + " volume type."), + ATTACH_READONLY_VOLUME: _("A readonly volume must be attached as " + "readonly."), + IMAGE_FROM_VOLUME_OVER_QUOTA: _("Failed to copy volume to image as image " + "quota has been met. Please delete images" + " or have your limit increased, then try " + "again."), +} + + +def get_message_text(event_id): + return event_id_message_map[event_id] diff --git a/cinder/message/resource_types.py b/cinder/message/resource_types.py new file mode 100644 index 00000000000..e29032be3db --- /dev/null +++ b/cinder/message/resource_types.py @@ -0,0 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Resource type constants.""" + +VOLUME = 'VOLUME' diff --git a/cinder/opts.py b/cinder/opts.py index 146342df1f9..9962bd28050 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -44,6 +44,7 @@ from cinder.image import image_utils as cinder_image_imageutils import cinder.keymgr from cinder.keymgr import conf_key_mgr as cinder_keymgr_confkeymgr from cinder.keymgr import key_mgr as cinder_keymgr_keymgr +from cinder.message import api as cinder_message_api from cinder import quota as cinder_quota from cinder.scheduler import driver as cinder_scheduler_driver from cinder.scheduler import host_manager as cinder_scheduler_hostmanager @@ -311,6 +312,7 @@ def list_opts(): cinder_volume_drivers_zfssa_zfssanfs.ZFSSA_OPTS, cinder_volume_drivers_disco_disco.disco_opts, cinder_volume_drivers_hgst.hgst_opts, + cinder_message_api.messages_opts, cinder_image_imageutils.image_helper_opts, cinder_compute_nova.nova_opts, cinder_volume_drivers_ibm_flashsystemfc.flashsystem_fc_opts, diff --git a/cinder/scheduler/flows/create_volume.py b/cinder/scheduler/flows/create_volume.py index 0f917a37fa4..2a879755702 100644 --- a/cinder/scheduler/flows/create_volume.py +++ b/cinder/scheduler/flows/create_volume.py @@ -18,6 +18,9 @@ from taskflow.patterns import linear_flow from cinder import exception from cinder import flow_utils from cinder.i18n import _LE +from cinder.message import api as message_api +from cinder.message import defined_messages +from cinder.message import resource_types from cinder import rpc from cinder import utils from cinder.volume.flows import common @@ -90,6 +93,7 @@ class ScheduleCreateVolumeTask(flow_utils.CinderTask): **kwargs) self.db_api = db_api self.driver_api = driver_api + self.message_api = message_api.API() def _handle_failure(self, context, request_spec, cause): try: @@ -127,6 +131,13 @@ class ScheduleCreateVolumeTask(flow_utils.CinderTask): # reraise (since what's the point?) with excutils.save_and_reraise_exception( reraise=not isinstance(e, exception.NoValidHost)): + if isinstance(e, exception.NoValidHost): + self.message_api.create( + context, + defined_messages.UNABLE_TO_ALLOCATE, + context.project_id, + resource_type=resource_types.VOLUME, + resource_uuid=request_spec['volume_id']) try: self._handle_failure(context, request_spec, e) finally: diff --git a/cinder/tests/unit/api/v3/stubs.py b/cinder/tests/unit/api/v3/stubs.py new file mode 100644 index 00000000000..98d761b5fc2 --- /dev/null +++ b/cinder/tests/unit/api/v3/stubs.py @@ -0,0 +1,42 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import iso8601 + +from cinder.message import defined_messages +from cinder.tests.unit import fake_constants + + +FAKE_UUID = fake_constants.object_id + + +def stub_message(id, **kwargs): + message = { + 'id': id, + 'event_id': defined_messages.UNABLE_TO_ALLOCATE, + 'message_level': "ERROR", + 'request_id': FAKE_UUID, + 'updated_at': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.iso8601.Utc()), + 'created_at': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.iso8601.Utc()), + 'expires_at': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.iso8601.Utc()), + } + + message.update(kwargs) + return message + + +def stub_message_get(self, context, message_id): + return stub_message(message_id) diff --git a/cinder/tests/unit/api/v3/test_messages.py b/cinder/tests/unit/api/v3/test_messages.py new file mode 100644 index 00000000000..a722ae3c0ee --- /dev/null +++ b/cinder/tests/unit/api/v3/test_messages.py @@ -0,0 +1,140 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslo_config import cfg +import webob + +from cinder.api import extensions +from cinder.api.v3 import messages +from cinder import context +from cinder import exception +from cinder.message import api as message_api +from cinder.message import defined_messages +from cinder import test +from cinder.tests.unit.api import fakes +from cinder.tests.unit.api.v3 import stubs + +CONF = cfg.CONF + +NS = '{http://docs.openstack.org/api/openstack-block-storage/3.0/content}' + + +class MessageApiTest(test.TestCase): + def setUp(self): + super(MessageApiTest, self).setUp() + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = messages.MessagesController(self.ext_mgr) + + self.maxDiff = None + self.ctxt = context.RequestContext('admin', 'fakeproject', True) + + def _expected_message_from_controller(self, id): + message = stubs.stub_message(id) + links = [ + {'href': 'http://localhost/v3/fakeproject/messages/%s' % id, + 'rel': 'self'}, + {'href': 'http://localhost/fakeproject/messages/%s' % id, + 'rel': 'bookmark'}, + ] + return { + 'message': { + 'id': message.get('id'), + 'user_message': defined_messages.get_message_text( + message.get('event_id')), + 'request_id': message.get('request_id'), + 'event_id': message.get('event_id'), + 'created_at': message.get('created_at'), + 'message_level': message.get('message_level'), + 'guaranteed_until': message.get('expires_at'), + 'links': links, + } + } + + def test_show(self): + self.stubs.Set(message_api.API, 'get', stubs.stub_message_get) + + req = fakes.HTTPRequest.blank( + '/v3/messages/%s' % fakes.FAKE_UUID, + version=messages.MESSAGES_BASE_MICRO_VERSION) + req.environ['cinder.context'] = self.ctxt + + res_dict = self.controller.show(req, fakes.FAKE_UUID) + + ex = self._expected_message_from_controller(fakes.FAKE_UUID) + self.assertEqual(ex, res_dict) + + def test_show_not_found(self): + self.stubs.Set(message_api.API, 'get', + mock.Mock(side_effect=exception.MessageNotFound( + message_id=fakes.FAKE_UUID))) + + req = fakes.HTTPRequest.blank( + '/v3/messages/%s' % fakes.FAKE_UUID, + version=messages.MESSAGES_BASE_MICRO_VERSION) + req.environ['cinder.context'] = self.ctxt + + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, fakes.FAKE_UUID) + + def test_show_pre_microversion(self): + self.stubs.Set(message_api.API, 'get', stubs.stub_message_get) + + req = fakes.HTTPRequest.blank('/v3/messages/%s' % fakes.FAKE_UUID, + version='3.0') + req.environ['cinder.context'] = self.ctxt + + self.assertRaises(exception.VersionNotFoundForAPIMethod, + self.controller.show, req, fakes.FAKE_UUID) + + def test_delete(self): + self.stubs.Set(message_api.API, 'get', stubs.stub_message_get) + self.stubs.Set(message_api.API, 'delete', mock.Mock()) + + req = fakes.HTTPRequest.blank( + '/v3/messages/%s' % fakes.FAKE_UUID, + version=messages.MESSAGES_BASE_MICRO_VERSION) + req.environ['cinder.context'] = self.ctxt + + resp = self.controller.delete(req, fakes.FAKE_UUID) + + self.assertEqual(204, resp.status_int) + self.assertTrue(message_api.API.delete.called) + + def test_delete_not_found(self): + self.stubs.Set(message_api.API, 'get', + mock.Mock(side_effect=exception.MessageNotFound( + message_id=fakes.FAKE_UUID))) + + req = fakes.HTTPRequest.blank( + '/v3/messages/%s' % fakes.FAKE_UUID, + version=messages.MESSAGES_BASE_MICRO_VERSION) + + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, fakes.FAKE_UUID) + + def test_index(self): + self.stubs.Set(message_api.API, 'get_all', mock.Mock( + return_value=[stubs.stub_message(fakes.FAKE_UUID)])) + req = fakes.HTTPRequest.blank( + '/v3/messages/%s' % fakes.FAKE_UUID, + version=messages.MESSAGES_BASE_MICRO_VERSION) + req.environ['cinder.context'] = self.ctxt + + res_dict = self.controller.index(req) + + ex = self._expected_message_from_controller(fakes.FAKE_UUID) + expected = { + 'messages': [ex['message']] + } + self.assertDictMatch(expected, res_dict) diff --git a/cinder/tests/unit/image/test_glance.py b/cinder/tests/unit/image/test_glance.py index 6ecb196c342..bc7ef5e4721 100644 --- a/cinder/tests/unit/image/test_glance.py +++ b/cinder/tests/unit/image/test_glance.py @@ -824,6 +824,20 @@ class TestGlanceClientVersion(test.TestCase): self.assertEqual('2', _mockglanceclient.call_args[0][0]) + @mock.patch('cinder.image.glance.glanceclient.Client') + @mock.patch('cinder.image.glance.get_api_servers', + return_value=itertools.cycle([(False, 'localhost:9292')])) + def test_call_glance_over_quota(self, api_servers, _mockglanceclient): + """Test glance version set by arg to GlanceClientWrapper""" + glance_wrapper = glance.GlanceClientWrapper() + fake_client = mock.Mock() + fake_client.images.method = mock.Mock( + side_effect=glanceclient.exc.HTTPOverLimit) + self.mock_object(glance_wrapper, 'client', fake_client) + self.assertRaises(exception.ImageLimitExceeded, + glance_wrapper.call, 'fake_context', 'method', + version=2) + def _create_failing_glance_client(info): class MyGlanceStubClient(glance_stubs.StubGlanceClient): diff --git a/cinder/tests/unit/message/__init__.py b/cinder/tests/unit/message/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/tests/unit/message/test_api.py b/cinder/tests/unit/message/test_api.py new file mode 100644 index 00000000000..59777dbce14 --- /dev/null +++ b/cinder/tests/unit/message/test_api.py @@ -0,0 +1,94 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import datetime + +import mock +from oslo_config import cfg +from oslo_utils import timeutils + +from cinder import context +from cinder.message import api as message_api +from cinder.message import defined_messages +from cinder import test + +CONF = cfg.CONF + + +class MessageApiTest(test.TestCase): + def setUp(self): + super(MessageApiTest, self).setUp() + self.message_api = message_api.API() + self.mock_object(self.message_api, 'db') + self.ctxt = context.RequestContext('admin', 'fakeproject', True) + self.ctxt.request_id = 'fakerequestid' + + def test_create(self): + CONF.set_override('message_ttl', 300) + timeutils.set_time_override() + self.addCleanup(timeutils.clear_time_override) + expected_expires_at = timeutils.utcnow() + datetime.timedelta( + seconds=300) + expected_message_record = { + 'project_id': 'fakeproject', + 'request_id': 'fakerequestid', + 'resource_type': 'fake_resource_type', + 'resource_uuid': None, + 'event_id': defined_messages.UNABLE_TO_ALLOCATE, + 'message_level': 'ERROR', + 'expires_at': expected_expires_at, + } + self.message_api.create(self.ctxt, + defined_messages.UNABLE_TO_ALLOCATE, + "fakeproject", + resource_type="fake_resource_type") + + self.message_api.db.message_create.assert_called_once_with( + self.ctxt, expected_message_record) + + def test_create_swallows_exception(self): + self.mock_object(self.message_api.db, 'create', + mock.Mock(side_effect=Exception())) + self.message_api.create(self.ctxt, + defined_messages.UNABLE_TO_ALLOCATE, + "fakeproject", + "fake_resource") + + self.message_api.db.message_create.assert_called_once_with( + self.ctxt, mock.ANY) + + def test_create_does_not_allow_undefined_messages(self): + self.assertRaises(KeyError, self.message_api.create, + self.ctxt, + "FAKE_EVENT_ID", + "fakeproject", + "fake_resource") + + def test_get(self): + self.message_api.get(self.ctxt, 'fake_id') + + self.message_api.db.message_get.assert_called_once_with(self.ctxt, + 'fake_id') + + def test_get_all(self): + self.message_api.get_all(self.ctxt) + + self.message_api.db.message_get_all.assert_called_once_with(self.ctxt) + + def test_delete(self): + admin_context = mock.Mock() + self.mock_object(self.ctxt, 'elevated', + mock.Mock(return_value=admin_context)) + + self.message_api.delete(self.ctxt, 'fake_id') + + self.message_api.db.message_destroy.assert_called_once_with( + admin_context, 'fake_id') diff --git a/cinder/tests/unit/policy.json b/cinder/tests/unit/policy.json index 1c4f9b554ff..a5a0f2fb520 100644 --- a/cinder/tests/unit/policy.json +++ b/cinder/tests/unit/policy.json @@ -107,5 +107,9 @@ "consistencygroup:get_cgsnapshot": "", "consistencygroup:get_all_cgsnapshots": "", - "scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api" + "scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api", + + "message:delete": "rule:admin_or_owner", + "message:get": "rule:admin_or_owner", + "message:get_all": "rule:admin_or_owner" } diff --git a/cinder/tests/unit/scheduler/test_scheduler.py b/cinder/tests/unit/scheduler/test_scheduler.py index 5914a56475d..26e57f0a0bc 100644 --- a/cinder/tests/unit/scheduler/test_scheduler.py +++ b/cinder/tests/unit/scheduler/test_scheduler.py @@ -23,6 +23,7 @@ from oslo_config import cfg from cinder import context from cinder import db from cinder import exception +from cinder.message import defined_messages from cinder.objects import fields from cinder.scheduler import driver from cinder.scheduler import filter_scheduler @@ -116,9 +117,11 @@ class SchedulerManagerTestCase(test.TestCase): _mock_update_cap.assert_called_once_with(service, host, capabilities) @mock.patch('cinder.scheduler.driver.Scheduler.schedule_create_volume') + @mock.patch('cinder.message.api.API.create') @mock.patch('cinder.db.volume_update') def test_create_volume_exception_puts_volume_in_error_state( - self, _mock_volume_update, _mock_sched_create): + self, _mock_volume_update, _mock_message_create, + _mock_sched_create): # Test NoValidHost exception behavior for create_volume. # Puts the volume in 'error' state and eats the exception. _mock_sched_create.side_effect = exception.NoValidHost(reason="") @@ -136,6 +139,11 @@ class SchedulerManagerTestCase(test.TestCase): _mock_sched_create.assert_called_once_with(self.context, request_spec, {}) + _mock_message_create.assert_called_once_with( + self.context, defined_messages.UNABLE_TO_ALLOCATE, + self.context.project_id, resource_type='VOLUME', + resource_uuid=volume.id) + @mock.patch('cinder.scheduler.driver.Scheduler.schedule_create_volume') @mock.patch('eventlet.sleep') def test_create_volume_no_delay(self, _mock_sleep, _mock_sched_create): diff --git a/cinder/tests/unit/test_migrations.py b/cinder/tests/unit/test_migrations.py index 782cb86177a..77be3d25f2d 100644 --- a/cinder/tests/unit/test_migrations.py +++ b/cinder/tests/unit/test_migrations.py @@ -812,6 +812,33 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin): fkey, = iscsi_targets.c.volume_id.foreign_keys self.assertIsNotNone(fkey) + def _check_074(self, engine, data): + """Test adding message table.""" + self.assertTrue(engine.dialect.has_table(engine.connect(), + "messages")) + messages = db_utils.get_table(engine, 'messages') + + self.assertIsInstance(messages.c.created_at.type, + self.TIME_TYPE) + self.assertIsInstance(messages.c.deleted_at.type, + self.TIME_TYPE) + self.assertIsInstance(messages.c.deleted.type, + self.BOOL_TYPE) + self.assertIsInstance(messages.c.message_level.type, + self.VARCHAR_TYPE) + self.assertIsInstance(messages.c.project_id.type, + self.VARCHAR_TYPE) + self.assertIsInstance(messages.c.id.type, + self.VARCHAR_TYPE) + self.assertIsInstance(messages.c.request_id.type, + self.VARCHAR_TYPE) + self.assertIsInstance(messages.c.resource_uuid.type, + self.VARCHAR_TYPE) + self.assertIsInstance(messages.c.event_id.type, + self.VARCHAR_TYPE) + self.assertIsInstance(messages.c.resource_type.type, + self.VARCHAR_TYPE) + def test_walk_versions(self): self.walk_versions(False, False) diff --git a/cinder/tests/unit/test_volume.py b/cinder/tests/unit/test_volume.py index cd2ca79a27d..0e5ccfc2626 100644 --- a/cinder/tests/unit/test_volume.py +++ b/cinder/tests/unit/test_volume.py @@ -47,6 +47,8 @@ from cinder import db from cinder import exception from cinder.image import image_utils from cinder import keymgr +from cinder.message import defined_messages +from cinder.message import resource_types from cinder import objects from cinder.objects import fields import cinder.policy @@ -130,6 +132,7 @@ class BaseVolumeTestCase(test.TestCase): notification_driver=["test"]) self.addCleanup(self._cleanup) self.volume = importutils.import_object(CONF.volume_manager) + self.volume.message_api = mock.Mock() self.configuration = mock.Mock(conf.Configuration) self.context = context.get_admin_context() self.context.user_id = fake.user_id @@ -277,6 +280,7 @@ class AvailabilityZoneTestCase(BaseVolumeTestCase): def test_list_availability_zones_refetched(self): timeutils.set_time_override() + self.addCleanup(timeutils.clear_time_override) volume_api = cinder.volume.api.API() with mock.patch.object(volume_api.db, 'service_get_all_by_topic') as get_all: @@ -343,7 +347,6 @@ class AvailabilityZoneTestCase(BaseVolumeTestCase): self.assertEqual(expected, azs) -@ddt.ddt class VolumeTestCase(BaseVolumeTestCase): def setUp(self): @@ -2879,6 +2882,13 @@ class VolumeTestCase(BaseVolumeTestCase): None, mountpoint, 'rw') + + # Assert a user message was created + self.volume.message_api.create.assert_called_once_with( + self.context, defined_messages.ATTACH_READONLY_VOLUME, + self.context.project_id, resource_type=resource_types.VOLUME, + resource_uuid=volume['id']) + vol = db.volume_get(context.get_admin_context(), volume_id) self.assertEqual('error_attaching', vol['status']) self.assertEqual('detached', vol['attach_status']) @@ -4179,17 +4189,27 @@ class VolumeTestCase(BaseVolumeTestCase): fake_new_volume.id) self.assertIsNone(volume.migration_status) - @ddt.data(False, True) - def test_check_volume_filters(self, filter_value): - """Test bootable as filter for True or False""" + def test_check_volume_filters_true(self): + """Test bootable as filter for true""" volume_api = cinder.volume.api.API() - filters = {'bootable': filter_value} + filters = {'bootable': 'TRUE'} # To convert filter value to True or False volume_api.check_volume_filters(filters) - # Confirming converted filter value against True or False - self.assertEqual(filter_value, filters['bootable']) + # Confirming converted filter value against True + self.assertTrue(filters['bootable']) + + def test_check_volume_filters_false(self): + """Test bootable as filter for false""" + volume_api = cinder.volume.api.API() + filters = {'bootable': 'false'} + + # To convert filter value to True or False + volume_api.check_volume_filters(filters) + + # Confirming converted filter value against False + self.assertEqual(False, filters['bootable']) def test_check_volume_filters_invalid(self): """Test bootable as filter""" @@ -4202,43 +4222,6 @@ class VolumeTestCase(BaseVolumeTestCase): # Confirming converted filter value against invalid value self.assertTrue(filters['bootable']) - @ddt.data('False', 'false', 'f', '0') - def test_check_volume_filters_strict_false(self, filter_value): - """Test bootable as filter for False, false, f and 0 values""" - volume_api = cinder.volume.api.API() - filters = {'bootable': filter_value} - - strict = True - # To convert filter value to True or False - volume_api.check_volume_filters(filters, strict) - - # Confirming converted filter value against False - self.assertFalse(filters['bootable']) - - @ddt.data('True', 'true', 't', '1') - def test_check_volume_filters_strict_true(self, filter_value): - """Test bootable as filter for True, true, t, 1 values""" - volume_api = cinder.volume.api.API() - filters = {'bootable': filter_value} - - strict = True - # To convert filter value to True or False - volume_api.check_volume_filters(filters, strict) - - # Confirming converted filter value against True - self.assertTrue(filters['bootable']) - - def test_check_volume_filters_strict_invalid(self): - """Test bootable as filter for invalid value.""" - volume_api = cinder.volume.api.API() - filters = {'bootable': 'invalid'} - - strict = True - # Confirming exception for invalid value in filter - self.assertRaises(exception.InvalidInput, - volume_api.check_volume_filters, - filters, strict) - def test_update_volume_readonly_flag(self): """Test volume readonly flag can be updated at API level.""" # create a volume and assign to host @@ -5989,6 +5972,27 @@ class CopyVolumeToImageTestCase(BaseVolumeTestCase): volume = db.volume_get(self.context, self.volume_id) self.assertEqual('available', volume['status']) + def test_copy_volume_to_image_over_image_quota(self): + # creating volume testdata + self.volume_attrs['instance_uuid'] = None + volume = db.volume_create(self.context, self.volume_attrs) + + with mock.patch.object(self.volume.driver, + 'copy_volume_to_image') as driver_copy_mock: + driver_copy_mock.side_effect = exception.ImageLimitExceeded + + # test with image not in queued state + self.assertRaises(exception.ImageLimitExceeded, + self.volume.copy_volume_to_image, + self.context, + self.volume_id, + self.image_meta) + # Assert a user message was created + self.volume.message_api.create.assert_called_once_with( + self.context, defined_messages.IMAGE_FROM_VOLUME_OVER_QUOTA, + self.context.project_id, resource_type=resource_types.VOLUME, + resource_uuid=volume['id']) + def test_copy_volume_to_image_instance_deleted(self): # During uploading volume to image if instance is deleted, # volume should be in available status. diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index 4f5bc1afba4..aaf9ef9359e 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -61,6 +61,9 @@ from cinder.i18n import _, _LE, _LI, _LW from cinder.image import cache as image_cache from cinder.image import glance from cinder import manager +from cinder.message import api as message_api +from cinder.message import defined_messages +from cinder.message import resource_types from cinder import objects from cinder.objects import fields from cinder import quota @@ -282,6 +285,7 @@ class VolumeManager(manager.SchedulerDependentManager): host=self.host, is_vol_db_empty=vol_db_empty, active_backend_id=curr_active_backend_id) + self.message_api = message_api.API() if CONF.profiler.enabled and profiler is not None: self.driver = profiler.trace_cls("driver")(self.driver) @@ -1000,6 +1004,10 @@ class VolumeManager(manager.SchedulerDependentManager): if volume_metadata.get('readonly') == 'True' and mode != 'ro': self.db.volume_update(context, volume_id, {'status': 'error_attaching'}) + self.message_api.create( + context, defined_messages.ATTACH_READONLY_VOLUME, + context.project_id, resource_type=resource_types.VOLUME, + resource_uuid=volume_id) raise exception.InvalidVolumeAttachMode(mode=mode, volume_id=volume_id) @@ -1317,6 +1325,12 @@ class VolumeManager(manager.SchedulerDependentManager): with excutils.save_and_reraise_exception(): payload['message'] = six.text_type(error) + if isinstance(error, exception.ImageLimitExceeded): + self.message_api.create( + context, defined_messages.IMAGE_FROM_VOLUME_OVER_QUOTA, + context.project_id, + resource_type=resource_types.VOLUME, + resource_uuid=volume_id) finally: self.db.volume_update_status_based_on_attachment(context, volume_id) diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst index c11beeca3fa..ff46f0f3e64 100644 --- a/doc/source/devref/index.rst +++ b/doc/source/devref/index.rst @@ -34,6 +34,7 @@ Programming HowTos and Tutorials drivers gmr replication + user_messages migration api.apache rolling.upgrades diff --git a/doc/source/devref/user_messages.rst b/doc/source/devref/user_messages.rst new file mode 100644 index 00000000000..044972818ff --- /dev/null +++ b/doc/source/devref/user_messages.rst @@ -0,0 +1,73 @@ +User Messages +============= + +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. These messages can be requested via the +/messages API. All user visible messages must be defined in the permitted +messages module in order to prevent sharing sensitive information with users. + + +Example message generation:: + + from cinder import context + from cinder.message import api as message_api + from cinder.message import defined_messages + from cinder.message import resource_types + + self.message_api = message_api.API() + + context = context.RequestContext() + project_id = '6c430ede-9476-4128-8838-8d3929ced223' + volume_id = 'f292cc0c-54a7-4b3b-8174-d2ff82d87008' + + self.message_api.create( + context, + defined_messages.UNABLE_TO_ALLOCATE, + project_id, + resource_type=resource_types.VOLUME, + resource_uuid=volume_id) + +Will produce the following:: + + GET /v3/6c430ede-9476-4128-8838-8d3929ced223/messages + { + "messages": [ + { + "id": "5429fffa-5c76-4d68-a671-37a8e24f37cf", + "event_id": "000002", + "user_message": "No storage could be allocated for this volume request.", + "message_level": "ERROR", + "resource_type": "VOLUME", + "resource_uuid": "f292cc0c-54a7-4b3b-8174-d2ff82d87008", + "created_at": 2015-08-27T09:49:58-05:00, + "guaranteed_until": 2015-09-27T09:49:58-05:00, + "request_id": "req-936666d2-4c8f-4e41-9ac9-237b43f8b848", + } + ] + } + + + +The Message API Module +---------------------- + +.. automodule:: cinder.message.api + :noindex: + :members: + :undoc-members: + +The Resource Types Module +------------------------- + +.. automodule:: cinder.message.resource_types + :noindex: + +The Permitted Messages Module +----------------------------- + +.. automodule:: cinder.message.defined_messages + :noindex: + :members: + :undoc-members: + :show-inheritance: diff --git a/etc/cinder/policy.json b/etc/cinder/policy.json index 29ca512d3dc..461ba7be548 100644 --- a/etc/cinder/policy.json +++ b/etc/cinder/policy.json @@ -102,5 +102,8 @@ "consistencygroup:get_cgsnapshot": "group:nobody", "consistencygroup:get_all_cgsnapshots": "group:nobody", - "scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api" + "scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api", + "message:delete": "rule:admin_or_owner", + "message:get": "rule:admin_or_owner", + "message:get_all": "rule:admin_or_owner" }