diff --git a/cinderclient/base.py b/cinderclient/base.py index 2374c5cb4..613a066f9 100644 --- a/cinderclient/base.py +++ b/cinderclient/base.py @@ -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 diff --git a/cinderclient/client.py b/cinderclient/client.py index aef00ef88..4ebc88f6a 100644 --- a/cinderclient/client.py +++ b/cinderclient/client.py @@ -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 ) diff --git a/cinderclient/tests/unit/v3/fakes.py b/cinderclient/tests/unit/v3/fakes.py index e61228e90..060b697c9 100644 --- a/cinderclient/tests/unit/v3/fakes.py +++ b/cinderclient/tests/unit/v3/fakes.py @@ -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} diff --git a/cinderclient/tests/unit/v3/test_messages.py b/cinderclient/tests/unit/v3/test_messages.py new file mode 100644 index 000000000..31ccdb6e3 --- /dev/null +++ b/cinderclient/tests/unit/v3/test_messages.py @@ -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) diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py index 7640b952e..a3718fa2b 100644 --- a/cinderclient/tests/unit/v3/test_shell.py +++ b/cinderclient/tests/unit/v3/test_shell.py @@ -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') diff --git a/cinderclient/v3/client.py b/cinderclient/v3/client.py index 363dad4ed..79bb110e2 100644 --- a/cinderclient/v3/client.py +++ b/cinderclient/v3/client.py @@ -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: diff --git a/cinderclient/v3/messages.py b/cinderclient/v3/messages.py new file mode 100644 index 000000000..8efd08884 --- /dev/null +++ b/cinderclient/v3/messages.py @@ -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 "" % 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) diff --git a/cinderclient/v3/shell.py b/cinderclient/v3/shell.py index 1ff8c425b..13097d232 100644 --- a/cinderclient/v3/shell.py +++ b/cinderclient/v3/shell.py @@ -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='', help='Host name.') @utils.arg('--backend_id', metavar='', 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='', + 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='', + default=None, + start_version='3.5', + help='Maximum number of messages to return. Default=None.') +@utils.arg('--sort', + metavar='[:]', + default=None, + start_version='3.5', + help=(('Comma-separated list of sort keys and directions in the ' + 'form of [:]. ' + 'Valid keys: %s. ' + 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) +@utils.arg('--resource_uuid', + metavar='', + default=None, + help='Filters results by a resource uuid. Default=None.') +@utils.arg('--resource_type', + metavar='', + default=None, + help='Filters results by a resource type. Default=None.') +@utils.arg('--event_id', + metavar='', + default=None, + help='Filters results by event id. Default=None.') +@utils.arg('--request_id', + metavar='', + default=None, + help='Filters results by request id. Default=None.') +@utils.arg('--level', + metavar='', + 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='', + 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='', 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.") diff --git a/releasenotes/notes/messages-v3-api-3da81f4f66bf5903.yaml b/releasenotes/notes/messages-v3-api-3da81f4f66bf5903.yaml new file mode 100644 index 000000000..1033dcd12 --- /dev/null +++ b/releasenotes/notes/messages-v3-api-3da81f4f66bf5903.yaml @@ -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}