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
This commit is contained in:
Stephen Finucane 2021-03-19 11:34:31 +00:00
parent 6dc94e1fb8
commit 0eddab36e5
8 changed files with 579 additions and 3 deletions

View File

@ -0,0 +1,8 @@
==============
volume message
==============
Block Storage v3
.. autoprogram-cliff:: openstack.volume.v3
:command: volume message *

View File

@ -160,6 +160,7 @@ referring to both Compute and Volume quotas.
* ``volume backup record``: (**Volume**) volume record that can be imported or exported
* ``volume backend``: (**Volume**) volume backend storage
* ``volume host``: (**Volume**) the physical computer for volumes
* ``volume message``: (**Volume**) volume API internal messages detailing volume failure messages
* ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes
* ``volume snapshot``: (**Volume**) a point-in-time copy of a volume
* ``volume type``: (**Volume**) deployment-specific types of volumes available

View File

@ -72,9 +72,9 @@ list,volume list,Lists all volumes.
list-filters,,List enabled filters. (Supported by API versions 3.33 - 3.latest)
manage,volume create --remote-source k=v,Manage an existing volume.
manageable-list,,Lists all manageable volumes. (Supported by API versions 3.8 - 3.latest)
message-delete,,Removes one or more messages. (Supported by API versions 3.3 - 3.latest)
message-list,,Lists all messages. (Supported by API versions 3.3 - 3.latest)
message-show,,Shows message details. (Supported by API versions 3.3 - 3.latest)
message-delete,volume message delete,Removes one or more messages. (Supported by API versions 3.3 - 3.latest)
message-list,volume message list,Lists all messages. (Supported by API versions 3.3 - 3.latest)
message-show,volume message show,Shows message details. (Supported by API versions 3.3 - 3.latest)
metadata,volume set --property k=v / volume unset --property k,Sets or deletes volume metadata.
metadata-show,volume show,Shows volume metadata.
metadata-update-all,volume set --property k=v,Updates volume metadata.

1 absolute-limits limits show --absolute Lists absolute limits for a user.
72 list-filters List enabled filters. (Supported by API versions 3.33 - 3.latest)
73 manage volume create --remote-source k=v Manage an existing volume.
74 manageable-list Lists all manageable volumes. (Supported by API versions 3.8 - 3.latest)
75 message-delete volume message delete Removes one or more messages. (Supported by API versions 3.3 - 3.latest)
76 message-list volume message list Lists all messages. (Supported by API versions 3.3 - 3.latest)
77 message-show volume message show Shows message details. (Supported by API versions 3.3 - 3.latest)
78 metadata volume set --property k=v / volume unset --property k Sets or deletes volume metadata.
79 metadata-show volume show Shows volume metadata.
80 metadata-update-all volume set --property k=v Updates volume metadata.

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
---
features:
- |
Add ``volume message list``, ``volume message get`` and
``volume message delete`` commands, to list, get and delete volume
failure messages, respectively.

View File

@ -716,6 +716,10 @@ openstack.volume.v3 =
volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost
volume_message_delete = openstackclient.volume.v3.volume_message:DeleteMessage
volume_message_list = openstackclient.volume.v3.volume_message:ListMessages
volume_message_show = openstackclient.volume.v3.volume_message:ShowMessage
volume_snapshot_create = openstackclient.volume.v2.volume_snapshot:CreateVolumeSnapshot
volume_snapshot_delete = openstackclient.volume.v2.volume_snapshot:DeleteVolumeSnapshot
volume_snapshot_list = openstackclient.volume.v2.volume_snapshot:ListVolumeSnapshot