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
a7526f7104
commit
ba8a160c34
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
Loading…
Reference in New Issue