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:
parent
73868becf3
commit
b449025d5f
cinder
api
db
message
tests/unit
@ -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"
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user