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.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"
|
||||
|
||||
|
@ -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.
|
||||
|
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 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'],
|
||||
|
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):
|
||||
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
|
||||
def driver_initiator_data_update(context, initiator, namespace, updates):
|
||||
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))
|
||||
|
||||
|
||||
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'
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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):
|
||||
|
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
|
||||
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,
|
||||
|
@ -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:
|
||||
|
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])
|
||||
|
||||
@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):
|
||||
|
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_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 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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -34,6 +34,7 @@ Programming HowTos and Tutorials
|
||||
drivers
|
||||
gmr
|
||||
replication
|
||||
user_messages
|
||||
migration
|
||||
api.apache
|
||||
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_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