From ba8a160c3450b0f52399f30c0893747a877c2a4c Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Fri, 6 May 2016 09:33:09 -0400 Subject: [PATCH] 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/ DELETE /messages/ 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 Change-Id: Ia0cc524e0bfb2ca5e495e575e17e9911c746690b --- manila_tempest_tests/config.py | 2 +- .../services/share/v2/json/shares_client.py | 44 ++++++++ .../tests/api/admin/test_user_messages.py | 103 ++++++++++++++++++ .../api/admin/test_user_messages_negative.py | 58 ++++++++++ manila_tempest_tests/tests/api/base.py | 19 ++++ 5 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 manila_tempest_tests/tests/api/admin/test_user_messages.py create mode 100644 manila_tempest_tests/tests/api/admin/test_user_messages_negative.py diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py index b48ac9f4..e13d334c 100644 --- a/manila_tempest_tests/config.py +++ b/manila_tempest_tests/config.py @@ -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", diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py index 353c8cc7..510b2f74 100644 --- a/manila_tempest_tests/services/share/v2/json/shares_client.py +++ b/manila_tempest_tests/services/share/v2/json/shares_client.py @@ -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) diff --git a/manila_tempest_tests/tests/api/admin/test_user_messages.py b/manila_tempest_tests/tests/api/admin/test_user_messages.py new file mode 100644 index 00000000..1d23487e --- /dev/null +++ b/manila_tempest_tests/tests/api/admin/test_user_messages.py @@ -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']) diff --git a/manila_tempest_tests/tests/api/admin/test_user_messages_negative.py b/manila_tempest_tests/tests/api/admin/test_user_messages_negative.py new file mode 100644 index 00000000..47eed3b5 --- /dev/null +++ b/manila_tempest_tests/tests/api/admin/test_user_messages_negative.py @@ -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())) diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py index 3df71537..764a8529 100644 --- a/manila_tempest_tests/tests/api/base.py +++ b/manila_tempest_tests/tests/api/base.py @@ -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."""