Add v3 user messages with pagination

GET /messages
GET /messages/{id}
DELETE /message/{id}

Partially-Implements: blueprint summarymessage
Depends-On: I398cbd02b61f30918a427291d1d3ae00435e0f4c
Change-Id: Ic057ab521c048a376d2a6bed513b8eb8118810d1
This commit is contained in:
Alex Meade 2016-03-29 21:28:01 -04:00
parent 29099bbc68
commit 1c87b6fa71
9 changed files with 349 additions and 6 deletions

View File

@ -38,7 +38,11 @@ SORT_KEY_VALUES = ('id', 'status', 'size', 'availability_zone', 'name',
# Mapping of client keys to actual sort keys
SORT_KEY_MAPPINGS = {'name': 'display_name'}
# Additional sort keys for resources
SORT_KEY_ADD_VALUES = {'backups': ('data_timestamp', ), }
SORT_KEY_ADD_VALUES = {
'backups': ('data_timestamp', ),
'messages': ('resource_type', 'event_id', 'resource_uuid',
'message_level', 'guaranteed_until', 'request_id'),
}
Resource = common_base.Resource

View File

@ -639,7 +639,8 @@ def _construct_http_client(username=None, password=None, project_id=None,
cacert=cacert,
auth_system=auth_system,
auth_plugin=auth_plugin,
logger=logger
logger=logger,
api_version=api_version
)

View File

@ -289,7 +289,6 @@ class FakeHTTPClient(fake_v2.FakeHTTPClient):
#
# Groups
#
def get_groups_detail(self, **kw):
return (200, {}, {"groups": [
_stub_group(id='1234'),
@ -414,3 +413,50 @@ class FakeHTTPClient(fake_v2.FakeHTTPClient):
"source_identifier": "myvol", "size": 5,
"extra_info": "qos_setting:low", "reason_not_safe": None}]
return (200, {}, {"manageable-snapshots": snaps})
#
# Messages
#
def get_messages(self, **kw):
return 200, {}, {'messages': [
{
'id': '1234',
'event_id': 'VOLUME_000002',
'user_message': 'Fake Message',
'created_at': '2012-08-27T00:00:00.000000',
'guaranteed_until': "2013-11-12T21:00:00.000000",
},
{
'id': '12345',
'event_id': 'VOLUME_000002',
'user_message': 'Fake Message',
'created_at': '2012-08-27T00:00:00.000000',
'guaranteed_until': "2013-11-12T21:00:00.000000",
}
]}
def delete_messages_1234(self, **kw):
return 204, {}, None
def delete_messages_12345(self, **kw):
return 204, {}, None
def get_messages_1234(self, **kw):
message = {
'id': '1234',
'event_id': 'VOLUME_000002',
'user_message': 'Fake Message',
'created_at': '2012-08-27T00:00:00.000000',
'guaranteed_until': "2013-11-12T21:00:00.000000",
}
return 200, {}, {'message': message}
def get_messages_12345(self, **kw):
message = {
'id': '12345',
'event_id': 'VOLUME_000002',
'user_message': 'Fake Message',
'created_at': '2012-08-27T00:00:00.000000',
'guaranteed_until': "2013-11-12T21:00:00.000000",
}
return 200, {}, {'message': message}

View File

@ -0,0 +1,55 @@
# 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.
import ddt
from six.moves.urllib import parse
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v3 import fakes
cs = fakes.FakeClient()
@ddt.ddt
class MessagesTest(utils.TestCase):
def test_list_messages(self):
cs.messages.list()
cs.assert_called('GET', '/messages')
@ddt.data('id', 'id:asc', 'id:desc', 'resource_type', 'event_id',
'resource_uuid', 'message_level', 'guaranteed_until',
'request_id')
def test_list_messages_with_sort(self, sort_string):
cs.messages.list(sort=sort_string)
cs.assert_called('GET', '/messages?sort=%s' % parse.quote(sort_string))
@ddt.data('id', 'resource_type', 'event_id', 'resource_uuid',
'message_level', 'guaranteed_until', 'request_id')
def test_list_messages_with_filters(self, filter_string):
cs.messages.list(search_opts={filter_string: 'value'})
cs.assert_called('GET', '/messages?%s=value' % parse.quote(
filter_string))
@ddt.data('fake', 'fake:asc', 'fake:desc')
def test_list_messages_with_invalid_sort(self, sort_string):
self.assertRaises(ValueError, cs.messages.list, sort=sort_string)
def test_get_messages(self):
fake_id = '1234'
cs.messages.get(fake_id)
cs.assert_called('GET', '/messages/%s' % fake_id)
def test_delete_messages(self):
fake_id = '1234'
cs.messages.delete(fake_id)
cs.assert_called('DELETE', '/messages/%s' % fake_id)

View File

@ -14,7 +14,6 @@
# under the License.
import ddt
import fixtures
import mock
from requests_mock.contrib import fixture as requests_mock_fixture
@ -339,3 +338,41 @@ class ShellTest(utils.TestCase):
self.run_command('--os-volume-api-version 3.8 '
'snapshot-manageable-list fakehost --detailed False')
self.assert_called('GET', '/manageable_snapshots?host=fakehost')
def test_list_messages(self):
self.run_command('--os-volume-api-version 3.3 message-list')
self.assert_called('GET', '/messages')
@ddt.data(('resource_type',), ('event_id',), ('resource_uuid',),
('level', 'message_level'), ('request_id',))
def test_list_messages_with_filters(self, filter):
self.run_command('--os-volume-api-version 3.5 message-list --%s=TEST'
% filter[0])
self.assert_called('GET', '/messages?%s=TEST' % filter[-1])
def test_list_messages_with_sort(self):
self.run_command('--os-volume-api-version 3.5 '
'message-list --sort=id:asc')
self.assert_called('GET', '/messages?sort=id%3Aasc')
def test_list_messages_with_limit(self):
self.run_command('--os-volume-api-version 3.5 message-list --limit=1')
self.assert_called('GET', '/messages?limit=1')
def test_list_messages_with_marker(self):
self.run_command('--os-volume-api-version 3.5 message-list --marker=1')
self.assert_called('GET', '/messages?marker=1')
def test_show_message(self):
self.run_command('--os-volume-api-version 3.5 message-show 1234')
self.assert_called('GET', '/messages/1234')
def test_delete_message(self):
self.run_command('--os-volume-api-version 3.5 message-delete 1234')
self.assert_called('DELETE', '/messages/1234')
def test_delete_messages(self):
self.run_command(
'--os-volume-api-version 3.3 message-delete 1234 12345')
self.assert_called_anytime('DELETE', '/messages/1234')
self.assert_called_anytime('DELETE', '/messages/12345')

View File

@ -26,6 +26,7 @@ from cinderclient.v3 import groups
from cinderclient.v3 import group_snapshots
from cinderclient.v3 import group_types
from cinderclient.v3 import limits
from cinderclient.v3 import messages
from cinderclient.v3 import pools
from cinderclient.v3 import qos_specs
from cinderclient.v3 import quota_classes
@ -68,6 +69,7 @@ class Client(object):
password = api_key
self.version = '3.0'
self.limits = limits.LimitsManager(self)
self.api_version = api_version or api_versions.APIVersion(self.version)
# extensions
self.volumes = volumes.VolumeManager(self)
@ -82,6 +84,7 @@ class Client(object):
self.quota_classes = quota_classes.QuotaClassSetManager(self)
self.quotas = quotas.QuotaSetManager(self)
self.backups = volume_backups.VolumeBackupManager(self)
self.messages = messages.MessageManager(self)
self.restores = volume_backups_restore.VolumeBackupRestoreManager(self)
self.transfers = volume_transfers.VolumeTransferManager(self)
self.services = services.ServiceManager(self)
@ -95,7 +98,6 @@ class Client(object):
availability_zones.AvailabilityZoneManager(self)
self.pools = pools.PoolManager(self)
self.capabilities = capabilities.CapabilitiesManager(self)
self.api_version = api_version or api_versions.APIVersion(self.version)
# Add in any extensions...
if extensions:

View File

@ -0,0 +1,77 @@
# 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.
"""Message interface (v3 extension)."""
from cinderclient import base
from cinderclient import api_versions
class Message(base.Resource):
NAME_ATTR = 'id'
def __repr__(self):
return "<Message: %s>" % self.id
def delete(self):
"""Delete this message."""
return self.manager.delete(self)
class MessageManager(base.ManagerWithFind):
"""Manage :class:`Message` resources."""
resource_class = Message
@api_versions.wraps('3.3')
def get(self, message_id):
"""Get a message.
:param message_id: The ID of the message to get.
:rtype: :class:`Message`
"""
return self._get("/messages/%s" % message_id, "message")
@api_versions.wraps('3.3', '3.4')
def list(self, **kwargs):
"""Lists all messages.
:rtype: list of :class:`Message`
"""
resource_type = "messages"
url = self._build_list_url(resource_type, detailed=False)
return self._list(url, resource_type)
@api_versions.wraps('3.5')
def list(self, search_opts=None, marker=None, limit=None, sort=None):
"""Lists all messages.
:param search_opts: Search options to filter out volumes.
:param marker: Begin returning volumes that appear later in the volume
list than that represented by this volume id.
:param limit: Maximum number of volumes to return.
:param sort: Sort information
:rtype: list of :class:`Message`
"""
resource_type = "messages"
url = self._build_list_url(resource_type, detailed=False,
search_opts=search_opts, marker=marker,
limit=limit, sort=sort)
return self._list(url, resource_type, limit=limit)
@api_versions.wraps('3.3')
def delete(self, message):
"""Delete a message."""
loc = "/messages/%s" % base.getid(message)
return self._delete(loc)

View File

@ -113,6 +113,11 @@ def _find_qos_specs(cs, qos_specs):
return utils.find_resource(cs.qos_specs, qos_specs)
def _find_message(cs, message):
"""Gets a message by ID."""
return utils.find_resource(cs.messages, message)
def _print_volume_snapshot(snapshot):
utils.print_dict(snapshot._info)
@ -3352,11 +3357,11 @@ def do_thaw_host(cs, args):
cs.services.thaw_host(args.host)
@utils.service_type('volumev3')
@utils.arg('host', metavar='<hostname>', help='Host name.')
@utils.arg('--backend_id',
metavar='<backend-id>',
help='ID of backend to failover to (Default=None)')
@utils.service_type('volumev3')
def do_failover_host(cs, args):
"""Failover a replicating cinder-volume host."""
cs.services.failover_host(args.host, args.backend_id)
@ -3369,3 +3374,108 @@ def do_api_version(cs, args):
columns = ['ID', 'Status', 'Version', 'Min_version']
response = cs.services.server_api_version()
utils.print_list(response, columns)
@utils.service_type('volumev3')
@api_versions.wraps("3.3")
@utils.arg('--marker',
metavar='<marker>',
default=None,
start_version='3.5',
help='Begin returning message that appear later in the message '
'list than that represented by this id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
start_version='3.5',
help='Maximum number of messages to return. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
start_version='3.5',
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--resource_uuid',
metavar='<resource_uuid>',
default=None,
help='Filters results by a resource uuid. Default=None.')
@utils.arg('--resource_type',
metavar='<type>',
default=None,
help='Filters results by a resource type. Default=None.')
@utils.arg('--event_id',
metavar='<id>',
default=None,
help='Filters results by event id. Default=None.')
@utils.arg('--request_id',
metavar='<request_id>',
default=None,
help='Filters results by request id. Default=None.')
@utils.arg('--level',
metavar='<level>',
default=None,
help='Filters results by the message level. Default=None.')
def do_message_list(cs, args):
"""Lists all messages."""
search_opts = {
'resource_uuid': args.resource_uuid,
'event_id': args.event_id,
'request_id': args.request_id,
}
if args.resource_type:
search_opts['resource_type'] = args.resource_type.upper()
if args.level:
search_opts['message_level'] = args.level.upper()
marker = args.marker if hasattr(args, 'marker') else None
limit = args.limit if hasattr(args, 'limit') else None
sort = args.sort if hasattr(args, 'sort') else None
messages = cs.messages.list(search_opts=search_opts,
marker=marker,
limit=limit,
sort=sort)
columns = ['ID', 'Resource Type', 'Resource UUID', 'Event ID',
'User Message']
if sort:
sortby_index = None
else:
sortby_index = 0
utils.print_list(messages, columns, sortby_index=sortby_index)
@utils.service_type('volumev3')
@api_versions.wraps("3.3")
@utils.arg('message',
metavar='<message>',
help='ID of message.')
def do_message_show(cs, args):
"""Shows message details."""
info = dict()
message = _find_message(cs, args.message)
info.update(message._info)
info.pop('links', None)
utils.print_dict(info)
@utils.service_type('volumev3')
@api_versions.wraps("3.3")
@utils.arg('message',
metavar='<message>', nargs='+',
help='ID of one or more message to be deleted.')
def do_message_delete(cs, args):
"""Removes one or more messages."""
failure_count = 0
for message in args.message:
try:
_find_message(cs, message).delete()
except Exception as e:
failure_count += 1
print("Delete for message %s failed: %s" % (message, e))
if failure_count == len(args.message):
raise exceptions.CommandError("Unable to delete any of the specified "
"messages.")

View File

@ -0,0 +1,11 @@
---
features:
- |
Add support for /messages API
GET /messages
cinder --os-volume-api-version 3.3 message-list
GET /messages/{id}
cinder --os-volume-api-version 3.3 message-show {id}
DELETE /message/{id}
cinder --os-volume-api-version 3.3 message-delete {id}