From 352ce99e87d9c85d33dbc23331df610e0fda3b26 Mon Sep 17 00:00:00 2001 From: Nikita Skakun Date: Thu, 16 Jul 2026 12:19:29 +0200 Subject: [PATCH] Fix: Preserve migration state on force-extend/shrink Before this change, running a force-extend or force-shrink on a share that was in the middle of a migration (either share migration or share-server migration) would overwrite the share's status with available or extending_error / shrinking_error. That looked fine on its own, but migration_driver_continue only picks up instances whose status is still "migrating" - so as soon as the status was overwritten, the migration got silently skipped forever and its task_state was left dangling. Storage team has been resetting the status back to migrating by hand to work around this. The manager now figures out whether a migration is in progress by looking at both the share's task_state and the share_server's status/task_state, so it can tell share-migration from server-migration. It remembers the correct original status and puts it back after the driver call finishes, in both the success and the error path - so force-extend/shrink no longer disrupt an ongoing migration. Extend and shrink can take a long time. If the migration happens to finish while the driver call is running, restoring "migrating" at the end would just create a new orphan (status=migrating with a task_state that has already moved on). So right before writing the final status we re-check the DB: if the migration is no longer active, we fall back to the normal available / *_error status instead. Change-Id: Ifaa79e3172ad97851fc84727da104f4c410786f8 Signed-off-by: Nikita Skakun --- manila/share/manager.py | 118 +++++++- manila/tests/share/test_manager.py | 440 ++++++++++++++++++++++++++++- 2 files changed, 549 insertions(+), 9 deletions(-) diff --git a/manila/share/manager.py b/manila/share/manager.py index 6883270bf4..5846a136da 100644 --- a/manila/share/manager.py +++ b/manila/share/manager.py @@ -1760,11 +1760,26 @@ def migration_driver_continue(self, context): for instance in instances: + share = self.db.share_get(context, instance['share_id']) + if instance['status'] != constants.STATUS_MIGRATING: + if share['task_state'] == ( + constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS): + LOG.warning( + "Share instance %(instance_id)s (share %(share_id)s) " + "is skipped by migration_driver_continue because its " + "status is %(status)s (expected %(expected)s), while " + "task_state is still %(task_state)s. The migration " + "task_state may be orphaned - this typically happens " + "when extend_share/shrink_share overwrote the " + "migrating status.", + {'instance_id': instance['id'], + 'share_id': instance['share_id'], + 'status': instance['status'], + 'expected': constants.STATUS_MIGRATING, + 'task_state': share['task_state']}) continue - share = self.db.share_get(context, instance['share_id']) - if share['task_state'] == ( constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS): @@ -5342,6 +5357,42 @@ def _delete_encryption_keys_quota(self, context): context, reservations, project_id=context.project_id, ) + def _detect_migration_status(self, share, share_server): + """Detect whether the share is being migrated.""" + + share_task_state = share.get('task_state') + was_share_migrating = share_task_state in constants.BUSY_TASK_STATES + + was_server_migrating = False + if share_server is not None and hasattr(share_server, 'get'): + server_status = share_server.get('status') + server_task_state = share_server.get('task_state') + was_server_migrating = ( + server_status == constants.STATUS_SERVER_MIGRATING + or server_task_state in constants.BUSY_TASK_STATES) + + if was_server_migrating: + return True, constants.STATUS_SERVER_MIGRATING + if was_share_migrating: + return True, constants.STATUS_MIGRATING + return False, share['status'] + + def _migration_still_running(self, context, share, share_server): + """Re-check migration state right before a final status write.""" + + fresh_share = self.db.share_get(context, share['id']) + fresh_server = None + if share_server: + try: + fresh_server = self.db.share_server_get( + context, share_server['id']) + except exception.ShareServerNotFound: + fresh_server = None + _, current_original = self._detect_migration_status( + fresh_share, fresh_server) + return current_original in (constants.STATUS_MIGRATING, + constants.STATUS_SERVER_MIGRATING) + @run_concurrently @add_hooks @utils.require_driver_initialized @@ -5353,6 +5404,9 @@ def extend_share(self, context, share_id, new_size, reservations): project_id = share['project_id'] user_id = share['user_id'] + was_migrating, original_status = self._detect_migration_status( + share, share_server) + self._notify_about_share_usage(context, share, share_instance, "extend.start") @@ -5369,9 +5423,15 @@ def extend_share(self, context, share_id, new_size, reservations): resource_id=share_id, detail=message_field.Detail.DRIVER_FAILED_EXTEND) try: + still_migrating = ( + was_migrating + and self._migration_still_running( + context, share, share_server)) + error_status = (original_status if still_migrating + else constants.STATUS_EXTENDING_ERROR) self.db.share_update( context, share['id'], - {'status': constants.STATUS_EXTENDING_ERROR} + {'status': error_status} ) raise exception.ShareExtendingError( reason=str(e), share_id=share_id) @@ -5390,11 +5450,16 @@ def extend_share(self, context, share_id, new_size, reservations): user_id=user_id, share_type_id=share_instance['share_type_id'], ) + still_migrating = ( + was_migrating + and self._migration_still_running(context, share, share_server)) + success_status = (original_status if still_migrating + else constants.STATUS_AVAILABLE.lower()) share_update = { 'size': int(new_size), # NOTE(u_glide): translation to lower case should be removed in # a row with usage of upper case of share statuses in all places - 'status': constants.STATUS_AVAILABLE.lower() + 'status': success_status } share = self.db.share_update(context, share['id'], share_update) @@ -5417,6 +5482,9 @@ def shrink_share(self, context, share_id, new_size): context, share['id']) supports_replication = len(replicas) > 0 + was_migrating, original_status = self._detect_migration_status( + share, share_server) + self._notify_about_share_usage(context, share, share_instance, "shrink.start") @@ -5432,7 +5500,13 @@ def error_occurred(exc, msg, status=constants.STATUS_SHRINKING_ERROR): resource_id=share['id'], detail=message_field.Detail.DRIVER_FAILED_SHRINK) LOG.warning(msg, resource=share) - self.db.share_update(context, share['id'], {'status': status}) + still_migrating = ( + was_migrating + and self._migration_still_running( + context, share, share_server)) + final_status = original_status if still_migrating else status + self.db.share_update( + context, share['id'], {'status': final_status}) raise exception.ShareShrinkingError( reason=str(exc), share_id=share_id) @@ -5497,9 +5571,14 @@ def error_occurred(exc, msg, status=constants.STATUS_SHRINKING_ERROR): user_id=user_id, share_type_id=share_instance['share_type_id'], ) + still_migrating = ( + was_migrating + and self._migration_still_running(context, share, share_server)) + success_status = (original_status if still_migrating + else constants.STATUS_AVAILABLE) share_update = { 'size': new_size, - 'status': constants.STATUS_AVAILABLE + 'status': success_status } share = self.db.share_update(context, share['id'], share_update) @@ -6352,8 +6431,31 @@ def _update_resource_status(self, context, status, task_state=None, if task_state: fields['task_state'] = task_state if share_instance_ids: - self.db.share_instance_status_update( - context, share_instance_ids, fields) + + preserved_statuses = ( + constants.STATUS_EXTENDING_ERROR, + constants.STATUS_SHRINKING_ERROR, + ) + ids_to_update = [] + for instance_id in share_instance_ids: + instance = self.db.share_instance_get(context, instance_id) + if instance['status'] in preserved_statuses: + LOG.warning( + "Not overwriting share instance %(id)s status " + "%(status)s during resource status update to " + "%(new_status)s - the error state is preserved for " + "operator visibility.", + {'id': instance_id, + 'status': instance['status'], + 'new_status': status}) + if task_state: + self.db.share_instance_update( + context, instance_id, {'task_state': task_state}) + else: + ids_to_update.append(instance_id) + if ids_to_update: + self.db.share_instance_status_update( + context, ids_to_update, fields) if snapshot_instance_ids: self.db.share_snapshot_instances_status_update( context, snapshot_instance_ids, fields) diff --git a/manila/tests/share/test_manager.py b/manila/tests/share/test_manager.py index 021e25fba7..bc7c229645 100644 --- a/manila/tests/share/test_manager.py +++ b/manila/tests/share/test_manager.py @@ -5381,6 +5381,349 @@ def test_extend_share(self, mock_notify): (['INFO', 'share.extend.start'], ['INFO', 'share.extend.end'])) + @ddt.data( + (constants.STATUS_MIGRATING, + constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + None, constants.STATUS_ACTIVE, + constants.STATUS_MIGRATING), + (constants.STATUS_SERVER_MIGRATING, + None, + constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + constants.STATUS_SERVER_MIGRATING, + constants.STATUS_SERVER_MIGRATING), + ) + @ddt.unpack + def test_extend_share_preserves_migrating_on_success( + self, migrating_status, share_task_state, server_task_state, + server_status, expected_status): + share_type = db_utils.create_share_type() + share = db_utils.create_share( + share_type_id=share_type['id'], + status=constants.STATUS_EXTENDING, + task_state=share_task_state) + share_id = share['id'] + new_size = 123 + reservations = {} + share_server = { + 'id': 'fake_ss_id', + 'status': server_status, + 'task_state': server_task_state, + } + + manager = self.share_manager + self.mock_object(manager, 'driver') + self.mock_object(manager.db, 'share_get', + mock.Mock(return_value=share)) + self.mock_object(manager.db, 'share_server_get', + mock.Mock(return_value=share_server)) + self.mock_object(manager.db, 'share_update', + mock.Mock(return_value=share)) + self.mock_object(quota.QUOTAS, 'commit') + self.mock_object(manager.driver, 'extend_share') + self.mock_object(manager, '_get_share_server', + mock.Mock(return_value=share_server)) + + manager.extend_share(self.context, share_id, new_size, reservations) + + manager.db.share_update.assert_called_once_with( + mock.ANY, share_id, + {'size': int(new_size), 'status': expected_status}) + + @ddt.data( + (constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + None, constants.STATUS_ACTIVE, + constants.STATUS_MIGRATING), + (None, + constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + constants.STATUS_SERVER_MIGRATING, + constants.STATUS_SERVER_MIGRATING), + ) + @ddt.unpack + def test_extend_share_preserves_migrating_on_error( + self, share_task_state, server_task_state, server_status, + expected_status): + share_type = db_utils.create_share_type() + share = db_utils.create_share( + share_type_id=share_type['id'], + status=constants.STATUS_EXTENDING, + task_state=share_task_state) + share_id = share['id'] + share_server = { + 'id': 'fake_ss_id', + 'status': server_status, + 'task_state': server_task_state, + } + + manager = self.share_manager + self.mock_object(manager, 'driver') + self.mock_object(manager.db, 'share_get', + mock.Mock(return_value=share)) + self.mock_object(manager.db, 'share_server_get', + mock.Mock(return_value=share_server)) + mock_update = self.mock_object(manager.db, 'share_update') + self.mock_object(quota.QUOTAS, 'rollback') + self.mock_object(manager.driver, 'extend_share', + mock.Mock(side_effect=Exception('fake'))) + self.mock_object(manager, '_get_share_server', + mock.Mock(return_value=share_server)) + + self.assertRaises( + exception.ShareExtendingError, + manager.extend_share, self.context, share_id, 123, {}) + + mock_update.assert_called_once_with( + mock.ANY, share_id, {'status': expected_status}) + + def test_extend_share_migration_finished_midflight_success(self): + share_type = db_utils.create_share_type() + initial_share = db_utils.create_share( + share_type_id=share_type['id'], + status=constants.STATUS_EXTENDING, + task_state=constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS) + share_id = initial_share['id'] + finished_share = dict(initial_share) + finished_share['task_state'] = constants.TASK_STATE_MIGRATION_SUCCESS + share_server = { + 'id': 'fake_ss_id', + 'status': constants.STATUS_ACTIVE, + 'task_state': None, + } + + manager = self.share_manager + self.mock_object(manager, 'driver') + self.mock_object(manager.db, 'share_get', + mock.Mock(side_effect=[initial_share, + finished_share])) + self.mock_object(manager.db, 'share_server_get', + mock.Mock(return_value=share_server)) + mock_update = self.mock_object(manager.db, 'share_update', + mock.Mock(return_value=finished_share)) + self.mock_object(quota.QUOTAS, 'commit') + self.mock_object(manager.driver, 'extend_share') + self.mock_object(manager, '_get_share_server', + mock.Mock(return_value=share_server)) + + manager.extend_share(self.context, share_id, 123, {}) + + mock_update.assert_called_once_with( + mock.ANY, share_id, + {'size': 123, 'status': constants.STATUS_AVAILABLE.lower()}) + + def test_extend_share_migration_finished_midflight_error(self): + share_type = db_utils.create_share_type() + initial_share = db_utils.create_share( + share_type_id=share_type['id'], + status=constants.STATUS_EXTENDING, + task_state=constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS) + share_id = initial_share['id'] + finished_share = dict(initial_share) + finished_share['task_state'] = constants.TASK_STATE_MIGRATION_SUCCESS + share_server = { + 'id': 'fake_ss_id', + 'status': constants.STATUS_ACTIVE, + 'task_state': None, + } + + manager = self.share_manager + self.mock_object(manager, 'driver') + self.mock_object(manager.db, 'share_get', + mock.Mock(side_effect=[initial_share, + finished_share])) + self.mock_object(manager.db, 'share_server_get', + mock.Mock(return_value=share_server)) + mock_update = self.mock_object(manager.db, 'share_update') + self.mock_object(quota.QUOTAS, 'rollback') + self.mock_object(manager.driver, 'extend_share', + mock.Mock(side_effect=Exception('fake'))) + self.mock_object(manager, '_get_share_server', + mock.Mock(return_value=share_server)) + + self.assertRaises( + exception.ShareExtendingError, + manager.extend_share, self.context, share_id, 123, {}) + + mock_update.assert_called_once_with( + mock.ANY, share_id, + {'status': constants.STATUS_EXTENDING_ERROR}) + + @ddt.data( + (constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + None, constants.STATUS_ACTIVE, + constants.STATUS_MIGRATING), + (None, + constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + constants.STATUS_SERVER_MIGRATING, + constants.STATUS_SERVER_MIGRATING), + ) + @ddt.unpack + def test_shrink_share_preserves_migrating_on_success( + self, share_task_state, server_task_state, server_status, + expected_status): + share_type = db_utils.create_share_type() + share = db_utils.create_share( + size=5, + share_type_id=share_type['id'], + status=constants.STATUS_SHRINKING, + task_state=share_task_state) + share_id = share['id'] + new_size = 3 + share_server = { + 'id': 'fake_ss_id', + 'status': server_status, + 'task_state': server_task_state, + } + + manager = self.share_manager + self.mock_object(manager, 'driver') + self.mock_object(manager.db, 'share_get', + mock.Mock(return_value=share)) + self.mock_object(manager.db, 'share_server_get', + mock.Mock(return_value=share_server)) + mock_update = self.mock_object(manager.db, 'share_update', + mock.Mock(return_value=share)) + self.mock_object(manager.db, 'share_replicas_get_all_by_share', + mock.Mock(return_value=[])) + self.mock_object(quota.QUOTAS, 'reserve') + self.mock_object(quota.QUOTAS, 'commit') + self.mock_object(manager.driver, 'shrink_share') + self.mock_object(manager, '_get_share_server', + mock.Mock(return_value=share_server)) + + manager.shrink_share(self.context, share_id, new_size) + + mock_update.assert_called_once_with( + mock.ANY, share_id, + {'size': new_size, 'status': expected_status}) + + @ddt.data( + (constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + None, constants.STATUS_ACTIVE, + constants.STATUS_MIGRATING), + (None, + constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + constants.STATUS_SERVER_MIGRATING, + constants.STATUS_SERVER_MIGRATING), + ) + @ddt.unpack + def test_shrink_share_preserves_migrating_on_error( + self, share_task_state, server_task_state, server_status, + expected_status): + share_type = db_utils.create_share_type() + share = db_utils.create_share( + size=5, + share_type_id=share_type['id'], + status=constants.STATUS_SHRINKING, + task_state=share_task_state) + share_id = share['id'] + share_server = { + 'id': 'fake_ss_id', + 'status': server_status, + 'task_state': server_task_state, + } + + manager = self.share_manager + self.mock_object(manager, 'driver') + self.mock_object(manager.db, 'share_get', + mock.Mock(return_value=share)) + self.mock_object(manager.db, 'share_server_get', + mock.Mock(return_value=share_server)) + mock_update = self.mock_object(manager.db, 'share_update') + self.mock_object(manager.db, 'share_replicas_get_all_by_share', + mock.Mock(return_value=[])) + self.mock_object(quota.QUOTAS, 'reserve') + self.mock_object(quota.QUOTAS, 'rollback') + self.mock_object(manager.driver, 'shrink_share', + mock.Mock(side_effect=Exception('fake'))) + self.mock_object(manager, '_get_share_server', + mock.Mock(return_value=share_server)) + + self.assertRaises( + exception.ShareShrinkingError, + manager.shrink_share, self.context, share_id, 3) + + mock_update.assert_called_once_with( + mock.ANY, share_id, {'status': expected_status}) + + def test_shrink_share_migration_finished_midflight_success(self): + share_type = db_utils.create_share_type() + initial_share = db_utils.create_share( + size=5, + share_type_id=share_type['id'], + status=constants.STATUS_SHRINKING, + task_state=constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS) + share_id = initial_share['id'] + finished_share = dict(initial_share) + finished_share['task_state'] = constants.TASK_STATE_MIGRATION_SUCCESS + share_server = { + 'id': 'fake_ss_id', + 'status': constants.STATUS_ACTIVE, + 'task_state': None, + } + + manager = self.share_manager + self.mock_object(manager, 'driver') + self.mock_object(manager.db, 'share_get', + mock.Mock(side_effect=[initial_share, + finished_share])) + self.mock_object(manager.db, 'share_server_get', + mock.Mock(return_value=share_server)) + mock_update = self.mock_object(manager.db, 'share_update', + mock.Mock(return_value=finished_share)) + self.mock_object(manager.db, 'share_replicas_get_all_by_share', + mock.Mock(return_value=[])) + self.mock_object(quota.QUOTAS, 'reserve') + self.mock_object(quota.QUOTAS, 'commit') + self.mock_object(manager.driver, 'shrink_share') + self.mock_object(manager, '_get_share_server', + mock.Mock(return_value=share_server)) + + manager.shrink_share(self.context, share_id, 3) + + mock_update.assert_called_once_with( + mock.ANY, share_id, + {'size': 3, 'status': constants.STATUS_AVAILABLE}) + + def test_shrink_share_migration_finished_midflight_error(self): + share_type = db_utils.create_share_type() + initial_share = db_utils.create_share( + size=5, + share_type_id=share_type['id'], + status=constants.STATUS_SHRINKING, + task_state=constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS) + share_id = initial_share['id'] + finished_share = dict(initial_share) + finished_share['task_state'] = constants.TASK_STATE_MIGRATION_SUCCESS + share_server = { + 'id': 'fake_ss_id', + 'status': constants.STATUS_ACTIVE, + 'task_state': None, + } + + manager = self.share_manager + self.mock_object(manager, 'driver') + self.mock_object(manager.db, 'share_get', + mock.Mock(side_effect=[initial_share, + finished_share])) + self.mock_object(manager.db, 'share_server_get', + mock.Mock(return_value=share_server)) + mock_update = self.mock_object(manager.db, 'share_update') + self.mock_object(manager.db, 'share_replicas_get_all_by_share', + mock.Mock(return_value=[])) + self.mock_object(quota.QUOTAS, 'reserve') + self.mock_object(quota.QUOTAS, 'rollback') + self.mock_object(manager.driver, 'shrink_share', + mock.Mock(side_effect=Exception('fake'))) + self.mock_object(manager, '_get_share_server', + mock.Mock(return_value=share_server)) + + self.assertRaises( + exception.ShareShrinkingError, + manager.shrink_share, self.context, share_id, 3) + + mock_update.assert_called_once_with( + mock.ANY, share_id, + {'status': constants.STATUS_SHRINKING_ERROR}) + def test_shrink_share_not_supported(self): share_type = db_utils.create_share_type() share = db_utils.create_share(size=2, share_type_id=share_type['id']) @@ -6949,6 +7292,11 @@ def test_migration_driver_continue(self, finished): regular_instance = db_utils.create_share_instance( status=constants.STATUS_AVAILABLE, share_id='other_id') + + other_share = db_utils.create_share( + task_state=None, + id='other_id', + status=constants.STATUS_AVAILABLE) dest_instance = db_utils.create_share_instance( share_id='share_id', host='fake_host', @@ -6970,7 +7318,8 @@ def test_migration_driver_continue(self, finished): 'share_instance_get_all_by_host', mock.Mock( return_value=[regular_instance, src_instance])) self.mock_object(self.share_manager.db, 'share_get', - mock.Mock(side_effect=[share, share_cancelled])) + mock.Mock(side_effect=[other_share, share, + share_cancelled])) self.mock_object(api.API, 'get_migrating_instances', mock.Mock(return_value=( src_instance['id'], dest_instance['id']))) @@ -7055,6 +7404,34 @@ def test_migration_driver_continue(self, finished): (self.share_manager.db.share_snapshot_instance_get_all_with_filters. assert_has_calls(snapshot_instance_get_all_calls)) + def test_migration_driver_continue_logs_orphan_task_state(self): + orphan_instance = db_utils.create_share_instance( + status=constants.STATUS_EXTENDING_ERROR, + share_id='orphan_share_id') + orphan_share = db_utils.create_share( + id='orphan_share_id', + task_state=constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + status=constants.STATUS_EXTENDING_ERROR) + + self.mock_object(manager.LOG, 'warning') + self.mock_object( + self.share_manager.db, 'share_instance_get_all_by_host', + mock.Mock(return_value=[orphan_instance])) + self.mock_object(self.share_manager.db, 'share_get', + mock.Mock(return_value=orphan_share)) + + self.share_manager.migration_driver_continue(self.context) + + self.assertTrue(manager.LOG.warning.called) + warning_call = manager.LOG.warning.call_args + self.assertIn('skipped by migration_driver_continue', + warning_call[0][0]) + self.assertEqual(orphan_instance['id'], + warning_call[0][1]['instance_id']) + self.assertEqual( + constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + warning_call[0][1]['task_state']) + @ddt.data({'task_state': constants.TASK_STATE_MIGRATION_DRIVER_PHASE1_DONE, 'exc': None}, {'task_state': constants.TASK_STATE_MIGRATION_DRIVER_PHASE1_DONE, @@ -9568,6 +9945,11 @@ def test__update_resource_status(self, kwargs, resource_type): if resource_type == 'share_instance': mock_db_instances_status_update = self.mock_object( db, 'share_instance_status_update') + self.mock_object( + db, 'share_instance_get', + mock.Mock(return_value={ + 'id': 'fakeid1', + 'status': constants.STATUS_MIGRATING_TO})) else: mock_db_instances_status_update = self.mock_object( db, 'share_snapshot_instances_status_update') @@ -9588,6 +9970,62 @@ def test__update_resource_status(self, kwargs, resource_type): mock_db_instances_status_update.assert_called_once_with( self.context, resource_ids, fields) + def test__update_resource_status_preserves_extending_error(self): + mock_status_update = self.mock_object( + db, 'share_instance_status_update') + mock_instance_update = self.mock_object(db, 'share_instance_update') + self.mock_object( + db, 'share_instance_get', + mock.Mock(return_value={ + 'id': 'errored-id', + 'status': constants.STATUS_EXTENDING_ERROR})) + + self.share_manager._update_resource_status( + self.context, constants.STATUS_AVAILABLE, + task_state=constants.TASK_STATE_MIGRATION_SUCCESS, + share_instance_ids=['errored-id']) + + mock_status_update.assert_not_called() + mock_instance_update.assert_called_once_with( + self.context, 'errored-id', + {'task_state': constants.TASK_STATE_MIGRATION_SUCCESS}) + + def test__update_resource_status_preserves_shrinking_error(self): + mock_status_update = self.mock_object( + db, 'share_instance_status_update') + self.mock_object(db, 'share_instance_update') + self.mock_object( + db, 'share_instance_get', + mock.Mock(return_value={ + 'id': 'errored-id', + 'status': constants.STATUS_SHRINKING_ERROR})) + + self.share_manager._update_resource_status( + self.context, constants.STATUS_AVAILABLE, + share_instance_ids=['errored-id']) + + mock_status_update.assert_not_called() + + def test__update_resource_status_mixed_preserved_and_normal(self): + mock_status_update = self.mock_object( + db, 'share_instance_status_update') + self.mock_object(db, 'share_instance_update') + self.mock_object( + db, 'share_instance_get', + mock.Mock(side_effect=[ + {'id': 'ok-id', 'status': constants.STATUS_MIGRATING_TO}, + {'id': 'err-id', + 'status': constants.STATUS_EXTENDING_ERROR}, + ])) + + self.share_manager._update_resource_status( + self.context, constants.STATUS_AVAILABLE, + share_instance_ids=['ok-id', 'err-id']) + + mock_status_update.assert_called_once_with( + self.context, ['ok-id'], + {'status': constants.STATUS_AVAILABLE}) + def _get_share_server_start_update_calls( self, source_share_server, dest_share_server, driver_failed=False): migration_in_progress_call = mock.call(