Add pagination support to messages

This patch adds pagination support to messages

Add pagination args like limit, marker, sort to query
messages

DocImpact
APIImpact
Co-Authored By: Alex Meade <mr.alex.meade@gmail.com>

Implements: BP add-pagination-to-messages
Change-Id: Ic57e3157143efdf8b03319ad6bb8ee3fb5454e62
This commit is contained in:
Sheel Rana 2016-05-10 21:17:54 +05:30
parent 73868becf3
commit b449025d5f
8 changed files with 295 additions and 19 deletions

View File

@ -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"

View File

@ -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

View File

@ -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.

View File

@ -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):

View File

@ -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)
}

View File

@ -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):

View File

@ -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])

View File

@ -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