Browse Source

Add user messages for some volume snapshot actions

Also adds tests to help keep the message_field module
consistent.

Co-authored-by: TommyLike <tommylikehu@gmail.com>
Co-authored-by: Brian Rosmaita <rosmaita.fossdev@gmail.com>

Change-Id: I8a16c9742cadfee1a75a6d738945476a9fc7c19b
changes/75/447975/2
TommyLike 5 years ago
committed by Brian Rosmaita
parent
commit
46bec90f03
  1. 25
      cinder/message/message_field.py
  2. 21
      cinder/tests/unit/message/test_message_field.py
  3. 148
      cinder/tests/unit/volume/test_volume_manager.py
  4. 34
      cinder/volume/manager.py

25
cinder/message/message_field.py

@ -27,6 +27,7 @@ from cinder.i18n import _
class Resource(object):
VOLUME = 'VOLUME'
VOLUME_SNAPSHOT = 'VOLUME_SNAPSHOT'
class Action(object):
@ -40,6 +41,10 @@ class Action(object):
EXTEND_VOLUME = ('007', _('extend volume'))
CREATE_VOLUME_FROM_BACKEND = ('008',
_('create volume from backend storage'))
SNAPSHOT_CREATE = ('009', _('create snapshot'))
SNAPSHOT_DELETE = ('010', _('delete snapshot'))
SNAPSHOT_UPDATE = ('011', _('update snapshot'))
SNAPSHOT_METADATA_UPDATE = ('012', _('update snapshot metadata'))
ALL = (SCHEDULE_ALLOCATE_VOLUME,
ATTACH_VOLUME,
@ -48,7 +53,11 @@ class Action(object):
COPY_IMAGE_TO_VOLUME,
UNMANAGE_VOLUME,
EXTEND_VOLUME,
CREATE_VOLUME_FROM_BACKEND
CREATE_VOLUME_FROM_BACKEND,
SNAPSHOT_CREATE,
SNAPSHOT_DELETE,
SNAPSHOT_UPDATE,
SNAPSHOT_METADATA_UPDATE,
)
@ -85,6 +94,12 @@ class Detail(object):
DRIVER_FAILED_CREATE = (
'012',
_('Driver failed to create the volume.'))
SNAPSHOT_CREATE_ERROR = ('013', _("Snapshot failed to create."))
SNAPSHOT_UPDATE_METADATA_FAILED = (
'014',
_("Volume snapshot update metadata failed."))
SNAPSHOT_IS_BUSY = ('015', _("Snapshot is busy."))
SNAPSHOT_DELETE_ERROR = ('016', _("Snapshot failed to delete."))
ALL = (UNKNOWN_ERROR,
DRIVER_NOT_INITIALIZED,
@ -97,7 +112,12 @@ class Detail(object):
NOTIFY_COMPUTE_SERVICE_FAILED,
DRIVER_FAILED_EXTEND,
SIGNATURE_VERIFICATION_FAILED,
DRIVER_FAILED_CREATE)
DRIVER_FAILED_CREATE,
SNAPSHOT_CREATE_ERROR,
SNAPSHOT_UPDATE_METADATA_FAILED,
SNAPSHOT_IS_BUSY,
SNAPSHOT_DELETE_ERROR,
)
# Exception and detail mappings
EXCEPTION_DETAIL_MAPPINGS = {
@ -108,6 +128,7 @@ class Detail(object):
'BackupLimitExceeded',
'SnapshotLimitExceeded'],
NOT_ENOUGH_SPACE_FOR_IMAGE: ['ImageTooBig'],
SNAPSHOT_IS_BUSY: ['SnapshotIsBusy'],
}

21
cinder/tests/unit/message/test_message_field.py

