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
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index b48ac9f..e13d334 100644
--- a/manila_tempest_tests/config.py
+++ b/manila_tempest_tests/config.py
@@ -30,7 +30,7 @@
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 353c8cc..510b2f7 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 @@
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 @@
"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 0000000..1d23487
--- /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 0000000..47eed3b
--- /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 3df7153..764a852 100644
--- a/manila_tempest_tests/tests/api/base.py
+++ b/manila_tempest_tests/tests/api/base.py
@@ -998,6 +998,25 @@
"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."""