User messages API for error cases
This patch implements basic user messages with the following APIs. GET /messages GET /messages/<message_id> DELETE /messages/<message_id> Implements : blueprint summarymessage Co-Authored-By: Alex Meade <mr.alex.meade@gmail.com> Co-Authored-By: Sheel Rana <ranasheel2000@gmail.com> Change-Id: Id8a4a700c1159be24b15056f401a2ea77804d0a0
This commit is contained in:
parent
db57966c1f
commit
53cfde43b8
@ -49,6 +49,7 @@ REST_API_VERSION_HISTORY = """
|
|||||||
* 3.1 - Adds visibility and protected to _volume_upload_image parameters.
|
* 3.1 - Adds visibility and protected to _volume_upload_image parameters.
|
||||||
* 3.2 - Bootable filters in volume GET call no longer treats all values
|
* 3.2 - Bootable filters in volume GET call no longer treats all values
|
||||||
passed to it as true.
|
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.
|
# minimum version of the API supported.
|
||||||
# Explicitly using /v1 or /v2 enpoints will still work
|
# Explicitly using /v1 or /v2 enpoints will still work
|
||||||
_MIN_API_VERSION = "3.0"
|
_MIN_API_VERSION = "3.0"
|
||||||
_MAX_API_VERSION = "3.2"
|
_MAX_API_VERSION = "3.3"
|
||||||
_LEGACY_API_VERSION1 = "1.0"
|
_LEGACY_API_VERSION1 = "1.0"
|
||||||
_LEGACY_API_VERSION2 = "2.0"
|
_LEGACY_API_VERSION2 = "2.0"
|
||||||
|
|
||||||
|
@ -51,3 +51,7 @@ user documentation.
|
|||||||
bootable filter values.
|
bootable filter values.
|
||||||
But for any other values passed for bootable filter, it will return
|
But for any other values passed for bootable filter, it will return
|
||||||
"Invalid input received: bootable={filter value}' error.
|
"Invalid input received: bootable={filter value}' error.
|
||||||
|
|
||||||
|
3.3
|
||||||
|
---
|
||||||
|
Added /messages API.
|
||||||
|
106
cinder/api/v3/messages.py
Normal file
106
cinder/api/v3/messages.py
Normal file
@ -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))
|
@ -28,6 +28,7 @@ from cinder.api.v2 import snapshot_metadata
|
|||||||
from cinder.api.v2 import snapshots
|
from cinder.api.v2 import snapshots
|
||||||
from cinder.api.v2 import types
|
from cinder.api.v2 import types
|
||||||
from cinder.api.v2 import volume_metadata
|
from cinder.api.v2 import volume_metadata
|
||||||
|
from cinder.api.v3 import messages
|
||||||
from cinder.api.v3 import volumes
|
from cinder.api.v3 import volumes
|
||||||
from cinder.api import versions
|
from cinder.api import versions
|
||||||
|
|
||||||
@ -53,6 +54,11 @@ class APIRouter(cinder.api.openstack.APIRouter):
|
|||||||
collection={'detail': 'GET'},
|
collection={'detail': 'GET'},
|
||||||
member={'action': 'POST'})
|
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()
|
self.resources['types'] = types.create_resource()
|
||||||
mapper.resource("type", "types",
|
mapper.resource("type", "types",
|
||||||
controller=self.resources['types'],
|
controller=self.resources['types'],
|
||||||
|
0
cinder/api/v3/views/__init__.py
Normal file
0
cinder/api/v3/views/__init__.py
Normal file
69
cinder/api/v3/views/messages.py
Normal file
69
cinder/api/v3/views/messages.py
Normal file
@ -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
|
@ -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):
|
def get_model_for_versioned_object(versioned_object):
|
||||||
return IMPL.get_model_for_versioned_object(versioned_object)
|
return IMPL.get_model_for_versioned_object(versioned_object)
|
||||||
|
|
||||||
|
@ -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
|
@require_context
|
||||||
def driver_initiator_data_update(context, initiator, namespace, updates):
|
def driver_initiator_data_update(context, initiator, namespace, updates):
|
||||||
session = get_session()
|
session = get_session()
|
||||||
|
@ -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()
|
@ -599,6 +599,24 @@ class DriverInitiatorData(BASE, models.TimestampMixin, models.ModelBase):
|
|||||||
value = Column(String(255))
|
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):
|
class ImageVolumeCacheEntry(BASE, models.ModelBase):
|
||||||
"""Represents an image volume cache entry"""
|
"""Represents an image volume cache entry"""
|
||||||
__tablename__ = 'image_volume_cache_entries'
|
__tablename__ = 'image_volume_cache_entries'
|
||||||
|
@ -286,6 +286,10 @@ class VolumeNotFound(NotFound):
|
|||||||
message = _("Volume %(volume_id)s could not be found.")
|
message = _("Volume %(volume_id)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class MessageNotFound(NotFound):
|
||||||
|
message = _("Message %(message_id)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
class VolumeAttachmentNotFound(NotFound):
|
class VolumeAttachmentNotFound(NotFound):
|
||||||
message = _("Volume attachment could not be found with "
|
message = _("Volume attachment could not be found with "
|
||||||
"filter: %(filter)s .")
|
"filter: %(filter)s .")
|
||||||
@ -532,6 +536,10 @@ class BackupLimitExceeded(QuotaError):
|
|||||||
message = _("Maximum number of backups allowed (%(allowed)d) exceeded")
|
message = _("Maximum number of backups allowed (%(allowed)d) exceeded")
|
||||||
|
|
||||||
|
|
||||||
|
class ImageLimitExceeded(QuotaError):
|
||||||
|
message = _("Image quota exceeded")
|
||||||
|
|
||||||
|
|
||||||
class DuplicateSfVolumeNames(Duplicate):
|
class DuplicateSfVolumeNames(Duplicate):
|
||||||
message = _("Detected more than one volume with name %(vol_name)s")
|
message = _("Detected more than one volume with name %(vol_name)s")
|
||||||
|
|
||||||
|
@ -219,6 +219,8 @@ class GlanceClientWrapper(object):
|
|||||||
'method': method,
|
'method': method,
|
||||||
'extra': extra})
|
'extra': extra})
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
except glanceclient.exc.HTTPOverLimit as e:
|
||||||
|
raise exception.ImageLimitExceeded(e)
|
||||||
|
|
||||||
|
|
||||||
class GlanceImageService(object):
|
class GlanceImageService(object):
|
||||||
|
0
cinder/message/__init__.py
Normal file
0
cinder/message/__init__.py
Normal file
75
cinder/message/api.py
Normal file
75
cinder/message/api.py
Normal file
@ -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)
|
44
cinder/message/defined_messages.py
Normal file
44
cinder/message/defined_messages.py
Normal file
@ -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]
|
15
cinder/message/resource_types.py
Normal file
15
cinder/message/resource_types.py
Normal file
@ -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'
|
@ -44,6 +44,7 @@ from cinder.image import image_utils as cinder_image_imageutils
|
|||||||
import cinder.keymgr
|
import cinder.keymgr
|
||||||
from cinder.keymgr import conf_key_mgr as cinder_keymgr_confkeymgr
|
from cinder.keymgr import conf_key_mgr as cinder_keymgr_confkeymgr
|
||||||
from cinder.keymgr import key_mgr as cinder_keymgr_keymgr
|
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 import quota as cinder_quota
|
||||||
from cinder.scheduler import driver as cinder_scheduler_driver
|
from cinder.scheduler import driver as cinder_scheduler_driver
|
||||||
from cinder.scheduler import host_manager as cinder_scheduler_hostmanager
|
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_zfssa_zfssanfs.ZFSSA_OPTS,
|
||||||
cinder_volume_drivers_disco_disco.disco_opts,
|
cinder_volume_drivers_disco_disco.disco_opts,
|
||||||
cinder_volume_drivers_hgst.hgst_opts,
|
cinder_volume_drivers_hgst.hgst_opts,
|
||||||
|
cinder_message_api.messages_opts,
|
||||||
cinder_image_imageutils.image_helper_opts,
|
cinder_image_imageutils.image_helper_opts,
|
||||||
cinder_compute_nova.nova_opts,
|
cinder_compute_nova.nova_opts,
|
||||||
cinder_volume_drivers_ibm_flashsystemfc.flashsystem_fc_opts,
|
cinder_volume_drivers_ibm_flashsystemfc.flashsystem_fc_opts,
|
||||||
|
@ -18,6 +18,9 @@ from taskflow.patterns import linear_flow
|
|||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder import flow_utils
|
from cinder import flow_utils
|
||||||
from cinder.i18n import _LE
|
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 rpc
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
from cinder.volume.flows import common
|
from cinder.volume.flows import common
|
||||||
@ -90,6 +93,7 @@ class ScheduleCreateVolumeTask(flow_utils.CinderTask):
|
|||||||
**kwargs)
|
**kwargs)
|
||||||
self.db_api = db_api
|
self.db_api = db_api
|
||||||
self.driver_api = driver_api
|
self.driver_api = driver_api
|
||||||
|
self.message_api = message_api.API()
|
||||||
|
|
||||||
def _handle_failure(self, context, request_spec, cause):
|
def _handle_failure(self, context, request_spec, cause):
|
||||||
try:
|
try:
|
||||||
@ -127,6 +131,13 @@ class ScheduleCreateVolumeTask(flow_utils.CinderTask):
|
|||||||
# reraise (since what's the point?)
|
# reraise (since what's the point?)
|
||||||
with excutils.save_and_reraise_exception(
|
with excutils.save_and_reraise_exception(
|
||||||
reraise=not isinstance(e, exception.NoValidHost)):
|
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:
|
try:
|
||||||
self._handle_failure(context, request_spec, e)
|
self._handle_failure(context, request_spec, e)
|
||||||
finally:
|
finally:
|
||||||
|
42
cinder/tests/unit/api/v3/stubs.py
Normal file
42
cinder/tests/unit/api/v3/stubs.py
Normal file
@ -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)
|
140
cinder/tests/unit/api/v3/test_messages.py
Normal file
140
cinder/tests/unit/api/v3/test_messages.py
Normal file
@ -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)
|
@ -824,6 +824,20 @@ class TestGlanceClientVersion(test.TestCase):
|
|||||||
|
|
||||||
self.assertEqual('2', _mockglanceclient.call_args[0][0])
|
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):
|
def _create_failing_glance_client(info):
|
||||||
class MyGlanceStubClient(glance_stubs.StubGlanceClient):
|
class MyGlanceStubClient(glance_stubs.StubGlanceClient):
|
||||||
|
0
cinder/tests/unit/message/__init__.py
Normal file
0
cinder/tests/unit/message/__init__.py
Normal file
94
cinder/tests/unit/message/test_api.py
Normal file
94
cinder/tests/unit/message/test_api.py
Normal file
@ -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')
|
@ -107,5 +107,9 @@
|
|||||||
"consistencygroup:get_cgsnapshot": "",
|
"consistencygroup:get_cgsnapshot": "",
|
||||||
"consistencygroup:get_all_cgsnapshots": "",
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ from oslo_config import cfg
|
|||||||
from cinder import context
|
from cinder import context
|
||||||
from cinder import db
|
from cinder import db
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
|
from cinder.message import defined_messages
|
||||||
from cinder.objects import fields
|
from cinder.objects import fields
|
||||||
from cinder.scheduler import driver
|
from cinder.scheduler import driver
|
||||||
from cinder.scheduler import filter_scheduler
|
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_update_cap.assert_called_once_with(service, host, capabilities)
|
||||||
|
|
||||||
@mock.patch('cinder.scheduler.driver.Scheduler.schedule_create_volume')
|
@mock.patch('cinder.scheduler.driver.Scheduler.schedule_create_volume')
|
||||||
|
@mock.patch('cinder.message.api.API.create')
|
||||||
@mock.patch('cinder.db.volume_update')
|
@mock.patch('cinder.db.volume_update')
|
||||||
def test_create_volume_exception_puts_volume_in_error_state(
|
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.
|
# Test NoValidHost exception behavior for create_volume.
|
||||||
# Puts the volume in 'error' state and eats the exception.
|
# Puts the volume in 'error' state and eats the exception.
|
||||||
_mock_sched_create.side_effect = exception.NoValidHost(reason="")
|
_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_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('cinder.scheduler.driver.Scheduler.schedule_create_volume')
|
||||||
@mock.patch('eventlet.sleep')
|
@mock.patch('eventlet.sleep')
|
||||||
def test_create_volume_no_delay(self, _mock_sleep, _mock_sched_create):
|
def test_create_volume_no_delay(self, _mock_sleep, _mock_sched_create):
|
||||||
|
@ -812,6 +812,33 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin):
|
|||||||
fkey, = iscsi_targets.c.volume_id.foreign_keys
|
fkey, = iscsi_targets.c.volume_id.foreign_keys
|
||||||
self.assertIsNotNone(fkey)
|
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):
|
def test_walk_versions(self):
|
||||||
self.walk_versions(False, False)
|
self.walk_versions(False, False)
|
||||||
|
|
||||||
|
@ -47,6 +47,8 @@ from cinder import db
|
|||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.image import image_utils
|
from cinder.image import image_utils
|
||||||
from cinder import keymgr
|
from cinder import keymgr
|
||||||
|
from cinder.message import defined_messages
|
||||||
|
from cinder.message import resource_types
|
||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder.objects import fields
|
from cinder.objects import fields
|
||||||
import cinder.policy
|
import cinder.policy
|
||||||
@ -130,6 +132,7 @@ class BaseVolumeTestCase(test.TestCase):
|
|||||||
notification_driver=["test"])
|
notification_driver=["test"])
|
||||||
self.addCleanup(self._cleanup)
|
self.addCleanup(self._cleanup)
|
||||||
self.volume = importutils.import_object(CONF.volume_manager)
|
self.volume = importutils.import_object(CONF.volume_manager)
|
||||||
|
self.volume.message_api = mock.Mock()
|
||||||
self.configuration = mock.Mock(conf.Configuration)
|
self.configuration = mock.Mock(conf.Configuration)
|
||||||
self.context = context.get_admin_context()
|
self.context = context.get_admin_context()
|
||||||
self.context.user_id = fake.user_id
|
self.context.user_id = fake.user_id
|
||||||
@ -277,6 +280,7 @@ class AvailabilityZoneTestCase(BaseVolumeTestCase):
|
|||||||
|
|
||||||
def test_list_availability_zones_refetched(self):
|
def test_list_availability_zones_refetched(self):
|
||||||
timeutils.set_time_override()
|
timeutils.set_time_override()
|
||||||
|
self.addCleanup(timeutils.clear_time_override)
|
||||||
volume_api = cinder.volume.api.API()
|
volume_api = cinder.volume.api.API()
|
||||||
with mock.patch.object(volume_api.db,
|
with mock.patch.object(volume_api.db,
|
||||||
'service_get_all_by_topic') as get_all:
|
'service_get_all_by_topic') as get_all:
|
||||||
@ -343,7 +347,6 @@ class AvailabilityZoneTestCase(BaseVolumeTestCase):
|
|||||||
self.assertEqual(expected, azs)
|
self.assertEqual(expected, azs)
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
|
||||||
class VolumeTestCase(BaseVolumeTestCase):
|
class VolumeTestCase(BaseVolumeTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -2879,6 +2882,13 @@ class VolumeTestCase(BaseVolumeTestCase):
|
|||||||
None,
|
None,
|
||||||
mountpoint,
|
mountpoint,
|
||||||
'rw')
|
'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)
|
vol = db.volume_get(context.get_admin_context(), volume_id)
|
||||||
self.assertEqual('error_attaching', vol['status'])
|
self.assertEqual('error_attaching', vol['status'])
|
||||||
self.assertEqual('detached', vol['attach_status'])
|
self.assertEqual('detached', vol['attach_status'])
|
||||||
@ -4179,17 +4189,27 @@ class VolumeTestCase(BaseVolumeTestCase):
|
|||||||
fake_new_volume.id)
|
fake_new_volume.id)
|
||||||
self.assertIsNone(volume.migration_status)
|
self.assertIsNone(volume.migration_status)
|
||||||
|
|
||||||
@ddt.data(False, True)
|
def test_check_volume_filters_true(self):
|
||||||
def test_check_volume_filters(self, filter_value):
|
"""Test bootable as filter for true"""
|
||||||
"""Test bootable as filter for True or False"""
|
|
||||||
volume_api = cinder.volume.api.API()
|
volume_api = cinder.volume.api.API()
|
||||||
filters = {'bootable': filter_value}
|
filters = {'bootable': 'TRUE'}
|
||||||
|
|
||||||
# To convert filter value to True or False
|
# To convert filter value to True or False
|
||||||
volume_api.check_volume_filters(filters)
|
volume_api.check_volume_filters(filters)
|
||||||
|
|
||||||
# Confirming converted filter value against True or False
|
# Confirming converted filter value against True
|
||||||
self.assertEqual(filter_value, filters['bootable'])
|
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):
|
def test_check_volume_filters_invalid(self):
|
||||||
"""Test bootable as filter"""
|
"""Test bootable as filter"""
|
||||||
@ -4202,43 +4222,6 @@ class VolumeTestCase(BaseVolumeTestCase):
|
|||||||
# Confirming converted filter value against invalid value
|
# Confirming converted filter value against invalid value
|
||||||
self.assertTrue(filters['bootable'])
|
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):
|
def test_update_volume_readonly_flag(self):
|
||||||
"""Test volume readonly flag can be updated at API level."""
|
"""Test volume readonly flag can be updated at API level."""
|
||||||
# create a volume and assign to host
|
# create a volume and assign to host
|
||||||
@ -5989,6 +5972,27 @@ class CopyVolumeToImageTestCase(BaseVolumeTestCase):
|
|||||||
volume = db.volume_get(self.context, self.volume_id)
|
volume = db.volume_get(self.context, self.volume_id)
|
||||||
self.assertEqual('available', volume['status'])
|
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):
|
def test_copy_volume_to_image_instance_deleted(self):
|
||||||
# During uploading volume to image if instance is deleted,
|
# During uploading volume to image if instance is deleted,
|
||||||
# volume should be in available status.
|
# volume should be in available status.
|
||||||
|
@ -61,6 +61,9 @@ from cinder.i18n import _, _LE, _LI, _LW
|
|||||||
from cinder.image import cache as image_cache
|
from cinder.image import cache as image_cache
|
||||||
from cinder.image import glance
|
from cinder.image import glance
|
||||||
from cinder import manager
|
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 import objects
|
||||||
from cinder.objects import fields
|
from cinder.objects import fields
|
||||||
from cinder import quota
|
from cinder import quota
|
||||||
@ -282,6 +285,7 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
host=self.host,
|
host=self.host,
|
||||||
is_vol_db_empty=vol_db_empty,
|
is_vol_db_empty=vol_db_empty,
|
||||||
active_backend_id=curr_active_backend_id)
|
active_backend_id=curr_active_backend_id)
|
||||||
|
self.message_api = message_api.API()
|
||||||
|
|
||||||
if CONF.profiler.enabled and profiler is not None:
|
if CONF.profiler.enabled and profiler is not None:
|
||||||
self.driver = profiler.trace_cls("driver")(self.driver)
|
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':
|
if volume_metadata.get('readonly') == 'True' and mode != 'ro':
|
||||||
self.db.volume_update(context, volume_id,
|
self.db.volume_update(context, volume_id,
|
||||||
{'status': 'error_attaching'})
|
{'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,
|
raise exception.InvalidVolumeAttachMode(mode=mode,
|
||||||
volume_id=volume_id)
|
volume_id=volume_id)
|
||||||
|
|
||||||
@ -1317,6 +1325,12 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
|
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
payload['message'] = six.text_type(error)
|
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:
|
finally:
|
||||||
self.db.volume_update_status_based_on_attachment(context,
|
self.db.volume_update_status_based_on_attachment(context,
|
||||||
volume_id)
|
volume_id)
|
||||||
|
@ -34,6 +34,7 @@ Programming HowTos and Tutorials
|
|||||||
drivers
|
drivers
|
||||||
gmr
|
gmr
|
||||||
replication
|
replication
|
||||||
|
user_messages
|
||||||
migration
|
migration
|
||||||
api.apache
|
api.apache
|
||||||
rolling.upgrades
|
rolling.upgrades
|
||||||
|
73
doc/source/devref/user_messages.rst
Normal file
73
doc/source/devref/user_messages.rst
Normal file
@ -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:
|
@ -102,5 +102,8 @@
|
|||||||
"consistencygroup:get_cgsnapshot": "group:nobody",
|
"consistencygroup:get_cgsnapshot": "group:nobody",
|
||||||
"consistencygroup:get_all_cgsnapshots": "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"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user