User Messages
For quite some time, OpenStack services have wanted to be able to send messages to API end users (by user I do not mean the operator, but the user that is interacting with the client). This patch implements basic user messages with the following APIs. GET /messages GET /messages/<message_id> DELETE /messages/<message_id> Implements the basic /messages resource and tempest tests The patch is aligned with related cinder patch where possible: I8a635a07ed6ff93ccb71df8c404c927d1ecef005 DocImpact APIImpact Needed-By: I5ffb840a271c518f62ee1accfd8e20a97f45594d Needed-By: I9ce096eebda3249687268e361b7141dea4032b57 Needed-By: Ic7d25a144905a39c56ababe8bd666b01bc0d0aef Partially-implements: blueprint user-messages Co-Authored-By: Jan Provaznik <jprovazn@redhat.com> Change-Id: Ia0cc524e0bfb2ca5e495e575e17e9911c746690b
This commit is contained in:
parent
dadf6efea2
commit
dd630c3929
@ -25,6 +25,7 @@ Shared File Systems API
|
||||
.. include:: availability-zones.inc
|
||||
.. include:: os-share-manage.inc
|
||||
.. include:: quota-sets.inc
|
||||
.. include:: user-messages.inc
|
||||
|
||||
======================================
|
||||
Shared File Systems API (EXPERIMENTAL)
|
||||
|
@ -50,6 +50,12 @@ export_location_id_path:
|
||||
in: path
|
||||
required: false
|
||||
type: string
|
||||
message_id:
|
||||
description: |
|
||||
The UUID of the message.
|
||||
in: path
|
||||
required: false
|
||||
type: string
|
||||
security_service_id_path:
|
||||
description: |
|
||||
The UUID of the security service.
|
||||
@ -121,6 +127,12 @@ tenant_id_path:
|
||||
type: string
|
||||
|
||||
# variables in query
|
||||
action_id:
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
description: >
|
||||
The ID of the action during which the message was created.
|
||||
all_tenants:
|
||||
description: |
|
||||
(Admin only). Defines whether to list shares for
|
||||
@ -206,6 +218,12 @@ consistency_group_id_5:
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
detail_id:
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
description: >
|
||||
The ID of the message detail.
|
||||
export_location_id_query:
|
||||
description: |
|
||||
The export location UUID that can be used to filter shares or
|
||||
@ -275,6 +293,12 @@ media_types:
|
||||
in: query
|
||||
required: false
|
||||
type: object
|
||||
message_level:
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
description: >
|
||||
The message level.
|
||||
metadata_1:
|
||||
description: |
|
||||
One or more metadata key-value pairs, as a
|
||||
@ -316,6 +340,30 @@ project_id_6:
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
project_id_messages:
|
||||
description: |
|
||||
The UUID of the project for which the message was created.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
request_id:
|
||||
description: |
|
||||
The UUID of the request during which the message was created.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_id:
|
||||
description: |
|
||||
The UUID of the resource for which the message was created.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_type:
|
||||
description: |
|
||||
The type of the resource for which the message was created.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
service_binary_query:
|
||||
description: |
|
||||
The service binary name. Default is the base name
|
||||
@ -399,6 +447,15 @@ sort_key:
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
sort_key_messages:
|
||||
description: |
|
||||
The key to sort a list of messages. A valid value
|
||||
is ``id``, ``project_id``, ``request_id``, ``resource_type``,
|
||||
``action_id``, ``detail_id``, ``resource_id``, ``message_level``,
|
||||
``expires_at``, ``created_at``.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
state_2:
|
||||
description: |
|
||||
The current state of the service. A valid value
|
||||
@ -577,6 +634,12 @@ access_type:
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
action_id_body:
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
The ID of the action during which the message was created.
|
||||
alias:
|
||||
description: |
|
||||
The alias for the extension. For example,
|
||||
@ -1205,6 +1268,12 @@ description_9:
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
detail_id_body:
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
The ID of the message detail.
|
||||
disabled:
|
||||
description: |
|
||||
Indicates whether the service is disabled.
|
||||
@ -1782,6 +1851,24 @@ maxTotalSnapshotGigabytes:
|
||||
in: body
|
||||
required: true
|
||||
type: integer
|
||||
message_level_body:
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
The message level.
|
||||
message_links:
|
||||
description: |
|
||||
The message links.
|
||||
in: body
|
||||
required: true
|
||||
type: array
|
||||
message_members_links:
|
||||
description: |
|
||||
The message member links.
|
||||
in: body
|
||||
required: true
|
||||
type: array
|
||||
metadata:
|
||||
description: |
|
||||
One or more metadata key and value pairs as a
|
||||
@ -2114,6 +2201,12 @@ project_id_9:
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
project_id_messages_body:
|
||||
description: |
|
||||
The UUID of the project for which the message was created.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
protocol:
|
||||
description: |
|
||||
The Shared File Systems protocol of the share to
|
||||
@ -2292,6 +2385,12 @@ replication_type:
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
request_id_body:
|
||||
description: |
|
||||
The UUID of the request during which the message was created.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
required_extra_specs:
|
||||
description: |
|
||||
The required extra specifications for the share
|
||||
@ -2312,6 +2411,18 @@ reset_status:
|
||||
in: body
|
||||
required: true
|
||||
type: object
|
||||
resource_id_body:
|
||||
description: |
|
||||
The UUID of the resource for which the message was created.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
resource_type_body:
|
||||
description: |
|
||||
The type of the resource for which the message was created.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
security_service_created_at:
|
||||
description: |
|
||||
The date and time stamp when the security service was created.
|
||||
|
24
api-ref/source/samples/user-message-show-response.json
Normal file
24
api-ref/source/samples/user-message-show-response.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"message": {
|
||||
"links": [
|
||||
{
|
||||
"href": "http://192.168.122.180:8786/v2/2e3de76b49b444fd9dc7ca9f7048ce6b/messages/4b319d29-d5b7-4b6e-8e7c-8d6e53f3c3d5",
|
||||
"rel": "self"
|
||||
}, {
|
||||
"href": "http://192.168.122.180:8786/2e3de76b49b444fd9dc7ca9f7048ce6b/messages/4b319d29-d5b7-4b6e-8e7c-8d6e53f3c3d5",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
],
|
||||
"resource_id": "351cc796-2d79-4a08-b878-a8ed933b6b68",
|
||||
"message_level": "ERROR",
|
||||
"user_message": "allocate host: No storage could be allocated for this share request. Trying again with a different size or share type may succeed.",
|
||||
"expires_at": "2017-07-10T10:27:43.000000",
|
||||
"id": "4b319d29-d5b7-4b6e-8e7c-8d6e53f3c3d5",
|
||||
"created_at": "2017-07-10T10:26:43.000000",
|
||||
"detail_id": "002",
|
||||
"request_id": "req-24e7ccb6-a7d5-4ddd-a8e4-d8f72a4509c8",
|
||||
"project_id": "2e3de76b49b444fd9dc7ca9f7048ce6b",
|
||||
"resource_type": "SHARE",
|
||||
"action_id": "001"
|
||||
}
|
||||
}
|
26
api-ref/source/samples/user-messages-list-response.json
Normal file
26
api-ref/source/samples/user-messages-list-response.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"links": [
|
||||
{
|
||||
"href": "http://192.168.122.180:8786/v2/2e3de76b49b444fd9dc7ca9f7048ce6b/messages/4b319d29-d5b7-4b6e-8e7c-8d6e53f3c3d5",
|
||||
"rel": "self"
|
||||
}, {
|
||||
"href": "http://192.168.122.180:8786/2e3de76b49b444fd9dc7ca9f7048ce6b/messages/4b319d29-d5b7-4b6e-8e7c-8d6e53f3c3d5",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
],
|
||||
"id": "4b319d29-d5b7-4b6e-8e7c-8d6e53f3c3d5",
|
||||
"resource_id": "351cc796-2d79-4a08-b878-a8ed933b6b68",
|
||||
"message_level": "ERROR",
|
||||
"user_message": "allocate host: No storage could be allocated for this share request. Trying again with a different size or share type may succeed.",
|
||||
"expires_at": "2017-07-10T10:27:43.000000",
|
||||
"created_at": "2017-07-10T10:26:43.000000",
|
||||
"detail_id": "002",
|
||||
"request_id": "req-24e7ccb6-a7d5-4ddd-a8e4-d8f72a4509c8",
|
||||
"project_id": "2e3de76b49b444fd9dc7ca9f7048ce6b",
|
||||
"resource_type": "SHARE",
|
||||
"action_id": "001"
|
||||
}
|
||||
]
|
||||
}
|
119
api-ref/source/user-messages.inc
Normal file
119
api-ref/source/user-messages.inc
Normal file
@ -0,0 +1,119 @@
|
||||
.. -*- rst -*-
|
||||
|
||||
==============================
|
||||
User messages (since API 2.37)
|
||||
==============================
|
||||
|
||||
Lists, shows and deletes user messages.
|
||||
|
||||
|
||||
List user messages
|
||||
==================
|
||||
|
||||
.. rest_method:: GET /v2/{tenant_id}/messages
|
||||
|
||||
Lists all user messages.
|
||||
|
||||
|
||||
Normal response codes: 200
|
||||
Error response codes: badRequest(400), unauthorized(401), forbidden(403)
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- tenant_id: tenant_id_1
|
||||
- limit: limit
|
||||
- offset: offset
|
||||
- sort_key: sort_key_messages
|
||||
- sort_dir: sort_dir
|
||||
- action_id: action_id
|
||||
- detail_id: detail_id
|
||||
- message_level: message_level
|
||||
- project_id: project_id_messages
|
||||
- request_id: request_id
|
||||
- resource_id: resource_id
|
||||
- resource_type: resource_type
|
||||
|
||||
Response parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- action_id: action_id_body
|
||||
- detail_id: detail_id_body
|
||||
- message_level: message_level_body
|
||||
- project_id: project_id_messages_body
|
||||
- request_id: request_id_body
|
||||
- resource_id: resource_id_body
|
||||
- resource_type: resource_type_body
|
||||
- message_members_links: message_members_links
|
||||
|
||||
|
||||
Response example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: samples/user-messages-list-response.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
Show user message details
|
||||
=========================
|
||||
|
||||
.. rest_method:: GET /v2/{tenant_id}/messages/{message_id}
|
||||
|
||||
Shows details for a user message.
|
||||
|
||||
Normal response codes: 200
|
||||
Error response codes: badRequest(400), unauthorized(401), forbidden(403),
|
||||
itemNotFound(404)
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- tenant_id: tenant_id_1
|
||||
- message_id: message_id
|
||||
|
||||
Response parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- action_id: action_id_body
|
||||
- detail_id: detail_id_body
|
||||
- message_level: message_level_body
|
||||
- project_id: project_id_messages_body
|
||||
- request_id: request_id_body
|
||||
- resource_id: resource_id_body
|
||||
- resource_type: resource_type_body
|
||||
- message_links: message_links
|
||||
|
||||
|
||||
Response example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: samples/user-message-show-response.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
Delete message
|
||||
==============
|
||||
|
||||
.. rest_method:: DELETE /v2/{tenant_id}/messages/{message_id}
|
||||
|
||||
Deletes a user message.
|
||||
|
||||
Normal response codes: 202
|
||||
Error response codes: badRequest(400), unauthorized(401), forbidden(403),
|
||||
itemNotFound(404)
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- tenant_id: tenant_id_1
|
||||
- message_id: message_id
|
@ -35,6 +35,7 @@ Programming HowTos and Tutorials
|
||||
adding_release_notes
|
||||
commit_message_tags
|
||||
guru_meditation_report
|
||||
user_messages
|
||||
|
||||
|
||||
Background Concepts for manila
|
||||
|
70
doc/source/devref/user_messages.rst
Normal file
70
doc/source/devref/user_messages.rst
Normal file
@ -0,0 +1,70 @@
|
||||
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 share
|
||||
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 manila import context
|
||||
from manila.message import api as message_api
|
||||
from manila.message import message_field
|
||||
|
||||
self.message_api = message_api.API()
|
||||
|
||||
context = context.RequestContext()
|
||||
project_id = '6c430ede-9476-4128-8838-8d3929ced223'
|
||||
share_id = 'f292cc0c-54a7-4b3b-8174-d2ff82d87008'
|
||||
|
||||
self.message_api.create(
|
||||
context,
|
||||
message_field.Actions.CREATE,
|
||||
project_id,
|
||||
resource_type=message_field.Resource.SHARE,
|
||||
resource_id=SHARE_id,
|
||||
detail=message_field.Detail.NO_VALID_HOST)
|
||||
|
||||
Will produce the following::
|
||||
|
||||
GET /v2/6c430ede-9476-4128-8838-8d3929ced223/messages
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": "5429fffa-5c76-4d68-a671-37a8e24f37cf",
|
||||
"action_id": "001",
|
||||
"detail_id": "002",
|
||||
"user_message": "create: No storage could be allocated for this share "
|
||||
"request. Trying again with a different size "
|
||||
"or share type may succeed."",
|
||||
"message_level": "ERROR",
|
||||
"resource_type": "SHARE",
|
||||
"resource_id": "f292cc0c-54a7-4b3b-8174-d2ff82d87008",
|
||||
"created_at": 2015-08-27T09:49:58-05:00,
|
||||
"expires_at": 2015-09-26T09:49:58-05:00,
|
||||
"request_id": "req-936666d2-4c8f-4e41-9ac9-237b43f8b848",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
The Message API Module
|
||||
----------------------
|
||||
|
||||
.. automodule:: manila.message.api
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
The Permitted Messages Module
|
||||
-----------------------------
|
||||
|
||||
.. automodule:: manila.message.message_field
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
@ -155,5 +155,9 @@
|
||||
"share_group_types_spec:update": "rule:admin_api",
|
||||
"share_group_types_spec:show": "rule:admin_api",
|
||||
"share_group_types_spec:index": "rule:admin_api",
|
||||
"share_group_types_spec:delete": "rule:admin_api"
|
||||
"share_group_types_spec:delete": "rule:admin_api",
|
||||
|
||||
"message:delete": "rule:default",
|
||||
"message:get": "rule:default",
|
||||
"message:get_all": "rule:default"
|
||||
}
|
||||
|
@ -106,6 +106,7 @@ REST_API_VERSION_HISTORY = """
|
||||
and export_location_path.
|
||||
* 2.36 - Added like filter support in ``shares``, ``snapshots``,
|
||||
``share-networks``, ``share-groups`` list APIs.
|
||||
* 2.37 - Added /messages APIs.
|
||||
|
||||
"""
|
||||
|
||||
@ -113,7 +114,7 @@ REST_API_VERSION_HISTORY = """
|
||||
# The default api version request is defined to be the
|
||||
# minimum version of the API supported.
|
||||
_MIN_API_VERSION = "2.0"
|
||||
_MAX_API_VERSION = "2.36"
|
||||
_MAX_API_VERSION = "2.37"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
||||
|
@ -210,3 +210,7 @@ user documentation.
|
||||
----
|
||||
Added like filter support in ``shares``, ``snapshots``, ``share-networks``,
|
||||
``share-groups`` list APIs.
|
||||
|
||||
2.37
|
||||
----
|
||||
Added /messages APIs.
|
||||
|
95
manila/api/v2/messages.py
Normal file
95
manila/api/v2/messages.py
Normal file
@ -0,0 +1,95 @@
|
||||
# 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 controller module.
|
||||
|
||||
This module handles the following requests:
|
||||
GET /messages
|
||||
GET /messages/<message_id>
|
||||
DELETE /messages/<message_id>
|
||||
"""
|
||||
|
||||
import webob
|
||||
from webob import exc
|
||||
|
||||
from manila.api import common
|
||||
from manila.api.openstack import wsgi
|
||||
from manila.api.views import messages as messages_view
|
||||
from manila import exception
|
||||
from manila.message import api as message_api
|
||||
|
||||
MESSAGES_BASE_MICRO_VERSION = '2.37'
|
||||
|
||||
|
||||
class MessagesController(wsgi.Controller):
|
||||
"""The User Messages API controller for the OpenStack API."""
|
||||
|
||||
_view_builder_class = messages_view.ViewBuilder
|
||||
resource_name = 'message'
|
||||
|
||||
def __init__(self):
|
||||
self.message_api = message_api.API()
|
||||
super(MessagesController, self).__init__()
|
||||
|
||||
@wsgi.Controller.api_version(MESSAGES_BASE_MICRO_VERSION)
|
||||
@wsgi.Controller.authorize('get')
|
||||
def show(self, req, id):
|
||||
"""Return the given message."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
try:
|
||||
message = self.message_api.get(context, id)
|
||||
except exception.MessageNotFound as error:
|
||||
raise exc.HTTPNotFound(explanation=error.msg)
|
||||
|
||||
return self._view_builder.detail(req, message)
|
||||
|
||||
@wsgi.Controller.api_version(MESSAGES_BASE_MICRO_VERSION)
|
||||
@wsgi.Controller.authorize
|
||||
@wsgi.action("delete")
|
||||
def delete(self, req, id):
|
||||
"""Delete a message."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
try:
|
||||
message = self.message_api.get(context, id)
|
||||
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)
|
||||
@wsgi.Controller.authorize('get_all')
|
||||
def index(self, req):
|
||||
"""Returns a list of messages, transformed through view builder."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
search_opts = {}
|
||||
search_opts.update(req.GET)
|
||||
|
||||
# Remove keys that are not related to message attrs
|
||||
search_opts.pop('limit', None)
|
||||
search_opts.pop('marker', None)
|
||||
sort_key = search_opts.pop('sort_key', 'created_at')
|
||||
sort_dir = search_opts.pop('sort_dir', 'desc')
|
||||
|
||||
messages = self.message_api.get_all(
|
||||
context, search_opts=search_opts, sort_dir=sort_dir,
|
||||
sort_key=sort_key)
|
||||
limited_list = common.limited(messages, req)
|
||||
|
||||
return self._view_builder.index(req, limited_list)
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(MessagesController())
|
@ -31,6 +31,7 @@ from manila.api.v1 import share_servers
|
||||
from manila.api.v1 import share_types_extra_specs
|
||||
from manila.api.v1 import share_unmanage
|
||||
from manila.api.v2 import availability_zones
|
||||
from manila.api.v2 import messages
|
||||
from manila.api.v2 import quota_class_sets
|
||||
from manila.api.v2 import quota_sets
|
||||
from manila.api.v2 import services
|
||||
@ -410,3 +411,7 @@ class APIRouter(manila.api.openstack.APIRouter):
|
||||
controller=self.resources['share-replicas'],
|
||||
collection={'detail': 'GET'},
|
||||
member={'action': 'POST'})
|
||||
|
||||
self.resources['messages'] = messages.create_resource()
|
||||
mapper.resource("message", "messages",
|
||||
controller=self.resources['messages'])
|
||||
|
68
manila/api/views/messages.py
Normal file
68
manila/api/views/messages.py
Normal file
@ -0,0 +1,68 @@
|
||||
# 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 manila.api import common
|
||||
from manila.message import message_field
|
||||
|
||||
|
||||
class ViewBuilder(common.ViewBuilder):
|
||||
"""Model a server API response as a python dictionary."""
|
||||
|
||||
_collection_name = "messages"
|
||||
|
||||
def index(self, request, messages):
|
||||
"""Show a list of messages."""
|
||||
return self._list_view(self.detail, request, messages)
|
||||
|
||||
def detail(self, request, message):
|
||||
"""Detailed view of a single message."""
|
||||
message_ref = {
|
||||
'id': message.get('id'),
|
||||
'project_id': message.get('project_id'),
|
||||
'action_id': message.get('action_id'),
|
||||
'detail_id': message.get('detail_id'),
|
||||
'message_level': message.get('message_level'),
|
||||
'created_at': message.get('created_at'),
|
||||
'expires_at': message.get('expires_at'),
|
||||
'request_id': message.get('request_id'),
|
||||
'links': self._get_links(request, message['id']),
|
||||
'resource_type': message.get('resource_type'),
|
||||
'resource_id': message.get('resource_id'),
|
||||
'user_message': "%s: %s" % (
|
||||
message_field.translate_action(message.get('action_id')),
|
||||
message_field.translate_detail(message.get('detail_id'))),
|
||||
}
|
||||
|
||||
return {'message': message_ref}
|
||||
|
||||
def _list_view(self, func, request, messages, 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 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)
|
||||
messages_dict = dict({"messages": messages_list})
|
||||
|
||||
if messages_links:
|
||||
messages_dict['messages_links'] = messages_links
|
||||
|
||||
return messages_dict
|
@ -1259,3 +1259,27 @@ def share_group_type_specs_update_or_create(context, type_id, group_specs):
|
||||
"""
|
||||
return IMPL.share_group_type_specs_update_or_create(
|
||||
context, type_id, group_specs)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
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, filters=None, sort_key=None, sort_dir=None):
|
||||
"""Returns all messages with the project of the specified context."""
|
||||
return IMPL.message_get_all(context, filters=filters, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
|
||||
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)
|
||||
|
@ -0,0 +1,66 @@
|
||||
# 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.
|
||||
|
||||
"""Add messages table
|
||||
|
||||
Revision ID: 238720805ce1
|
||||
Revises: 31252d671ae5
|
||||
Create Date: 2017-02-02 08:38:55.134095
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '238720805ce1'
|
||||
down_revision = '31252d671ae5'
|
||||
|
||||
from alembic import op
|
||||
from oslo_log import log
|
||||
from sqlalchemy import Column, DateTime
|
||||
from sqlalchemy import MetaData, String, Table
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade():
|
||||
meta = MetaData()
|
||||
meta.bind = op.get_bind()
|
||||
|
||||
# New table
|
||||
messages = Table(
|
||||
'messages',
|
||||
meta,
|
||||
Column('id', String(36), primary_key=True, nullable=False),
|
||||
Column('project_id', String(255), nullable=False),
|
||||
Column('request_id', String(255), nullable=True),
|
||||
Column('resource_type', String(255)),
|
||||
Column('resource_id', String(36), nullable=True),
|
||||
Column('action_id', String(10), nullable=False),
|
||||
Column('detail_id', String(10), nullable=True),
|
||||
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', String(36)),
|
||||
Column('expires_at', DateTime(timezone=False)),
|
||||
mysql_engine='InnoDB',
|
||||
mysql_charset='utf8'
|
||||
)
|
||||
|
||||
messages.create()
|
||||
|
||||
|
||||
def downgrade():
|
||||
try:
|
||||
op.drop_table('messages')
|
||||
except Exception:
|
||||
LOG.error("messages table not dropped")
|
||||
raise
|
@ -4569,3 +4569,66 @@ def share_group_type_specs_update_or_create(context, type_id, specs):
|
||||
spec_ref.save(session=session)
|
||||
|
||||
return specs
|
||||
|
||||
|
||||
###############################
|
||||
|
||||
|
||||
@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 result
|
||||
|
||||
|
||||
@require_context
|
||||
def message_get_all(context, filters=None, sort_key='created_at',
|
||||
sort_dir='asc'):
|
||||
messages = models.Message
|
||||
query = model_query(context,
|
||||
messages,
|
||||
read_deleted="no",
|
||||
project_only="yes")
|
||||
|
||||
legal_filter_keys = ('request_id', 'resource_type', 'resource_id',
|
||||
'action_id', 'detail_id', 'message_level')
|
||||
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
query = exact_filter(query, messages, filters, legal_filter_keys)
|
||||
try:
|
||||
query = apply_sorting(messages, query, sort_key, sort_dir)
|
||||
except AttributeError:
|
||||
msg = _("Wrong sorting key provided - '%s'.") % sort_key
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
return query.all()
|
||||
|
||||
|
||||
@require_context
|
||||
def message_create(context, message_values):
|
||||
values = copy.deepcopy(message_values)
|
||||
message_ref = models.Message()
|
||||
if not values.get('id'):
|
||||
values['id'] = uuidutils.generate_uuid()
|
||||
message_ref.update(values)
|
||||
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
session.add(message_ref)
|
||||
|
||||
return message_get(context, message_ref['id'])
|
||||
|
||||
|
||||
@require_context
|
||||
def message_destroy(context, message):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
(model_query(context, models.Message, session=session).
|
||||
filter_by(id=message.get('id')).soft_delete())
|
||||
|
@ -1178,6 +1178,31 @@ class ShareGroupShareTypeMapping(BASE, ManilaBase):
|
||||
)
|
||||
|
||||
|
||||
class Message(BASE, ManilaBase):
|
||||
"""Represents a user message.
|
||||
|
||||
User messages show information about API operations to the API end-user.
|
||||
"""
|
||||
__tablename__ = 'messages'
|
||||
id = Column(String(36), primary_key=True, nullable=False)
|
||||
project_id = Column(String(255), 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_id = Column(String(36), nullable=True)
|
||||
# Operation specific action ID, this ID is mapped
|
||||
# to a message in manila/message/message_field.py
|
||||
action_id = Column(String(10), nullable=False)
|
||||
# After this time the message may no longer exist.
|
||||
expires_at = Column(DateTime, nullable=True)
|
||||
# Message detail ID, this ID is mapped
|
||||
# to a message in manila/message/message_field.py
|
||||
detail_id = Column(String(10), nullable=True)
|
||||
deleted = Column(String(36), default='False')
|
||||
|
||||
|
||||
def register_models():
|
||||
"""Register Models and create metadata.
|
||||
|
||||
|
@ -206,6 +206,10 @@ class NotFound(ManilaException):
|
||||
safe = True
|
||||
|
||||
|
||||
class MessageNotFound(NotFound):
|
||||
message = _("Message %(message_id)s could not be found.")
|
||||
|
||||
|
||||
class Found(ManilaException):
|
||||
message = _("Resource was found.")
|
||||
code = 302
|
||||
|
0
manila/message/__init__.py
Normal file
0
manila/message/__init__.py
Normal file
85
manila/message/api.py
Normal file
85
manila/message/api.py
Normal file
@ -0,0 +1,85 @@
|
||||
# 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
|
||||
import six
|
||||
|
||||
from manila.db import base
|
||||
from manila.message import message_field
|
||||
from manila.message import message_levels
|
||||
|
||||
|
||||
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, action, project_id, resource_type=None,
|
||||
resource_id=None, exception=None, detail=None,
|
||||
level=message_levels.ERROR):
|
||||
"""Create a message with the specified information."""
|
||||
LOG.info("Creating message record for request_id = %s" %
|
||||
context.request_id)
|
||||
|
||||
# Updates expiry time for message as per message_ttl config.
|
||||
expires_at = (timeutils.utcnow() + datetime.timedelta(
|
||||
seconds=CONF.message_ttl))
|
||||
detail_id = message_field.translate_detail_id(exception, detail)
|
||||
|
||||
message_record = {
|
||||
'project_id': project_id,
|
||||
'request_id': context.request_id,
|
||||
'resource_type': resource_type,
|
||||
'resource_id': resource_id,
|
||||
'action_id': action[0],
|
||||
'detail_id': detail_id,
|
||||
'message_level': level,
|
||||
'expires_at': expires_at,
|
||||
}
|
||||
try:
|
||||
self.db.message_create(context, message_record)
|
||||
except Exception:
|
||||
LOG.exception("Failed to create message record "
|
||||
"for request_id %s" % context.request_id)
|
||||
|
||||
def get(self, context, id):
|
||||
"""Return message with the specified message id."""
|
||||
return self.db.message_get(context, id)
|
||||
|
||||
def get_all(self, context, search_opts={}, sort_key=None, sort_dir=None):
|
||||
"""Return messages for the given context."""
|
||||
LOG.debug("Searching for messages by: %s",
|
||||
six.text_type(search_opts))
|
||||
|
||||
messages = self.db.message_get_all(
|
||||
context, filters=search_opts, sort_key=sort_key, sort_dir=sort_dir)
|
||||
|
||||
return messages
|
||||
|
||||
def delete(self, context, id):
|
||||
"""Delete message with the specified message id."""
|
||||
return self.db.message_destroy(context, id)
|
64
manila/message/message_field.py
Normal file
64
manila/message/message_field.py
Normal file
@ -0,0 +1,64 @@
|
||||
# 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 manila.i18n import _
|
||||
|
||||
|
||||
class Resource(object):
|
||||
|
||||
SHARE = 'SHARE'
|
||||
|
||||
|
||||
class Action(object):
|
||||
|
||||
ALLOCATE_HOST = ('001', _('allocate host'))
|
||||
|
||||
ALL = (ALLOCATE_HOST,)
|
||||
|
||||
|
||||
class Detail(object):
|
||||
|
||||
UNKNOWN_ERROR = ('001', _('An unknown error occurred.'))
|
||||
NO_VALID_HOST = ('002', _("No storage could be allocated for this share "
|
||||
"request. Trying again with a different size "
|
||||
"or share type may succeed."))
|
||||
|
||||
ALL = (UNKNOWN_ERROR,
|
||||
NO_VALID_HOST)
|
||||
|
||||
# Exception and detail mappings
|
||||
EXCEPTION_DETAIL_MAPPINGS = {
|
||||
NO_VALID_HOST: ['NoValidHost'],
|
||||
}
|
||||
|
||||
|
||||
def translate_action(action_id):
|
||||
action_message = next((action[1] for action in Action.ALL
|
||||
if action[0] == action_id), None)
|
||||
return action_message or 'unknown action'
|
||||
|
||||
|
||||
def translate_detail(detail_id):
|
||||
detail_message = next((action[1] for action in Detail.ALL
|
||||
if action[0] == detail_id), None)
|
||||
return detail_message or Detail.UNKNOWN_ERROR[1]
|
||||
|
||||
|
||||
def translate_detail_id(exception, detail):
|
||||
if exception is not None and isinstance(exception, Exception):
|
||||
for key, value in Detail.EXCEPTION_DETAIL_MAPPINGS.items():
|
||||
if exception.__class__.__name__ in value:
|
||||
return key[0]
|
||||
if (detail in Detail.ALL and
|
||||
detail is not Detail.EXCEPTION_DETAIL_MAPPINGS):
|
||||
return detail[0]
|
||||
return Detail.UNKNOWN_ERROR[0]
|
15
manila/message/message_levels.py
Normal file
15
manila/message/message_levels.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.
|
||||
|
||||
"""Message level constants."""
|
||||
|
||||
ERROR = 'ERROR'
|
@ -33,6 +33,7 @@ import manila.coordination
|
||||
import manila.db.api
|
||||
import manila.db.base
|
||||
import manila.exception
|
||||
import manila.message.api
|
||||
import manila.network
|
||||
import manila.network.linux.interface
|
||||
import manila.network.neutron.api
|
||||
@ -102,6 +103,7 @@ _global_opt_lists = [
|
||||
manila.db.api.db_opts,
|
||||
[manila.db.base.db_driver_opt],
|
||||
manila.exception.exc_log_opts,
|
||||
manila.message.api.messages_opts,
|
||||
manila.network.linux.interface.OPTS,
|
||||
manila.network.network_opts,
|
||||
manila.network.neutron.neutron_network_plugin.
|
||||
|
@ -30,6 +30,8 @@ from manila import context
|
||||
from manila import db
|
||||
from manila import exception
|
||||
from manila import manager
|
||||
from manila.message import api as message_api
|
||||
from manila.message import message_field
|
||||
from manila import quota
|
||||
from manila import rpc
|
||||
from manila.share import rpcapi as share_rpcapi
|
||||
@ -77,6 +79,7 @@ class SchedulerManager(manager.Manager):
|
||||
scheduler_driver = MAPPING[scheduler_driver]
|
||||
|
||||
self.driver = importutils.import_object(scheduler_driver)
|
||||
self.message_api = message_api.API()
|
||||
super(self.__class__, self).__init__(*args, **kwargs)
|
||||
|
||||
def init_host(self):
|
||||
@ -106,16 +109,15 @@ class SchedulerManager(manager.Manager):
|
||||
self.driver.schedule_create_share(context, request_spec,
|
||||
filter_properties)
|
||||
except exception.NoValidHost as ex:
|
||||
self._set_share_state_and_notify('create_share',
|
||||
{'status':
|
||||
constants.STATUS_ERROR},
|
||||
context, ex, request_spec)
|
||||
self._set_share_state_and_notify(
|
||||
'create_share', {'status': constants.STATUS_ERROR},
|
||||
context, ex, request_spec,
|
||||
message_field.Action.ALLOCATE_HOST)
|
||||
except Exception as ex:
|
||||
with excutils.save_and_reraise_exception():
|
||||
self._set_share_state_and_notify('create_share',
|
||||
{'status':
|
||||
constants.STATUS_ERROR},
|
||||
context, ex, request_spec)
|
||||
self._set_share_state_and_notify(
|
||||
'create_share', {'status': constants.STATUS_ERROR},
|
||||
context, ex, request_spec)
|
||||
|
||||
def get_pools(self, context, filters=None):
|
||||
"""Get active pools from the scheduler's cache."""
|
||||
@ -188,7 +190,7 @@ class SchedulerManager(manager.Manager):
|
||||
_migrate_share_set_error(self, context, ex, request_spec)
|
||||
|
||||
def _set_share_state_and_notify(self, method, state, context, ex,
|
||||
request_spec):
|
||||
request_spec, action=None):
|
||||
|
||||
LOG.error("Failed to schedule %(method)s: %(ex)s",
|
||||
{"method": method, "ex": ex})
|
||||
@ -200,6 +202,12 @@ class SchedulerManager(manager.Manager):
|
||||
if share_id:
|
||||
db.share_update(context, share_id, state)
|
||||
|
||||
if action:
|
||||
self.message_api.create(
|
||||
context, action, context.project_id,
|
||||
resource_type=message_field.Resource.SHARE,
|
||||
resource_id=share_id, exception=ex)
|
||||
|
||||
payload = dict(request_spec=request_spec,
|
||||
share_properties=properties,
|
||||
share_id=share_id,
|
||||
|
47
manila/tests/api/v2/stubs.py
Normal file
47
manila/tests/api/v2/stubs.py
Normal file
@ -0,0 +1,47 @@
|
||||
# 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 manila.message import message_field
|
||||
from manila.message import message_levels
|
||||
from manila.tests.api import fakes
|
||||
|
||||
|
||||
FAKE_UUID = fakes.FAKE_UUID
|
||||
|
||||
|
||||
def stub_message(id, **kwargs):
|
||||
message = {
|
||||
'id': id,
|
||||
'project_id': 'fake_project',
|
||||
'action_id': message_field.Action.ALLOCATE_HOST[0],
|
||||
'message_level': message_levels.ERROR,
|
||||
'request_id': FAKE_UUID,
|
||||
'resource_type': message_field.Resource.SHARE,
|
||||
'resource_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()),
|
||||
'detail_id': message_field.Detail.NO_VALID_HOST[0],
|
||||
}
|
||||
|
||||
message.update(kwargs)
|
||||
return message
|
||||
|
||||
|
||||
def stub_message_get(self, context, message_id):
|
||||
return stub_message(message_id)
|
186
manila/tests/api/v2/test_messages.py
Normal file
186
manila/tests/api/v2/test_messages.py
Normal file
@ -0,0 +1,186 @@
|
||||
# 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 manila.api.v2 import messages
|
||||
from manila import context
|
||||
from manila import exception
|
||||
from manila.message import api as message_api
|
||||
from manila.message import message_field
|
||||
from manila import policy
|
||||
from manila import test
|
||||
from manila.tests.api import fakes
|
||||
from manila.tests.api.v2 import stubs
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class MessageApiTest(test.TestCase):
|
||||
def setUp(self):
|
||||
super(MessageApiTest, self).setUp()
|
||||
self.controller = messages.MessagesController()
|
||||
|
||||
self.maxDiff = None
|
||||
self.ctxt = context.RequestContext('admin', 'fake', True)
|
||||
self.mock_object(policy, 'check_policy',
|
||||
mock.Mock(return_value=True))
|
||||
|
||||
def _expected_message_from_controller(self, id):
|
||||
message = stubs.stub_message(id)
|
||||
links = [
|
||||
{'href': 'http://localhost/v2/fake/messages/%s' % id,
|
||||
'rel': 'self'},
|
||||
{'href': 'http://localhost/fake/messages/%s' % id,
|
||||
'rel': 'bookmark'},
|
||||
]
|
||||
return {
|
||||
'message': {
|
||||
'id': message.get('id'),
|
||||
'project_id': message.get('project_id'),
|
||||
'user_message': "%s: %s" % (
|
||||
message_field.translate_action(message.get('action_id')),
|
||||
message_field.translate_detail(message.get('detail_id'))),
|
||||
'request_id': message.get('request_id'),
|
||||
'action_id': message.get('action_id'),
|
||||
'detail_id': message.get('detail_id'),
|
||||
'created_at': message.get('created_at'),
|
||||
'message_level': message.get('message_level'),
|
||||
'expires_at': message.get('expires_at'),
|
||||
'links': links,
|
||||
'resource_type': message.get('resource_type'),
|
||||
'resource_id': message.get('resource_id'),
|
||||
}
|
||||
}
|
||||
|
||||
def test_show(self):
|
||||
self.mock_object(message_api.API, 'get', stubs.stub_message_get)
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/messages/%s' % fakes.FAKE_UUID,
|
||||
version=messages.MESSAGES_BASE_MICRO_VERSION,
|
||||
base_url='http://localhost/v2')
|
||||
req.environ['manila.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_with_resource(self):
|
||||
resource_type = "FAKE_RESOURCE"
|
||||
resource_id = "b1872cb2-4c5f-4072-9828-8a51b02926a3"
|
||||
fake_message = stubs.stub_message(fakes.FAKE_UUID,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id)
|
||||
mock_get = mock.Mock(return_value=fake_message)
|
||||
self.mock_object(message_api.API, 'get', mock_get)
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/messages/%s' % fakes.FAKE_UUID,
|
||||
version=messages.MESSAGES_BASE_MICRO_VERSION,
|
||||
base_url='http://localhost/v2')
|
||||
req.environ['manila.context'] = self.ctxt
|
||||
|
||||
res_dict = self.controller.show(req, fakes.FAKE_UUID)
|
||||
|
||||
self.assertEqual(resource_type,
|
||||
res_dict['message']['resource_type'])
|
||||
self.assertEqual(resource_id,
|
||||
res_dict['message']['resource_id'])
|
||||
|
||||
def test_show_not_found(self):
|
||||
fake_not_found = exception.MessageNotFound(message_id=fakes.FAKE_UUID)
|
||||
self.mock_object(message_api.API, 'get',
|
||||
mock.Mock(side_effect=fake_not_found))
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/messages/%s' % fakes.FAKE_UUID,
|
||||
version=messages.MESSAGES_BASE_MICRO_VERSION,
|
||||
base_url='http://localhost/v2')
|
||||
req.environ['manila.context'] = self.ctxt
|
||||
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
|
||||
req, fakes.FAKE_UUID)
|
||||
|
||||
def test_show_pre_microversion(self):
|
||||
self.mock_object(message_api.API, 'get', stubs.stub_message_get)
|
||||
|
||||
req = fakes.HTTPRequest.blank('/messages/%s' % fakes.FAKE_UUID,
|
||||
version='2.35',
|
||||
base_url='http://localhost/v2')
|
||||
req.environ['manila.context'] = self.ctxt
|
||||
|
||||
self.assertRaises(exception.VersionNotFoundForAPIMethod,
|
||||
self.controller.show, req, fakes.FAKE_UUID)
|
||||
|
||||
def test_delete(self):
|
||||
self.mock_object(message_api.API, 'get', stubs.stub_message_get)
|
||||
self.mock_object(message_api.API, 'delete')
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/messages/%s' % fakes.FAKE_UUID,
|
||||
version=messages.MESSAGES_BASE_MICRO_VERSION)
|
||||
req.environ['manila.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):
|
||||
fake_not_found = exception.MessageNotFound(message_id=fakes.FAKE_UUID)
|
||||
self.mock_object(message_api.API, 'get',
|
||||
mock.Mock(side_effect=fake_not_found))
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/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):
|
||||
msg1 = stubs.stub_message(fakes.get_fake_uuid())
|
||||
msg2 = stubs.stub_message(fakes.get_fake_uuid())
|
||||
self.mock_object(message_api.API, 'get_all', mock.Mock(
|
||||
return_value=[msg1, msg2]))
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/messages',
|
||||
version=messages.MESSAGES_BASE_MICRO_VERSION,
|
||||
base_url='http://localhost/v2')
|
||||
req.environ['manila.context'] = self.ctxt
|
||||
|
||||
res_dict = self.controller.index(req)
|
||||
|
||||
ex1 = self._expected_message_from_controller(msg1['id'])['message']
|
||||
ex2 = self._expected_message_from_controller(msg2['id'])['message']
|
||||
expected = {'messages': [ex1, ex2]}
|
||||
self.assertDictMatch(expected, res_dict)
|
||||
|
||||
def test_index_with_limit_and_offset(self):
|
||||
msg1 = stubs.stub_message(fakes.get_fake_uuid())
|
||||
msg2 = stubs.stub_message(fakes.get_fake_uuid())
|
||||
self.mock_object(message_api.API, 'get_all', mock.Mock(
|
||||
return_value=[msg1, msg2]))
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/messages?limit=1&offset=1',
|
||||
version=messages.MESSAGES_BASE_MICRO_VERSION,
|
||||
base_url='http://localhost/v2')
|
||||
req.environ['manila.context'] = self.ctxt
|
||||
|
||||
res_dict = self.controller.index(req)
|
||||
|
||||
ex2 = self._expected_message_from_controller(msg2['id'])['message']
|
||||
self.assertEqual([ex2], res_dict['messages'])
|
@ -2364,3 +2364,35 @@ class SquashSGSnapshotMembersAndSSIModelsChecks(BaseMigrationChecks):
|
||||
db_result = engine.execute(ssi_table.select().where(
|
||||
ssi_table.c.id == self.share_group_snapshot_member_id))
|
||||
self.test_case.assertEqual(0, db_result.rowcount)
|
||||
|
||||
|
||||
@map_to_migration('238720805ce1')
|
||||
class MessagesTableChecks(BaseMigrationChecks):
|
||||
new_table_name = 'messages'
|
||||
|
||||
def setup_upgrade_data(self, engine):
|
||||
pass
|
||||
|
||||
def check_upgrade(self, engine, data):
|
||||
message_data = {
|
||||
'id': uuidutils.generate_uuid(),
|
||||
'project_id': 'x' * 255,
|
||||
'request_id': 'x' * 255,
|
||||
'resource_type': 'x' * 255,
|
||||
'resource_id': 'y' * 36,
|
||||
'action_id': 'y' * 10,
|
||||
'detail_id': 'y' * 10,
|
||||
'message_level': 'x' * 255,
|
||||
'created_at': datetime.datetime(2017, 7, 10, 18, 5, 58),
|
||||
'updated_at': None,
|
||||
'deleted_at': None,
|
||||
'deleted': 0,
|
||||
'expires_at': datetime.datetime(2017, 7, 11, 18, 5, 58),
|
||||
}
|
||||
|
||||
new_table = utils.load_table(self.new_table_name, engine)
|
||||
engine.execute(new_table.insert(message_data))
|
||||
|
||||
def check_downgrade(self, engine):
|
||||
self.test_case.assertRaises(sa_exc.NoSuchTableError, utils.load_table,
|
||||
'messages', engine)
|
||||
|
@ -2738,3 +2738,89 @@ class ShareTypeAPITestCase(test.TestCase):
|
||||
result = db_api.share_type_get_by_name_or_id(self.ctxt, fake_id)
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class MessagesDatabaseAPITestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(MessagesDatabaseAPITestCase, self).setUp()
|
||||
self.user_id = uuidutils.generate_uuid()
|
||||
self.project_id = uuidutils.generate_uuid()
|
||||
self.ctxt = context.RequestContext(
|
||||
user_id=self.user_id, project_id=self.project_id, is_admin=False)
|
||||
|
||||
def test_message_create(self):
|
||||
result = db_utils.create_message(project_id=self.project_id,
|
||||
action_id='001')
|
||||
|
||||
self.assertIsNotNone(result['id'])
|
||||
|
||||
def test_message_delete(self):
|
||||
result = db_utils.create_message(project_id=self.project_id,
|
||||
action_id='001')
|
||||
|
||||
db_api.message_destroy(self.ctxt, result)
|
||||
|
||||
self.assertRaises(exception.NotFound, db_api.message_get,
|
||||
self.ctxt, result['id'])
|
||||
|
||||
def test_message_get(self):
|
||||
message = db_utils.create_message(project_id=self.project_id,
|
||||
action_id='001')
|
||||
|
||||
result = db_api.message_get(self.ctxt, message['id'])
|
||||
|
||||
self.assertEqual(message['id'], result['id'])
|
||||
self.assertEqual(message['action_id'], result['action_id'])
|
||||
self.assertEqual(message['detail_id'], result['detail_id'])
|
||||
self.assertEqual(message['project_id'], result['project_id'])
|
||||
self.assertEqual(message['message_level'], result['message_level'])
|
||||
|
||||
def test_message_get_not_found(self):
|
||||
self.assertRaises(exception.MessageNotFound, db_api.message_get,
|
||||
self.ctxt, 'fake_id')
|
||||
|
||||
def test_message_get_different_project(self):
|
||||
message = db_utils.create_message(project_id='another-project',
|
||||
action_id='001')
|
||||
|
||||
self.assertRaises(exception.MessageNotFound, db_api.message_get,
|
||||
self.ctxt, message['id'])
|
||||
|
||||
def test_message_get_all(self):
|
||||
db_utils.create_message(project_id=self.project_id, action_id='001')
|
||||
db_utils.create_message(project_id=self.project_id, action_id='001')
|
||||
db_utils.create_message(project_id='another-project', action_id='001')
|
||||
|
||||
result = db_api.message_get_all(self.ctxt)
|
||||
|
||||
self.assertEqual(2, len(result))
|
||||
|
||||
def test_message_get_all_as_admin(self):
|
||||
db_utils.create_message(project_id=self.project_id, action_id='001')
|
||||
db_utils.create_message(project_id=self.project_id, action_id='001')
|
||||
db_utils.create_message(project_id='another-project', action_id='001')
|
||||
|
||||
result = db_api.message_get_all(self.ctxt.elevated())
|
||||
|
||||
self.assertEqual(3, len(result))
|
||||
|
||||
def test_message_get_all_with_filter(self):
|
||||
for i in ['001', '002', '002']:
|
||||
db_utils.create_message(project_id=self.project_id, action_id=i)
|
||||
|
||||
result = db_api.message_get_all(self.ctxt,
|
||||
filters={'action_id': '002'})
|
||||
|
||||
self.assertEqual(2, len(result))
|
||||
|
||||
def test_message_get_all_sorted(self):
|
||||
ids = []
|
||||
for i in ['001', '002', '003']:
|
||||
msg = db_utils.create_message(project_id=self.project_id,
|
||||
action_id=i)
|
||||
ids.append(msg.id)
|
||||
|
||||
result = db_api.message_get_all(self.ctxt, sort_key='action_id')
|
||||
result_ids = [r.id for r in result]
|
||||
self.assertEqual(result_ids, ids)
|
||||
|
@ -18,6 +18,7 @@ import copy
|
||||
from manila.common import constants
|
||||
from manila import context
|
||||
from manila import db
|
||||
from manila.message import message_levels
|
||||
|
||||
|
||||
def _create_db_row(method, default_values, custom_values):
|
||||
@ -264,3 +265,12 @@ def create_security_service(**kwargs):
|
||||
share_network_id,
|
||||
service_ref['id'])
|
||||
return service_ref
|
||||
|
||||
|
||||
def create_message(**kwargs):
|
||||
message_dict = {
|
||||
'action': 'fake_Action',
|
||||
'project_id': 'fake-project-id',
|
||||
'message_level': message_levels.ERROR,
|
||||
}
|
||||
return _create_db_row(db.message_create, message_dict, kwargs)
|
||||
|
0
manila/tests/message/__init__.py
Normal file
0
manila/tests/message/__init__.py
Normal file
92
manila/tests/message/test_api.py
Normal file
92
manila/tests/message/test_api.py
Normal file
@ -0,0 +1,92 @@
|
||||
# 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 manila import context
|
||||
from manila.message import api as message_api
|
||||
from manila.message.message_field import Action as MsgAction
|
||||
from manila.message.message_field import Detail as MsgDetail
|
||||
from manila.message import message_levels
|
||||
from manila 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_id': None,
|
||||
'action_id': MsgAction.ALLOCATE_HOST[0],
|
||||
'detail_id': MsgDetail.NO_VALID_HOST[0],
|
||||
'message_level': message_levels.ERROR,
|
||||
'expires_at': expected_expires_at,
|
||||
}
|
||||
|
||||
self.message_api.create(self.ctxt,
|
||||
MsgAction.ALLOCATE_HOST,
|
||||
"fakeproject",
|
||||
detail=MsgDetail.NO_VALID_HOST,
|
||||
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, 'message_create',
|
||||
mock.Mock(side_effect=Exception()))
|
||||
exception_log = self.mock_object(message_api.LOG, 'exception')
|
||||
self.message_api.create(self.ctxt,
|
||||
MsgAction.ALLOCATE_HOST,
|
||||
'fakeproject',
|
||||
'fake_resource')
|
||||
|
||||
self.message_api.db.message_create.assert_called_once_with(
|
||||
self.ctxt, mock.ANY)
|
||||
exception_log.assert_called_once_with(
|
||||
'Failed to create message record for request_id fakerequestid')
|
||||
|
||||
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, filters={}, sort_dir=None, sort_key=None)
|
||||
|
||||
def test_delete(self):
|
||||
self.message_api.delete(self.ctxt, 'fake_id')
|
||||
|
||||
self.message_api.db.message_destroy.assert_called_once_with(
|
||||
self.ctxt, 'fake_id')
|
62
manila/tests/message/test_message_field.py
Normal file
62
manila/tests/message/test_message_field.py
Normal file
@ -0,0 +1,62 @@
|
||||
# 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 ddt
|
||||
from oslo_config import cfg
|
||||
|
||||
from manila import exception
|
||||
from manila.message import message_field
|
||||
from manila import test
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class MessageFieldTest(test.TestCase):
|
||||
|
||||
@ddt.data(message_field.Action, message_field.Detail)
|
||||
def test_unique_ids(self, cls):
|
||||
"""Assert that no action or detail id is duplicated."""
|
||||
ids = [name[0] for name in cls.ALL]
|
||||
self.assertEqual(len(ids), len(set(ids)))
|
||||
|
||||
@ddt.data({'id': '001', 'content': 'allocate host'},
|
||||
{'id': 'invalid', 'content': None})
|
||||
@ddt.unpack
|
||||
def test_translate_action(self, id, content):
|
||||
result = message_field.translate_action(id)
|
||||
if content is None:
|
||||
content = 'unknown action'
|
||||
self.assertEqual(content, result)
|
||||
|
||||
@ddt.data({'id': '001',
|
||||
'content': 'An unknown error occurred.'},
|
||||
{'id': '002',
|
||||
'content': 'No storage could be allocated for this share '
|
||||
'request. Trying again with a different size or '
|
||||
'share type may succeed.'},
|
||||
{'id': 'invalid', 'content': None})
|
||||
@ddt.unpack
|
||||
def test_translate_detail(self, id, content):
|
||||
result = message_field.translate_detail(id)
|
||||
if content is None:
|
||||
content = 'An unknown error occurred.'
|
||||
self.assertEqual(content, result)
|
||||
|
||||
@ddt.data({'exception': exception.NoValidHost(reason='fake reason'),
|
||||
'detail': '',
|
||||
'expected': '002'},
|
||||
{'exception': '', 'detail': message_field.Detail.NO_VALID_HOST,
|
||||
'expected': '002'})
|
||||
@ddt.unpack
|
||||
def test_translate_detail_id(self, exception, detail, expected):
|
||||
result = message_field.translate_detail_id(exception, detail)
|
||||
self.assertEqual(expected, result)
|
@ -130,5 +130,9 @@
|
||||
"share_group_types_spec:update": "rule:admin_api",
|
||||
"share_group_types_spec:show": "rule:admin_api",
|
||||
"share_group_types_spec:index": "rule:admin_api",
|
||||
"share_group_types_spec:delete": "rule:admin_api"
|
||||
"share_group_types_spec:delete": "rule:admin_api",
|
||||
|
||||
"message:delete": "rule:default",
|
||||
"message:get": "rule:default",
|
||||
"message:get_all": "rule:default"
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ from manila.common import constants
|
||||
from manila import context
|
||||
from manila import db
|
||||
from manila import exception
|
||||
from manila.message import message_field
|
||||
from manila import quota
|
||||
from manila.scheduler.drivers import base
|
||||
from manila.scheduler.drivers import filter
|
||||
@ -136,7 +137,9 @@ class SchedulerManagerTestCase(test.TestCase):
|
||||
assert_called_once_with(service_name, host, capabilities))
|
||||
|
||||
@mock.patch.object(db, 'share_update', mock.Mock())
|
||||
def test_create_share_exception_puts_share_in_error_state(self):
|
||||
@mock.patch('manila.message.api.API.create')
|
||||
def test_create_share_exception_puts_share_in_error_state(
|
||||
self, _mock_message_create):
|
||||
"""Test NoValidHost exception for create_share.
|
||||
|
||||
Puts the share in 'error' state and eats the exception.
|
||||
@ -144,9 +147,10 @@ class SchedulerManagerTestCase(test.TestCase):
|
||||
fake_share_id = 1
|
||||
|
||||
request_spec = {'share_id': fake_share_id}
|
||||
ex = exception.NoValidHost(reason='')
|
||||
with mock.patch.object(
|
||||
self.manager.driver, 'schedule_create_share',
|
||||
mock.Mock(side_effect=self.raise_no_valid_host)):
|
||||
mock.Mock(side_effect=ex)):
|
||||
self.mock_object(manager.LOG, 'error')
|
||||
|
||||
self.manager.create_share_instance(
|
||||
@ -158,6 +162,12 @@ class SchedulerManagerTestCase(test.TestCase):
|
||||
assert_called_once_with(self.context, request_spec, {}))
|
||||
manager.LOG.error.assert_called_once_with(mock.ANY, mock.ANY)
|
||||
|
||||
_mock_message_create.assert_called_once_with(
|
||||
self.context,
|
||||
message_field.Action.ALLOCATE_HOST,
|
||||
self.context.project_id, resource_type='SHARE',
|
||||
exception=ex, resource_id=fake_share_id)
|
||||
|
||||
@mock.patch.object(db, 'share_update', mock.Mock())
|
||||
def test_create_share_other_exception_puts_share_in_error_state(self):
|
||||
"""Test any exception except NoValidHost for create_share.
|
||||
|
@ -30,7 +30,7 @@ ShareGroup = [
|
||||
help="The minimum api microversion is configured to be the "
|
||||
"value of the minimum microversion supported by Manila."),
|
||||
cfg.StrOpt("max_api_microversion",
|
||||
default="2.36",
|
||||
default="2.37",
|
||||
help="The maximum api microversion is configured to be the "
|
||||
"value of the latest microversion supported by Manila."),
|
||||
cfg.StrOpt("region",
|
||||
|
@ -186,6 +186,9 @@ class SharesV2Client(shares_client.SharesClient):
|
||||
elif "replica_id" in kwargs:
|
||||
return self._is_resource_deleted(
|
||||
self.get_share_replica, kwargs.get("replica_id"))
|
||||
elif "message_id" in kwargs:
|
||||
return self._is_resource_deleted(
|
||||
self.get_message, kwargs.get("message_id"))
|
||||
else:
|
||||
return super(SharesV2Client, self).is_resource_deleted(
|
||||
*args, **kwargs)
|
||||
@ -1673,3 +1676,44 @@ class SharesV2Client(shares_client.SharesClient):
|
||||
"snapshots/%s/export-locations" % snapshot_id, version=version)
|
||||
self.expected_success(200, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
###############
|
||||
|
||||
def get_message(self, message_id, version=LATEST_MICROVERSION):
|
||||
"""Show details for a single message."""
|
||||
url = 'messages/%s' % message_id
|
||||
resp, body = self.get(url, version=version)
|
||||
self.expected_success(200, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
def list_messages(self, params=None, version=LATEST_MICROVERSION):
|
||||
"""List all messages."""
|
||||
url = 'messages'
|
||||
url += '?%s' % urlparse.urlencode(params) if params else ''
|
||||
resp, body = self.get(url, version=version)
|
||||
self.expected_success(200, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
def delete_message(self, message_id, version=LATEST_MICROVERSION):
|
||||
"""Delete a single message."""
|
||||
url = 'messages/%s' % message_id
|
||||
resp, body = self.delete(url, version=version)
|
||||
self.expected_success(204, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
def wait_for_message(self, resource_id):
|
||||
"""Waits until a message for a resource with given id exists"""
|
||||
start = int(time.time())
|
||||
message = None
|
||||
|
||||
while not message:
|
||||
time.sleep(self.build_interval)
|
||||
for msg in self.list_messages():
|
||||
if msg['resource_id'] == resource_id:
|
||||
return msg
|
||||
|
||||
if int(time.time()) - start >= self.build_timeout:
|
||||
message = ('No message for resource with id %s was created in'
|
||||
' the required time (%s s).' %
|
||||
(resource_id, self.build_timeout))
|
||||
raise exceptions.TimeoutException(message)
|
||||
|
103
manila_tempest_tests/tests/api/admin/test_user_messages.py
Normal file
103
manila_tempest_tests/tests/api/admin/test_user_messages.py
Normal file
@ -0,0 +1,103 @@
|
||||
# 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 oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
from tempest import config
|
||||
from tempest import test
|
||||
|
||||
from manila_tempest_tests.tests.api import base
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
MICROVERSION = '2.37'
|
||||
MESSAGE_KEYS = (
|
||||
'created_at',
|
||||
'action_id',
|
||||
'detail_id',
|
||||
'expires_at',
|
||||
'id',
|
||||
'message_level',
|
||||
'request_id',
|
||||
'resource_type',
|
||||
'resource_id',
|
||||
'user_message',
|
||||
'project_id',
|
||||
'links',
|
||||
)
|
||||
|
||||
|
||||
@base.skip_if_microversion_lt(MICROVERSION)
|
||||
class UserMessageTest(base.BaseSharesAdminTest):
|
||||
|
||||
def setUp(self):
|
||||
super(UserMessageTest, self).setUp()
|
||||
self.message = self.create_user_message()
|
||||
|
||||
@test.attr(type=[base.TAG_POSITIVE, base.TAG_API])
|
||||
def test_list_messages(self):
|
||||
body = self.shares_v2_client.list_messages()
|
||||
self.assertIsInstance(body, list)
|
||||
self.assertTrue(self.message['id'], [x['id'] for x in body])
|
||||
message = body[0]
|
||||
self.assertEqual(set(MESSAGE_KEYS), set(message.keys()))
|
||||
|
||||
@test.attr(type=[base.TAG_POSITIVE, base.TAG_API])
|
||||
def test_list_messages_sorted_and_paginated(self):
|
||||
self.create_user_message()
|
||||
self.create_user_message()
|
||||
params = {'sort_key': 'resource_id', 'sort_dir': 'asc', 'limit': 2}
|
||||
body = self.shares_v2_client.list_messages(params=params)
|
||||
# tempest/lib/common/rest_client.py's _parse_resp checks
|
||||
# for number of keys in response's dict, if there is only single
|
||||
# key, it returns directly this key, otherwise it returns
|
||||
# parsed body. If limit param is used, then API returns
|
||||
# multiple keys in reponse ('messages' and 'message_links')
|
||||
messages = body['messages']
|
||||
self.assertIsInstance(messages, list)
|
||||
ids = [x['resource_id'] for x in messages]
|
||||
self.assertEqual(2, len(ids))
|
||||
self.assertEqual(ids, sorted(ids))
|
||||
|
||||
@test.attr(type=[base.TAG_POSITIVE, base.TAG_API])
|
||||
def test_list_messages_filtered(self):
|
||||
self.create_user_message()
|
||||
params = {'resource_id': self.message['resource_id']}
|
||||
body = self.shares_v2_client.list_messages(params=params)
|
||||
self.assertIsInstance(body, list)
|
||||
ids = [x['id'] for x in body]
|
||||
self.assertEqual([self.message['id']], ids)
|
||||
|
||||
@test.attr(type=[base.TAG_POSITIVE, base.TAG_API])
|
||||
def test_show_message(self):
|
||||
self.addCleanup(self.shares_v2_client.delete_message,
|
||||
self.message['id'])
|
||||
|
||||
message = self.shares_v2_client.get_message(self.message['id'])
|
||||
|
||||
self.assertEqual(set(MESSAGE_KEYS), set(message.keys()))
|
||||
self.assertTrue(uuidutils.is_uuid_like(message['id']))
|
||||
self.assertEqual('001', message['action_id'])
|
||||
self.assertEqual('002', message['detail_id'])
|
||||
self.assertEqual('SHARE', message['resource_type'])
|
||||
self.assertTrue(uuidutils.is_uuid_like(message['resource_id']))
|
||||
self.assertEqual('ERROR', message['message_level'])
|
||||
created_at = timeutils.parse_strtime(message['created_at'])
|
||||
expires_at = timeutils.parse_strtime(message['expires_at'])
|
||||
self.assertGreater(expires_at, created_at)
|
||||
self.assertEqual(set(MESSAGE_KEYS), set(message.keys()))
|
||||
|
||||
@test.attr(type=[base.TAG_POSITIVE, base.TAG_API])
|
||||
def test_delete_message(self):
|
||||
self.shares_v2_client.delete_message(self.message['id'])
|
||||
self.shares_v2_client.wait_for_resource_deletion(
|
||||
message_id=self.message['id'])
|
@ -0,0 +1,58 @@
|
||||
# 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 oslo_utils import uuidutils
|
||||
import six
|
||||
from tempest import config
|
||||
from tempest.lib import exceptions as lib_exc
|
||||
from tempest import test
|
||||
|
||||
from manila_tempest_tests.tests.api import base
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
MICROVERSION = '2.37'
|
||||
|
||||
|
||||
@base.skip_if_microversion_lt(MICROVERSION)
|
||||
class UserMessageNegativeTest(base.BaseSharesAdminTest):
|
||||
|
||||
def setUp(self):
|
||||
super(UserMessageNegativeTest, self).setUp()
|
||||
self.message = self.create_user_message()
|
||||
|
||||
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API])
|
||||
def test_show_message_of_other_tenants(self):
|
||||
isolated_client = self.get_client_with_isolated_creds(
|
||||
type_of_creds='alt', client_version='2')
|
||||
self.assertRaises(lib_exc.NotFound,
|
||||
isolated_client.get_message,
|
||||
self.message['id'])
|
||||
|
||||
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API])
|
||||
def test_show_nonexistent_message(self):
|
||||
self.assertRaises(lib_exc.NotFound,
|
||||
self.shares_v2_client.get_message,
|
||||
six.text_type(uuidutils.generate_uuid()))
|
||||
|
||||
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API])
|
||||
def test_delete_message_of_other_tenants(self):
|
||||
isolated_client = self.get_client_with_isolated_creds(
|
||||
type_of_creds='alt', client_version='2')
|
||||
self.assertRaises(lib_exc.NotFound,
|
||||
isolated_client.delete_message,
|
||||
self.message['id'])
|
||||
|
||||
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API])
|
||||
def test_delete_nonexistent_message(self):
|
||||
self.assertRaises(lib_exc.NotFound,
|
||||
self.shares_v2_client.delete_message,
|
||||
six.text_type(uuidutils.generate_uuid()))
|
@ -998,6 +998,25 @@ class BaseSharesTest(test.BaseTestCase):
|
||||
"d2value": d2value
|
||||
})
|
||||
|
||||
def create_user_message(self):
|
||||
"""Trigger a 'no valid host' situation to generate a message."""
|
||||
extra_specs = {
|
||||
'vendor_name': 'foobar',
|
||||
'driver_handles_share_servers': CONF.share.multitenancy_enabled,
|
||||
}
|
||||
share_type_name = data_utils.rand_name("share-type")
|
||||
|
||||
bogus_type = self.create_share_type(
|
||||
name=share_type_name,
|
||||
extra_specs=extra_specs)['share_type']
|
||||
|
||||
params = {'share_type_id': bogus_type['id'],
|
||||
'share_network_id': self.shares_v2_client.share_network_id}
|
||||
share = self.shares_v2_client.create_share(**params)
|
||||
self.addCleanup(self.shares_v2_client.delete_share, share['id'])
|
||||
self.shares_v2_client.wait_for_share_status(share['id'], "error")
|
||||
return self.shares_v2_client.wait_for_message(share['id'])
|
||||
|
||||
|
||||
class BaseSharesAltTest(BaseSharesTest):
|
||||
"""Base test case class for all Shares Alt API tests."""
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- Added new user messages API - GET /messages, GET /messages/<message_id>
|
||||
and DELETE /messages/<message_id>.
|
||||
- Added sorting, filtering and pagination to the user messages listing.
|
||||
- Added 'message_ttl' configuration option which can be used for
|
||||
configuring message expiration time.
|
Loading…
Reference in New Issue
Block a user