diff --git a/cinder/message/message_field.py b/cinder/message/message_field.py index 875059caf01..aa0aceab709 100644 --- a/cinder/message/message_field.py +++ b/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'], } diff --git a/cinder/tests/unit/message/test_message_field.py b/cinder/tests/unit/message/test_message_field.py index 79fdd050ee9..4611bb0b437 100644 --- a/cinder/tests/unit/message/test_message_field.py +++ b/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)] diff --git a/cinder/tests/unit/volume/test_volume_manager.py b/cinder/tests/unit/volume/test_volume_manager.py new file mode 100644 index 00000000000..c65707b4007 --- /dev/null +++ b/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) diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index aa2db6325ac..c10e9de4e4a 100644 --- a/cinder/volume/manager.py +++ b/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