@ -30,11 +30,32 @@ class MessageFieldTest(test.TestCase):
action_ids = [x[0] for x in message_field.Action.ALL]
self.assertEqual(len(action_ids), len(set(action_ids)))
def test_all_action_fields_in_ALL(self):
"""Assert that all and only defined fields are in the ALL tuple"""
defined_fields = [k for k in message_field.Action.__dict__.keys()
if k != 'ALL' and not k.startswith('__')]
for d in defined_fields:
self.assertIn(getattr(message_field.Action, d),
message_field.Action.ALL)
self.assertEqual(len(message_field.Action.ALL),
len(defined_fields))
def test_unique_detail_ids(self):
"""Assert that no detail_id is duplicated."""
detail_ids = [x[0] for x in message_field.Detail.ALL]
self.assertEqual(len(detail_ids), len(set(detail_ids)))
def test_all_detail_fields_in_ALL(self):
"""Assert that all and only defined fields are in the ALL tuple"""
defined_fields = [k for k in message_field.Detail.__dict__.keys()
if k != 'ALL' and not k.startswith('__')
and k != 'EXCEPTION_DETAIL_MAPPINGS']
for d in defined_fields:
self.assertIn(getattr(message_field.Detail, d),
message_field.Detail.ALL)
self.assertEqual(len(message_field.Detail.ALL),
len(defined_fields))
known_exceptions = [
name for name, _ in
inspect.getmembers(exception, inspect.isclass)]

148
cinder/tests/unit/volume/test_volume_manager.py

@ -0,0 +1,148 @@
# Copyright 2019, Red Hat Inc.
# All Rights Reserved.
#
# 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.
"""Tests for Volume Manager Code."""
import mock
from cinder import exception
from cinder.message import message_field
from cinder.tests.unit import volume as base
from cinder.volume import manager as vol_manager
class VolumeManagerTestCase(base.BaseVolumeTestCase):
@mock.patch('cinder.message.api.API.create')
@mock.patch('cinder.utils.require_driver_initialized')
@mock.patch('cinder.volume.manager.VolumeManager.'
'_notify_about_snapshot_usage')
def test_create_snapshot_driver_not_initialized_generates_user_message(
self, fake_notify, fake_init, fake_msg_create):
manager = vol_manager.VolumeManager()
fake_init.side_effect = exception.CinderException()
fake_snapshot = mock.MagicMock(id='22')
fake_context = mock.MagicMock()
fake_context.elevated.return_value = fake_context
ex = self.assertRaises(exception.CinderException,
manager.create_snapshot,
fake_context,
fake_snapshot)
# make sure a user message was generated
fake_msg_create.assert_called_once_with(
fake_context,
action=message_field.Action.SNAPSHOT_CREATE,
resource_type=message_field.Resource.VOLUME_SNAPSHOT,
resource_uuid=fake_snapshot['id'],
exception=ex,
detail=message_field.Detail.SNAPSHOT_CREATE_ERROR)
@mock.patch('cinder.message.api.API.create')
@mock.patch('cinder.utils.require_driver_initialized')
@mock.patch('cinder.volume.manager.VolumeManager.'
'_notify_about_snapshot_usage')
def test_create_snapshot_metadata_update_failure_generates_user_message(
self, fake_notify, fake_init, fake_msg_create):
manager = vol_manager.VolumeManager()
fake_driver = mock.MagicMock()
fake_driver.create_snapshot.return_value = False
manager.driver = fake_driver
fake_vol_ref = mock.MagicMock()
fake_vol_ref.bootable.return_value = True
fake_db = mock.MagicMock()
fake_db.volume_get.return_value = fake_vol_ref
fake_exp = exception.CinderException()
fake_db.volume_glance_metadata_copy_to_snapshot.side_effect = fake_exp
manager.db = fake_db
fake_snapshot = mock.MagicMock(id='86')
fake_context = mock.MagicMock()
fake_context.elevated.return_value = fake_context
self.assertRaises(exception.CinderException,
manager.create_snapshot,
fake_context,
fake_snapshot)
# make sure a user message was generated
fake_msg_create.assert_called_once_with(
fake_context,
action=message_field.Action.SNAPSHOT_CREATE,
resource_type=message_field.Resource.VOLUME_SNAPSHOT,
resource_uuid=fake_snapshot['id'],
exception=fake_exp,
detail=message_field.Detail.SNAPSHOT_UPDATE_METADATA_FAILED)
@mock.patch('cinder.message.api.API.create')
@mock.patch('cinder.utils.require_driver_initialized')
@mock.patch('cinder.volume.manager.VolumeManager.'
'_notify_about_snapshot_usage')
def test_delete_snapshot_when_busy_generates_user_message(
self, fake_notify, fake_init, fake_msg_create):
manager = vol_manager.VolumeManager()
fake_snapshot = mock.MagicMock(id='0', project_id='1')
fake_context = mock.MagicMock()
fake_context.elevated.return_value = fake_context
fake_exp = exception.SnapshotIsBusy(snapshot_name='Fred')
fake_init.side_effect = fake_exp
manager.delete_snapshot(fake_context, fake_snapshot)
# make sure a user message was generated
fake_msg_create.assert_called_once_with(
fake_context,
action=message_field.Action.SNAPSHOT_DELETE,
resource_type=message_field.Resource.VOLUME_SNAPSHOT,
resource_uuid=fake_snapshot['id'],
exception=fake_exp)
@mock.patch('cinder.message.api.API.create')
@mock.patch('cinder.utils.require_driver_initialized')
@mock.patch('cinder.volume.manager.VolumeManager.'
'_notify_about_snapshot_usage')
def test_delete_snapshot_general_exception_generates_user_message(
self, fake_notify, fake_init, fake_msg_create):
manager = vol_manager.VolumeManager()
fake_snapshot = mock.MagicMock(id='0', project_id='1')
fake_context = mock.MagicMock()
fake_context.elevated.return_value = fake_context
class LocalException(Exception):
pass
fake_exp = LocalException()
# yeah, this isn't where it would be coming from in real life,
# but it saves mocking out a bunch more stuff
fake_init.side_effect = fake_exp
self.assertRaises(LocalException,
manager.delete_snapshot,
fake_context,
fake_snapshot)
# make sure a user message was generated
fake_msg_create.assert_called_once_with(
fake_context,
action=message_field.Action.SNAPSHOT_DELETE,
resource_type=message_field.Resource.VOLUME_SNAPSHOT,
resource_uuid=fake_snapshot['id'],
exception=fake_exp,
detail=message_field.Detail.SNAPSHOT_DELETE_ERROR)

