diff options
| author | Stephen Finucane <sfinucan@redhat.com> | 2021-03-19 11:34:31 +0000 |
|---|---|---|
| committer | Stephen Finucane <sfinucan@redhat.com> | 2021-06-03 15:52:18 +0100 |
| commit | 0eddab36e52e813e2329ac10044fa4f67830efec (patch) | |
| tree | 8133b20828ba1b3ed135c6ac0abef0b41dfe5a5d /openstackclient | |
| parent | 6dc94e1fb85595653dcdd24185c914b9df1741df (diff) | |
| download | python-openstackclient-0eddab36e52e813e2329ac10044fa4f67830efec.tar.gz | |
volume: Add 'volume message *' commands
This patch implements the necessary commands to utilize the Messages API
introduced in Cinder API version 3.3. Version 3.5 built upon this by
implementing pagination support for these commands which is present in
this patch as well.
volume message get
volume message list
volume message delete
Change-Id: I64aa0b4a8d4468baa8c63e5e30ee31de68df999d
Diffstat (limited to 'openstackclient')
| -rw-r--r-- | openstackclient/tests/unit/volume/v3/fakes.py | 68 | ||||
| -rw-r--r-- | openstackclient/tests/unit/volume/v3/test_volume_message.py | 324 | ||||
| -rw-r--r-- | openstackclient/volume/v3/volume_message.py | 165 |
3 files changed, 557 insertions, 0 deletions
diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py index fb3b1b74..45cad8c1 100644 --- a/openstackclient/tests/unit/volume/v3/fakes.py +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -32,6 +32,8 @@ class FakeVolumeClient(object): self.attachments = mock.Mock() self.attachments.resource_class = fakes.FakeResource(None, {}) + self.messages = mock.Mock() + self.messages.resource_class = fakes.FakeResource(None, {}) self.volumes = mock.Mock() self.volumes.resource_class = fakes.FakeResource(None, {}) @@ -59,6 +61,72 @@ class TestVolume(utils.TestCommand): FakeVolume = volume_v2_fakes.FakeVolume +class FakeVolumeMessage: + """Fake one or more volume messages.""" + + @staticmethod + def create_one_volume_message(attrs=None): + """Create a fake message. + + :param attrs: A dictionary with all attributes of message + :return: A FakeResource object with id, name, status, etc. + """ + attrs = attrs or {} + + # Set default attribute + message_info = { + 'created_at': '2016-02-11T11:17:37.000000', + 'event_id': f'VOLUME_{random.randint(1, 999999):06d}', + 'guaranteed_until': '2016-02-11T11:17:37.000000', + 'id': uuid.uuid4().hex, + 'message_level': 'ERROR', + 'request_id': f'req-{uuid.uuid4().hex}', + 'resource_type': 'VOLUME', + 'resource_uuid': uuid.uuid4().hex, + 'user_message': f'message-{uuid.uuid4().hex}', + } + + # Overwrite default attributes if there are some attributes set + message_info.update(attrs) + + message = fakes.FakeResource( + None, + message_info, + loaded=True) + return message + + @staticmethod + def create_volume_messages(attrs=None, count=2): + """Create multiple fake messages. + + :param attrs: A dictionary with all attributes of message + :param count: The number of messages to be faked + :return: A list of FakeResource objects + """ + messages = [] + for n in range(0, count): + messages.append(FakeVolumeMessage.create_one_volume_message(attrs)) + + return messages + + @staticmethod + def get_volume_messages(messages=None, count=2): + """Get an iterable MagicMock object with a list of faked messages. + + If messages list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param messages: A list of FakeResource objects faking messages + :param count: The number of messages to be faked + :return An iterable Mock object with side_effect set to a list of faked + messages + """ + if messages is None: + messages = FakeVolumeMessage.create_messages(count) + + return mock.Mock(side_effect=messages) + + class FakeVolumeAttachment: """Fake one or more volume attachments.""" diff --git a/openstackclient/tests/unit/volume/v3/test_volume_message.py b/openstackclient/tests/unit/volume/v3/test_volume_message.py new file mode 100644 index 00000000..68becf44 --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_volume_message.py @@ -0,0 +1,324 @@ +# 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. + +from unittest.mock import call + +from cinderclient import api_versions +from osc_lib import exceptions + +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import volume_message + + +class TestVolumeMessage(volume_fakes.TestVolume): + + def setUp(self): + super().setUp() + + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + self.volume_messages_mock = self.app.client_manager.volume.messages + self.volume_messages_mock.reset_mock() + + +class TestVolumeMessageDelete(TestVolumeMessage): + + fake_messages = volume_fakes.FakeVolumeMessage.create_volume_messages( + count=2) + + def setUp(self): + super().setUp() + + self.volume_messages_mock.get = \ + volume_fakes.FakeVolumeMessage.get_volume_messages( + self.fake_messages) + self.volume_messages_mock.delete.return_value = None + + # Get the command object to mock + self.cmd = volume_message.DeleteMessage(self.app, None) + + def test_message_delete(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [ + self.fake_messages[0].id, + ] + verifylist = [ + ('message_ids', [self.fake_messages[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_messages_mock.delete.assert_called_with( + self.fake_messages[0].id) + self.assertIsNone(result) + + def test_message_delete_multiple_messages(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [ + self.fake_messages[0].id, + self.fake_messages[1].id, + ] + verifylist = [ + ('message_ids', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for m in self.fake_messages: + calls.append(call(m.id)) + self.volume_messages_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + def test_message_delete_multiple_messages_with_exception(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [ + self.fake_messages[0].id, + 'invalid_message', + ] + verifylist = [ + ('message_ids', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.volume_messages_mock.delete.side_effect = [ + self.fake_messages[0], exceptions.CommandError] + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + self.assertEqual('Failed to delete 1 of 2 messages.', str(exc)) + + self.volume_messages_mock.delete.assert_any_call( + self.fake_messages[0].id) + self.volume_messages_mock.delete.assert_any_call('invalid_message') + + self.assertEqual(2, self.volume_messages_mock.delete.call_count) + + def test_message_delete_pre_v33(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.2') + + arglist = [ + self.fake_messages[0].id, + ] + verifylist = [ + ('message_ids', [self.fake_messages[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.3 or greater is required', + str(exc)) + + +class TestVolumeMessageList(TestVolumeMessage): + + fake_project = identity_fakes.FakeProject.create_one_project() + fake_messages = volume_fakes.FakeVolumeMessage.create_volume_messages( + count=3) + + columns = ( + 'ID', + 'Event ID', + 'Resource Type', + 'Resource UUID', + 'Message Level', + 'User Message', + 'Request ID', + 'Created At', + 'Guaranteed Until', + ) + data = [] + for fake_message in fake_messages: + data.append(( + fake_message.id, + fake_message.event_id, + fake_message.resource_type, + fake_message.resource_uuid, + fake_message.message_level, + fake_message.user_message, + fake_message.request_id, + fake_message.created_at, + fake_message.guaranteed_until, + )) + + def setUp(self): + super().setUp() + + self.projects_mock.get.return_value = self.fake_project + self.volume_messages_mock.list.return_value = self.fake_messages + # Get the command to test + self.cmd = volume_message.ListMessages(self.app, None) + + def test_message_list(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [] + verifylist = [ + ('project', None), + ('marker', None), + ('limit', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'project_id': None, + } + self.volume_messages_mock.list.assert_called_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + + def test_message_list_with_options(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [ + '--project', self.fake_project.name, + '--marker', self.fake_messages[0].id, + '--limit', '3', + ] + verifylist = [ + ('project', self.fake_project.name), + ('marker', self.fake_messages[0].id), + ('limit', 3), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'project_id': self.fake_project.id, + } + self.volume_messages_mock.list.assert_called_with( + search_opts=search_opts, + marker=self.fake_messages[0].id, + limit=3, + ) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + + def test_message_list_pre_v33(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.2') + + arglist = [] + verifylist = [ + ('project', None), + ('marker', None), + ('limit', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.3 or greater is required', + str(exc)) + + +class TestVolumeMessageShow(TestVolumeMessage): + + fake_message = volume_fakes.FakeVolumeMessage.create_one_volume_message() + + columns = ( + 'created_at', + 'event_id', + 'guaranteed_until', + 'id', + 'message_level', + 'request_id', + 'resource_type', + 'resource_uuid', + 'user_message', + ) + data = ( + fake_message.created_at, + fake_message.event_id, + fake_message.guaranteed_until, + fake_message.id, + fake_message.message_level, + fake_message.request_id, + fake_message.resource_type, + fake_message.resource_uuid, + fake_message.user_message, + ) + + def setUp(self): + super().setUp() + + self.volume_messages_mock.get.return_value = self.fake_message + # Get the command object to test + self.cmd = volume_message.ShowMessage(self.app, None) + + def test_message_show(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [ + self.fake_message.id + ] + verifylist = [ + ('message_id', self.fake_message.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.volume_messages_mock.get.assert_called_with(self.fake_message.id) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_message_show_pre_v33(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.2') + + arglist = [ + self.fake_message.id + ] + verifylist = [ + ('message_id', self.fake_message.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.3 or greater is required', + str(exc)) diff --git a/openstackclient/volume/v3/volume_message.py b/openstackclient/volume/v3/volume_message.py new file mode 100644 index 00000000..4fe5ae92 --- /dev/null +++ b/openstackclient/volume/v3/volume_message.py @@ -0,0 +1,165 @@ +# +# 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. +# + +"""Volume V3 Messages implementations""" + +import logging as LOG + +from cinderclient import api_versions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +class DeleteMessage(command.Command): + _description = _('Delete a volume failure message') + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'message_ids', + metavar='<message-id>', + nargs='+', + help=_('Message(s) to delete (ID)') + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.3'): + msg = _( + "--os-volume-api-version 3.3 or greater is required to " + "support the 'volume message delete' command" + ) + raise exceptions.CommandError(msg) + + errors = 0 + for message_id in parsed_args.message_ids: + try: + volume_client.messages.delete(message_id) + except Exception: + LOG.error(_('Failed to delete message: %s'), message_id) + errors += 1 + + if errors > 0: + total = len(parsed_args.message_ids) + msg = _('Failed to delete %(errors)s of %(total)s messages.') % { + 'errors': errors, 'total': total, + } + raise exceptions.CommandError(msg) + + +class ListMessages(command.Lister): + _description = _('List volume failure messages') + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + + parser.add_argument( + '--project', + metavar='<project>', + help=_('Filter results by project (name or ID) (admin only)'), + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--marker', + metavar='<message-id>', + help=_('The last message ID of the previous page'), + default=None, + ) + parser.add_argument( + '--limit', + type=int, + metavar='<limit>', + help=_('Maximum number of messages to display'), + default=None, + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + identity_client = self.app.client_manager.identity + + if volume_client.api_version < api_versions.APIVersion('3.3'): + msg = _( + "--os-volume-api-version 3.3 or greater is required to " + "support the 'volume message list' command" + ) + raise exceptions.CommandError(msg) + + column_headers = ( + 'ID', + 'Event ID', + 'Resource Type', + 'Resource UUID', + 'Message Level', + 'User Message', + 'Request ID', + 'Created At', + 'Guaranteed Until', + ) + + project_id = None + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain).id + + search_opts = { + 'project_id': project_id, + } + data = volume_client.messages.list( + search_opts=search_opts, + marker=parsed_args.marker, + limit=parsed_args.limit) + + return ( + column_headers, + (utils.get_item_properties(s, column_headers) for s in data) + ) + + +class ShowMessage(command.ShowOne): + _description = _('Show a volume failure message') + + def get_parser(self, prog_name): + parser = super(ShowMessage, self).get_parser(prog_name) + parser.add_argument( + 'message_id', + metavar='<message-id>', + help=_('Message to show (ID).') + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.3'): + msg = _( + "--os-volume-api-version 3.3 or greater is required to " + "support the 'volume message show' command" + ) + raise exceptions.CommandError(msg) + + message = volume_client.messages.get(parsed_args.message_id) + + return zip(*sorted(message._info.items())) |
