Merge "User Messages"
This commit is contained in:
commit
e2acd43198
@ -25,6 +25,7 @@ Shared File Systems API
|
|||||||
.. include:: availability-zones.inc
|
.. include:: availability-zones.inc
|
||||||
.. include:: os-share-manage.inc
|
.. include:: os-share-manage.inc
|
||||||
.. include:: quota-sets.inc
|
.. include:: quota-sets.inc
|
||||||
|
.. include:: user-messages.inc
|
||||||
|
|
||||||
======================================
|
======================================
|
||||||
Shared File Systems API (EXPERIMENTAL)
|
Shared File Systems API (EXPERIMENTAL)
|
||||||
|
@ -50,6 +50,12 @@ export_location_id_path:
|
|||||||
in: path
|
in: path
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
message_id:
|
||||||
|
description: |
|
||||||
|
The UUID of the message.
|
||||||
|
in: path
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
security_service_id_path:
|
security_service_id_path:
|
||||||
description: |
|
description: |
|
||||||
The UUID of the security service.
|
The UUID of the security service.
|
||||||
@ -121,6 +127,12 @@ tenant_id_path:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
# variables in query
|
# 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:
|
all_tenants:
|
||||||
description: |
|
description: |
|
||||||
(Admin only). Defines whether to list shares for
|
(Admin only). Defines whether to list shares for
|
||||||
@ -206,6 +218,12 @@ consistency_group_id_5:
|
|||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
detail_id:
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
The ID of the message detail.
|
||||||
export_location_id_query:
|
export_location_id_query:
|
||||||
description: |
|
description: |
|
||||||
The export location UUID that can be used to filter shares or
|
The export location UUID that can be used to filter shares or
|
||||||
@ -275,6 +293,12 @@ media_types:
|
|||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
type: object
|
type: object
|
||||||
|
message_level:
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
The message level.
|
||||||
metadata_1:
|
metadata_1:
|
||||||
description: |
|
description: |
|
||||||
One or more metadata key-value pairs, as a
|
One or more metadata key-value pairs, as a
|
||||||
@ -316,6 +340,30 @@ project_id_6:
|
|||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
type: string
|
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:
|
service_binary_query:
|
||||||
description: |
|
description: |
|
||||||
The service binary name. Default is the base name
|
The service binary name. Default is the base name
|
||||||
@ -399,6 +447,15 @@ sort_key:
|
|||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
type: string
|
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:
|
state_2:
|
||||||
description: |
|
description: |
|
||||||
The current state of the service. A valid value
|
The current state of the service. A valid value
|
||||||
@ -577,6 +634,12 @@ access_type:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
action_id_body:
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
The ID of the action during which the message was created.
|
||||||
alias:
|
alias:
|
||||||
description: |
|
description: |
|
||||||
The alias for the extension. For example,
|
The alias for the extension. For example,
|
||||||
@ -1205,6 +1268,12 @@ description_9:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
detail_id_body:
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
The ID of the message detail.
|
||||||
disabled:
|
disabled:
|
||||||
description: |
|
description: |
|
||||||
Indicates whether the service is disabled.
|
Indicates whether the service is disabled.
|
||||||
@ -1782,6 +1851,24 @@ maxTotalSnapshotGigabytes:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: integer
|
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:
|
metadata:
|
||||||
description: |
|
description: |
|
||||||
One or more metadata key and value pairs as a
|
One or more metadata key and value pairs as a
|
||||||
@ -2114,6 +2201,12 @@ project_id_9:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
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:
|
protocol:
|
||||||
description: |
|
description: |
|
||||||
The Shared File Systems protocol of the share to
|
The Shared File Systems protocol of the share to
|
||||||
@ -2292,6 +2385,12 @@ replication_type:
|
|||||||
in: body
|
in: body
|
||||||
required: false
|
required: false
|
||||||
type: string
|
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:
|
required_extra_specs:
|
||||||
description: |
|
description: |
|
||||||
The required extra specifications for the share
|
The required extra specifications for the share
|
||||||
@ -2312,6 +2411,18 @@ reset_status:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: object
|
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:
|
security_service_created_at:
|
||||||
description: |
|
description: |
|
||||||
The date and time stamp when the security service was created.
|
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
|
adding_release_notes
|
||||||
commit_message_tags
|
commit_message_tags
|
||||||
guru_meditation_report
|
guru_meditation_report
|
||||||
|
user_messages
|
||||||
|
|
||||||
|
|
||||||
Background Concepts for manila
|
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:update": "rule:admin_api",
|
||||||
"share_group_types_spec:show": "rule:admin_api",
|
"share_group_types_spec:show": "rule:admin_api",
|
||||||
"share_group_types_spec:index": "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.
|
and export_location_path.
|
||||||
* 2.36 - Added like filter support in ``shares``, ``snapshots``,
|
* 2.36 - Added like filter support in ``shares``, ``snapshots``,
|
||||||
``share-networks``, ``share-groups`` list APIs.
|
``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
|
# The default api version request is defined to be the
|
||||||
# minimum version of the API supported.
|
# minimum version of the API supported.
|
||||||
_MIN_API_VERSION = "2.0"
|
_MIN_API_VERSION = "2.0"
|
||||||
_MAX_API_VERSION = "2.36"
|
_MAX_API_VERSION = "2.37"
|
||||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@ -210,3 +210,7 @@ user documentation.
|
|||||||
----
|
----
|
||||||
Added like filter support in ``shares``, ``snapshots``, ``share-networks``,
|
Added like filter support in ``shares``, ``snapshots``, ``share-networks``,
|
||||||
``share-groups`` list APIs.
|
``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_types_extra_specs
|
||||||
from manila.api.v1 import share_unmanage
|
from manila.api.v1 import share_unmanage
|
||||||
from manila.api.v2 import availability_zones
|
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_class_sets
|
||||||
from manila.api.v2 import quota_sets
|
from manila.api.v2 import quota_sets
|
||||||
from manila.api.v2 import services
|
from manila.api.v2 import services
|
||||||
@ -410,3 +411,7 @@ class APIRouter(manila.api.openstack.APIRouter):
|
|||||||
controller=self.resources['share-replicas'],
|
controller=self.resources['share-replicas'],
|
||||||
collection={'detail': 'GET'},
|
collection={'detail': 'GET'},
|
||||||
member={'action': 'POST'})
|
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(
|
return IMPL.share_group_type_specs_update_or_create(
|
||||||
context, type_id, group_specs)
|
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)
|
spec_ref.save(session=session)
|
||||||
|
|
||||||
return specs
|
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():
|
def register_models():
|
||||||
"""Register Models and create metadata.
|
"""Register Models and create metadata.
|
||||||
|
|
||||||
|
@ -206,6 +206,10 @@ class NotFound(ManilaException):
|
|||||||
safe = True
|
safe = True
|
||||||
|
|
||||||
|
|
||||||
|
class MessageNotFound(NotFound):
|
||||||
|
message = _("Message %(message_id)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
class Found(ManilaException):
|
class Found(ManilaException):
|
||||||
message = _("Resource was found.")
|
message = _("Resource was found.")
|
||||||
code = 302
|
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.api
|
||||||
import manila.db.base
|
import manila.db.base
|
||||||
import manila.exception
|
import manila.exception
|
||||||
|
import manila.message.api
|
||||||
import manila.network
|
import manila.network
|
||||||
import manila.network.linux.interface
|
import manila.network.linux.interface
|
||||||
import manila.network.neutron.api
|
import manila.network.neutron.api
|
||||||
@ -102,6 +103,7 @@ _global_opt_lists = [
|
|||||||
manila.db.api.db_opts,
|
manila.db.api.db_opts,
|
||||||
[manila.db.base.db_driver_opt],
|
[manila.db.base.db_driver_opt],
|
||||||
manila.exception.exc_log_opts,
|
manila.exception.exc_log_opts,
|
||||||
|
manila.message.api.messages_opts,
|
||||||
manila.network.linux.interface.OPTS,
|
manila.network.linux.interface.OPTS,
|
||||||
manila.network.network_opts,
|
manila.network.network_opts,
|
||||||
manila.network.neutron.neutron_network_plugin.
|
manila.network.neutron.neutron_network_plugin.
|
||||||
|
@ -30,6 +30,8 @@ from manila import context
|
|||||||
from manila import db
|
from manila import db
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila import manager
|
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 quota
|
||||||
from manila import rpc
|
from manila import rpc
|
||||||
from manila.share import rpcapi as share_rpcapi
|
from manila.share import rpcapi as share_rpcapi
|
||||||
@ -77,6 +79,7 @@ class SchedulerManager(manager.Manager):
|
|||||||
scheduler_driver = MAPPING[scheduler_driver]
|
scheduler_driver = MAPPING[scheduler_driver]
|
||||||
|
|
||||||
self.driver = importutils.import_object(scheduler_driver)
|
self.driver = importutils.import_object(scheduler_driver)
|
||||||
|
self.message_api = message_api.API()
|
||||||
super(self.__class__, self).__init__(*args, **kwargs)
|
super(self.__class__, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def init_host(self):
|
def init_host(self):
|
||||||
@ -106,15 +109,14 @@ class SchedulerManager(manager.Manager):
|
|||||||
self.driver.schedule_create_share(context, request_spec,
|
self.driver.schedule_create_share(context, request_spec,
|
||||||
filter_properties)
|
filter_properties)
|
||||||
except exception.NoValidHost as ex:
|
except exception.NoValidHost as ex:
|
||||||
self._set_share_state_and_notify('create_share',
|
self._set_share_state_and_notify(
|
||||||
{'status':
|
'create_share', {'status': constants.STATUS_ERROR},
|
||||||
constants.STATUS_ERROR},
|
context, ex, request_spec,
|
||||||
context, ex, request_spec)
|
message_field.Action.ALLOCATE_HOST)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
self._set_share_state_and_notify('create_share',
|
self._set_share_state_and_notify(
|
||||||
{'status':
|
'create_share', {'status': constants.STATUS_ERROR},
|
||||||
constants.STATUS_ERROR},
|
|
||||||
context, ex, request_spec)
|
context, ex, request_spec)
|
||||||
|
|
||||||
def get_pools(self, context, filters=None):
|
def get_pools(self, context, filters=None):
|
||||||
@ -188,7 +190,7 @@ class SchedulerManager(manager.Manager):
|
|||||||
_migrate_share_set_error(self, context, ex, request_spec)
|
_migrate_share_set_error(self, context, ex, request_spec)
|
||||||
|
|
||||||
def _set_share_state_and_notify(self, method, state, context, ex,
|
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",
|
LOG.error("Failed to schedule %(method)s: %(ex)s",
|
||||||
{"method": method, "ex": ex})
|
{"method": method, "ex": ex})
|
||||||
@ -200,6 +202,12 @@ class SchedulerManager(manager.Manager):
|
|||||||
if share_id:
|
if share_id:
|
||||||
db.share_update(context, share_id, state)
|
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,
|
payload = dict(request_spec=request_spec,
|
||||||
share_properties=properties,
|
share_properties=properties,
|
||||||
share_id=share_id,
|
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(
|
db_result = engine.execute(ssi_table.select().where(
|
||||||
ssi_table.c.id == self.share_group_snapshot_member_id))
|
ssi_table.c.id == self.share_group_snapshot_member_id))
|
||||||
self.test_case.assertEqual(0, db_result.rowcount)
|
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)
|
result = db_api.share_type_get_by_name_or_id(self.ctxt, fake_id)
|
||||||
|
|
||||||
self.assertIsNone(result)
|
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.common import constants
|
||||||
from manila import context
|
from manila import context
|
||||||
from manila import db
|
from manila import db
|
||||||
|
from manila.message import message_levels
|
||||||
|
|
||||||
|
|
||||||
def _create_db_row(method, default_values, custom_values):
|
def _create_db_row(method, default_values, custom_values):
|
||||||
@ -264,3 +265,12 @@ def create_security_service(**kwargs):
|
|||||||
share_network_id,
|
share_network_id,
|
||||||
service_ref['id'])
|
service_ref['id'])
|
||||||
return service_ref
|
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:update": "rule:admin_api",
|
||||||
"share_group_types_spec:show": "rule:admin_api",
|
"share_group_types_spec:show": "rule:admin_api",
|
||||||
"share_group_types_spec:index": "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 context
|
||||||
from manila import db
|
from manila import db
|
||||||
from manila import exception
|
from manila import exception
|
||||||
|
from manila.message import message_field
|
||||||
from manila import quota
|
from manila import quota
|
||||||
from manila.scheduler.drivers import base
|
from manila.scheduler.drivers import base
|
||||||
from manila.scheduler.drivers import filter
|
from manila.scheduler.drivers import filter
|
||||||
@ -136,7 +137,9 @@ class SchedulerManagerTestCase(test.TestCase):
|
|||||||
assert_called_once_with(service_name, host, capabilities))
|
assert_called_once_with(service_name, host, capabilities))
|
||||||
|
|
||||||
@mock.patch.object(db, 'share_update', mock.Mock())
|
@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.
|
"""Test NoValidHost exception for create_share.
|
||||||
|
|
||||||
Puts the share in 'error' state and eats the exception.
|
Puts the share in 'error' state and eats the exception.
|
||||||
@ -144,9 +147,10 @@ class SchedulerManagerTestCase(test.TestCase):
|
|||||||
fake_share_id = 1
|
fake_share_id = 1
|
||||||
|
|
||||||
request_spec = {'share_id': fake_share_id}
|
request_spec = {'share_id': fake_share_id}
|
||||||
|
ex = exception.NoValidHost(reason='')
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
self.manager.driver, 'schedule_create_share',
|
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.mock_object(manager.LOG, 'error')
|
||||||
|
|
||||||
self.manager.create_share_instance(
|
self.manager.create_share_instance(
|
||||||
@ -158,6 +162,12 @@ class SchedulerManagerTestCase(test.TestCase):
|
|||||||
assert_called_once_with(self.context, request_spec, {}))
|
assert_called_once_with(self.context, request_spec, {}))
|
||||||
manager.LOG.error.assert_called_once_with(mock.ANY, mock.ANY)
|
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())
|
@mock.patch.object(db, 'share_update', mock.Mock())
|
||||||
def test_create_share_other_exception_puts_share_in_error_state(self):
|
def test_create_share_other_exception_puts_share_in_error_state(self):
|
||||||
"""Test any exception except NoValidHost for create_share.
|
"""Test any exception except NoValidHost for create_share.
|
||||||
|
@ -30,7 +30,7 @@ ShareGroup = [
|
|||||||
help="The minimum api microversion is configured to be the "
|
help="The minimum api microversion is configured to be the "
|
||||||
"value of the minimum microversion supported by Manila."),
|
"value of the minimum microversion supported by Manila."),
|
||||||
cfg.StrOpt("max_api_microversion",
|
cfg.StrOpt("max_api_microversion",
|
||||||
default="2.36",
|
default="2.37",
|
||||||
help="The maximum api microversion is configured to be the "
|
help="The maximum api microversion is configured to be the "
|
||||||
"value of the latest microversion supported by Manila."),
|
"value of the latest microversion supported by Manila."),
|
||||||
cfg.StrOpt("region",
|
cfg.StrOpt("region",
|
||||||
|
@ -186,6 +186,9 @@ class SharesV2Client(shares_client.SharesClient):
|
|||||||
elif "replica_id" in kwargs:
|
elif "replica_id" in kwargs:
|
||||||
return self._is_resource_deleted(
|
return self._is_resource_deleted(
|
||||||
self.get_share_replica, kwargs.get("replica_id"))
|
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:
|
else:
|
||||||
return super(SharesV2Client, self).is_resource_deleted(
|
return super(SharesV2Client, self).is_resource_deleted(
|
||||||
*args, **kwargs)
|
*args, **kwargs)
|
||||||
@ -1673,3 +1676,44 @@ class SharesV2Client(shares_client.SharesClient):
|
|||||||
"snapshots/%s/export-locations" % snapshot_id, version=version)
|
"snapshots/%s/export-locations" % snapshot_id, version=version)
|
||||||
self.expected_success(200, resp.status)
|
self.expected_success(200, resp.status)
|
||||||
return self._parse_resp(body)
|
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
|
"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):
|
class BaseSharesAltTest(BaseSharesTest):
|
||||||
"""Base test case class for all Shares Alt API tests."""
|
"""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