34
cinder/volume/manager.py

@ -1149,10 +1149,17 @@ class VolumeManager(manager.CleanableManager,
snapshot.update(model_update)
snapshot.save()
except Exception:
except Exception as create_error:
with excutils.save_and_reraise_exception():
snapshot.status = fields.SnapshotStatus.ERROR
snapshot.save()
self.message_api.create(
context,
action=message_field.Action.SNAPSHOT_CREATE,
resource_type=message_field.Resource.VOLUME_SNAPSHOT,
resource_uuid=snapshot['id'],
exception=create_error,
detail=message_field.Detail.SNAPSHOT_CREATE_ERROR)
vol_ref = self.db.volume_get(context, snapshot.volume_id)
if vol_ref.bootable:
@ -1172,6 +1179,14 @@ class VolumeManager(manager.CleanableManager,
resource=snapshot)
snapshot.status = fields.SnapshotStatus.ERROR
snapshot.save()
self.message_api.create(
context,
action=message_field.Action.SNAPSHOT_CREATE,
resource_type=message_field.Resource.VOLUME_SNAPSHOT,
resource_uuid=snapshot['id'],
exception=ex,
detail=message_field.Detail.SNAPSHOT_UPDATE_METADATA_FAILED
)
raise exception.MetadataCopyFailure(reason=six.text_type(ex))
snapshot.status = fields.SnapshotStatus.AVAILABLE
@ -1213,16 +1228,29 @@ class VolumeManager(manager.CleanableManager,
self.driver.unmanage_snapshot(snapshot)
else:
self.driver.delete_snapshot(snapshot)
except exception.SnapshotIsBusy:
except exception.SnapshotIsBusy as busy_error:
LOG.error("Delete snapshot failed, due to snapshot busy.",
resource=snapshot)
snapshot.status = fields.SnapshotStatus.AVAILABLE
snapshot.save()
self.message_api.create(
context,
action=message_field.Action.SNAPSHOT_DELETE,
resource_type=message_field.Resource.VOLUME_SNAPSHOT,
resource_uuid=snapshot['id'],
exception=busy_error)
return
except Exception:
except Exception as delete_error:
with excutils.save_and_reraise_exception():
snapshot.status = fields.SnapshotStatus.ERROR_DELETING
snapshot.save()
self.message_api.create(
context,
action=message_field.Action.SNAPSHOT_DELETE,
resource_type=message_field.Resource.VOLUME_SNAPSHOT,
resource_uuid=snapshot['id'],
exception=delete_error,
detail=message_field.Detail.SNAPSHOT_DELETE_ERROR)
# Get reservations
reservations = None

Loading…
Cancel
Save