diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index 35a5efbeb..23239ae6a 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -51,6 +51,7 @@ REST_API_VERSION_HISTORY = """ passed to it as true. * 3.3 - Add user messages APIs. * 3.4 - Adds glance_metadata filter to list/detail volumes in _get_volumes. + * 3.5 - Add pagination support to messages API. """ @@ -59,7 +60,7 @@ REST_API_VERSION_HISTORY = """ # minimum version of the API supported. # Explicitly using /v1 or /v2 enpoints will still work _MIN_API_VERSION = "3.0" -_MAX_API_VERSION = "3.4" +_MAX_API_VERSION = "3.5" _LEGACY_API_VERSION1 = "1.0" _LEGACY_API_VERSION2 = "2.0" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 077ab365b..78f2f1893 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -60,3 +60,7 @@ user documentation. --- Added the filter parameters ``glance_metadata`` to list/detail volumes requests. + +3.5 +--- + Added pagination support to /messages API diff --git a/cinder/api/v3/messages.py b/cinder/api/v3/messages.py index f8cb4cdae..f4db1eeb7 100644 --- a/cinder/api/v3/messages.py +++ b/cinder/api/v3/messages.py @@ -18,6 +18,7 @@ from oslo_log import log as logging import webob from webob import exc +from cinder.api import common from cinder.api.openstack import wsgi from cinder.api.v3.views import messages as messages_view from cinder import exception @@ -89,8 +90,23 @@ class MessagesController(wsgi.Controller): """Returns a list of messages, transformed through view builder.""" context = req.environ['cinder.context'] check_policy(context, 'get_all') + filters = None + marker = None + limit = None + offset = None + sort_keys = None + sort_dirs = None - messages = self.message_api.get_all(context) + if (req.api_version_request.matches("3.5")): + filters = req.params.copy() + marker, limit, offset = common.get_pagination_params(filters) + sort_keys, sort_dirs = common.get_sort_params(filters) + + messages = self.message_api.get_all(context, filters=filters, + marker=marker, limit=limit, + offset=offset, + sort_keys=sort_keys, + sort_dirs=sort_dirs) for message in messages: # Fetches message text based on event id passed to it. diff --git a/cinder/db/api.py b/cinder/db/api.py index 73a48cf2c..2a94f5151 100644 --- a/cinder/db/api.py +++ b/cinder/db/api.py @@ -1126,8 +1126,11 @@ def message_get(context, message_id): return IMPL.message_get(context, message_id) -def message_get_all(context): - return IMPL.message_get_all(context) +def message_get_all(context, filters=None, marker=None, limit=None, + offset=None, sort_keys=None, sort_dirs=None): + return IMPL.message_get_all(context, filters=filters, marker=marker, + limit=limit, offset=offset, + sort_keys=sort_keys, sort_dirs=sort_dirs) def message_create(context, values): diff --git a/cinder/db/sqlalchemy/api.py b/cinder/db/sqlalchemy/api.py index 9f1b29e6d..9165884b9 100644 --- a/cinder/db/sqlalchemy/api.py +++ b/cinder/db/sqlalchemy/api.py @@ -4296,28 +4296,74 @@ def _translate_message(message): } -@require_context -def message_get(context, message_id): +def _message_get(context, message_id, session=None): query = model_query(context, models.Message, read_deleted="no", - project_only="yes") + project_only="yes", + session=session) 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(context, message_id, session=None): + result = _message_get(context, message_id, session) return _translate_message(result) @require_context -def message_get_all(context): - """Fetch all messages for the contexts project.""" +def message_get_all(context, filters=None, marker=None, limit=None, + offset=None, sort_keys=None, sort_dirs=None): + """Retrieves all messages. + + If no sort parameters are specified then the returned messages are + sorted first by the 'created_at' key and then by the 'id' key in + descending order. + + :param context: context to query under + :param marker: the last item of the previous page, used to determine the + next page of results to return + :param limit: maximum number of items to return + :param sort_keys: list of attributes by which results should be sorted, + paired with corresponding item in sort_dirs + :param sort_dirs: list of directions in which results should be sorted, + paired with corresponding item in sort_keys + :param filters: dictionary of filters; values that are in lists, tuples, + or sets cause an 'IN' operation, while exact matching + is used for other values, see + _process_messages_filters function for more + information + :returns: list of matching messages + """ messages = models.Message - query = (model_query(context, - messages, - read_deleted="no", - project_only="yes")) - results = query.all() - return _translate_messages(results) + + session = get_session() + with session.begin(): + # Generate the paginate query + query = _generate_paginate_query(context, session, marker, + limit, sort_keys, sort_dirs, filters, + offset, messages) + if query is None: + return [] + results = query.all() + return _translate_messages(results) + + +def _process_messages_filters(query, filters): + if filters: + # Ensure that filters' keys exist on the model + if not is_valid_model_filters(models.Message, filters): + return None + query = query.filter_by(**filters) + return query + + +def _messages_get_query(context, session=None, project_only=False): + return model_query(context, models.Message, session=session, + project_only=project_only) @require_context @@ -4402,7 +4448,9 @@ PAGINATION_HELPERS = { _volume_type_get_db_object), models.ConsistencyGroup: (_consistencygroups_get_query, _process_consistencygroups_filters, - _consistencygroup_get) + _consistencygroup_get), + models.Message: (_messages_get_query, _process_messages_filters, + _message_get) } diff --git a/cinder/message/api.py b/cinder/message/api.py index d631a5508..ae464e2df 100644 --- a/cinder/message/api.py +++ b/cinder/message/api.py @@ -64,9 +64,17 @@ class API(base.Base): """Return message with the specified id.""" return self.db.message_get(context, id) - def get_all(self, context): + def get_all(self, context, filters=None, marker=None, + limit=None, offset=None, sort_keys=None, + sort_dirs=None): """Return all messages for the given context.""" - messages = self.db.message_get_all(context) + + filters = filters or {} + + messages = self.db.message_get_all(context, filters=filters, + marker=marker, limit=limit, + offset=offset, sort_keys=sort_keys, + sort_dirs=sort_dirs) return messages def delete(self, context, id): diff --git a/cinder/tests/unit/message/test_api.py b/cinder/tests/unit/message/test_api.py index 59777dbce..59fb83fac 100644 --- a/cinder/tests/unit/message/test_api.py +++ b/cinder/tests/unit/message/test_api.py @@ -15,13 +15,21 @@ import mock from oslo_config import cfg from oslo_utils import timeutils +from cinder.api import extensions +from cinder.api.openstack import api_version_request as api_version +from cinder.api.v3 import messages from cinder import context from cinder.message import api as message_api from cinder.message import defined_messages from cinder import test +from cinder.tests.unit.api import fakes +import cinder.tests.unit.fake_constants as fake_constants +from cinder.tests.unit import utils CONF = cfg.CONF +version_header_name = 'OpenStack-API-Version' + class MessageApiTest(test.TestCase): def setUp(self): @@ -30,6 +38,9 @@ class MessageApiTest(test.TestCase): self.mock_object(self.message_api, 'db') self.ctxt = context.RequestContext('admin', 'fakeproject', True) self.ctxt.request_id = 'fakerequestid' + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = messages.MessagesController(self.ext_mgr) def test_create(self): CONF.set_override('message_ttl', 300) @@ -81,7 +92,9 @@ class MessageApiTest(test.TestCase): 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) + self.message_api.db.message_get_all.assert_called_once_with( + self.ctxt, filters={}, limit=None, marker=None, offset=None, + sort_dirs=None, sort_keys=None) def test_delete(self): admin_context = mock.Mock() @@ -92,3 +105,165 @@ class MessageApiTest(test.TestCase): self.message_api.db.message_destroy.assert_called_once_with( admin_context, 'fake_id') + + def create_message_for_tests(self): + """Create messages to test pagination functionality""" + utils.create_message( + self.ctxt, event_id=defined_messages.UNKNOWN_ERROR) + utils.create_message( + self.ctxt, event_id=defined_messages.UNABLE_TO_ALLOCATE) + utils.create_message( + self.ctxt, event_id=defined_messages.ATTACH_READONLY_VOLUME) + utils.create_message( + self.ctxt, event_id=defined_messages.IMAGE_FROM_VOLUME_OVER_QUOTA) + + def test_get_all_messages_with_limit(self): + self.create_message_for_tests() + + url = ('/v3/messages?limit=1') + req = fakes.HTTPRequest.blank(url) + req.method = 'GET' + req.content_type = 'application/json' + req.headers = {version_header_name: 'volume 3.5'} + req.api_version_request = api_version.max_api_version() + req.environ['cinder.context'].is_admin = True + + res = self.controller.index(req) + self.assertEqual(1, len(res['messages'])) + + url = ('/v3/messages?limit=3') + req = fakes.HTTPRequest.blank(url) + req.method = 'GET' + req.content_type = 'application/json' + req.headers = {version_header_name: 'volume 3.5'} + req.api_version_request = api_version.max_api_version() + req.environ['cinder.context'].is_admin = True + + res = self.controller.index(req) + self.assertEqual(3, len(res['messages'])) + + def test_get_all_messages_with_limit_wrong_version(self): + self.create_message_for_tests() + + url = ('/v3/messages?limit=1') + req = fakes.HTTPRequest.blank(url) + req.method = 'GET' + req.content_type = 'application/json' + req.headers["OpenStack-API-Version"] = "volume 3.3" + req.api_version_request = api_version.APIVersionRequest('3.3') + req.environ['cinder.context'].is_admin = True + + res = self.controller.index(req) + self.assertEqual(4, len(res['messages'])) + + def test_get_all_messages_with_offset(self): + self.create_message_for_tests() + + url = ('/v3/messages?offset=1') + req = fakes.HTTPRequest.blank(url) + req.method = 'GET' + req.content_type = 'application/json' + req.headers["OpenStack-API-Version"] = "volume 3.5" + req.api_version_request = api_version.APIVersionRequest('3.5') + req.environ['cinder.context'].is_admin = True + + res = self.controller.index(req) + self.assertEqual(3, len(res['messages'])) + + def test_get_all_messages_with_limit_and_offset(self): + self.create_message_for_tests() + + url = ('/v3/messages?limit=2&offset=1') + req = fakes.HTTPRequest.blank(url) + req.method = 'GET' + req.content_type = 'application/json' + req.headers["OpenStack-API-Version"] = "volume 3.5" + req.api_version_request = api_version.APIVersionRequest('3.5') + req.environ['cinder.context'].is_admin = True + + res = self.controller.index(req) + self.assertEqual(2, len(res['messages'])) + + def test_get_all_messages_with_filter(self): + self.create_message_for_tests() + + url = ('/v3/messages?' + 'event_id=%s') % defined_messages.UNKNOWN_ERROR + req = fakes.HTTPRequest.blank(url) + req.method = 'GET' + req.content_type = 'application/json' + req.headers["OpenStack-API-Version"] = "volume 3.5" + req.api_version_request = api_version.APIVersionRequest('3.5') + req.environ['cinder.context'].is_admin = True + + res = self.controller.index(req) + self.assertEqual(1, len(res['messages'])) + + def test_get_all_messages_with_sort(self): + self.create_message_for_tests() + + url = ('/v3/messages?sort=event_id:asc') + req = fakes.HTTPRequest.blank(url) + req.method = 'GET' + req.content_type = 'application/json' + req.headers["OpenStack-API-Version"] = "volume 3.5" + req.api_version_request = api_version.APIVersionRequest('3.5') + req.environ['cinder.context'].is_admin = True + + res = self.controller.index(req) + + expect_result = [defined_messages.UNKNOWN_ERROR, + defined_messages.UNABLE_TO_ALLOCATE, + defined_messages.IMAGE_FROM_VOLUME_OVER_QUOTA, + defined_messages.ATTACH_READONLY_VOLUME] + expect_result.sort() + + self.assertEqual(4, len(res['messages'])) + self.assertEqual(expect_result[0], + res['messages'][0]['event_id']) + self.assertEqual(expect_result[1], + res['messages'][1]['event_id']) + self.assertEqual(expect_result[2], + res['messages'][2]['event_id']) + self.assertEqual(expect_result[3], + res['messages'][3]['event_id']) + + def test_get_all_messages_paging(self): + self.create_message_for_tests() + + # first request of this test + url = ('/v3/fake/messages?limit=2') + req = fakes.HTTPRequest.blank(url) + req.method = 'GET' + req.content_type = 'application/json' + req.headers = {version_header_name: 'volume 3.5'} + req.api_version_request = api_version.max_api_version() + req.environ['cinder.context'].is_admin = True + + res = self.controller.index(req) + self.assertEqual(2, len(res['messages'])) + + next_link = ('http://localhost/v3/%s/messages?limit=' + '2&marker=%s') % (fake_constants.PROJECT_ID, + res['messages'][1]['id']) + self.assertEqual(next_link, + res['messages_links'][0]['href']) + + # Second request in this test + # Test for second page using marker (res['messages][0]['id']) + # values fetched in first request with limit 2 in this test + url = ('/v3/fake/messages?limit=1&marker=%s') % ( + res['messages'][0]['id']) + req = fakes.HTTPRequest.blank(url) + req.method = 'GET' + req.content_type = 'application/json' + req.headers = {version_header_name: 'volume 3.5'} + req.api_version_request = api_version.max_api_version() + req.environ['cinder.context'].is_admin = True + + result = self.controller.index(req) + self.assertEqual(1, len(result['messages'])) + + # checking second message of first request in this test with first + # message of second request. (to test paging mechanism) + self.assertEqual(res['messages'][1], result['messages'][0]) diff --git a/cinder/tests/unit/utils.py b/cinder/tests/unit/utils.py index a502c20e0..89eccb569 100644 --- a/cinder/tests/unit/utils.py +++ b/cinder/tests/unit/utils.py @@ -13,6 +13,7 @@ # under the License. # +import datetime import socket import sys import uuid @@ -198,6 +199,26 @@ def create_backup(ctxt, return db.backup_create(ctxt, backup) +def create_message(ctxt, + project_id='fake_project', + request_id='test_backup', + resource_type='This is a test backup', + resource_uuid='3asf434-3s433df43-434adf3-343df443', + event_id=None, + message_level='Error'): + """Create a message in the DB.""" + expires_at = (timeutils.utcnow() + datetime.timedelta( + seconds=30)) + message_record = {'project_id': project_id, + 'request_id': request_id, + 'resource_type': resource_type, + 'resource_uuid': resource_uuid, + 'event_id': event_id, + 'message_level': message_level, + 'expires_at': expires_at} + return db.message_create(ctxt, message_record) + + class ZeroIntervalLoopingCall(loopingcall.FixedIntervalLoopingCall): def start(self, interval, **kwargs): kwargs['initial_delay'] = 0