1# Copyright 2010 United States Government as represented by the 2# Administrator of the National Aeronautics and Space Administration. 3# All Rights Reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may 6# not use this file except in compliance with the License. You may obtain 7# a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations 15# under the License. 16""" 17Tests For Scheduler 18""" 19 20import collections 21from datetime import datetime 22import ddt 23import mock 24from oslo_config import cfg 25 26from cinder.common import constants 27from cinder import context 28from cinder import exception 29from cinder.message import message_field 30from cinder import objects 31from cinder.scheduler import driver 32from cinder.scheduler import manager 33from cinder import test 34from cinder.tests.unit import fake_constants as fake 35from cinder.tests.unit import fake_volume 36from cinder.tests.unit.scheduler import fakes as fake_scheduler 37from cinder.tests.unit import utils as tests_utils 38 39CONF = cfg.CONF 40 41 42@ddt.ddt 43class SchedulerManagerTestCase(test.TestCase): 44 """Test case for scheduler manager.""" 45 46 manager_cls = manager.SchedulerManager 47 driver_cls = driver.Scheduler 48 driver_cls_name = 'cinder.scheduler.driver.Scheduler' 49 50 class AnException(Exception): 51 pass 52 53 def setUp(self): 54 super(SchedulerManagerTestCase, self).setUp() 55 self.flags(scheduler_driver=self.driver_cls_name) 56 self.manager = self.manager_cls() 57 self.manager._startup_delay = False 58 self.context = context.get_admin_context() 59 self.topic = 'fake_topic' 60 self.fake_args = (1, 2, 3) 61 self.fake_kwargs = {'cat': 'meow', 'dog': 'woof'} 62 63 def test_1_correct_init(self): 64 # Correct scheduler driver 65 manager = self.manager 66 self.assertIsInstance(manager.driver, self.driver_cls) 67 68 @mock.patch('eventlet.sleep') 69 @mock.patch('cinder.volume.rpcapi.VolumeAPI.publish_service_capabilities') 70 def test_init_host_with_rpc(self, publish_capabilities_mock, sleep_mock): 71 self.manager._startup_delay = True 72 self.manager.init_host_with_rpc() 73 publish_capabilities_mock.assert_called_once_with(mock.ANY) 74 sleep_mock.assert_called_once_with(CONF.periodic_interval) 75 self.assertFalse(self.manager._startup_delay) 76 77 @mock.patch('cinder.objects.service.Service.get_minimum_rpc_version') 78 @mock.patch('cinder.objects.service.Service.get_minimum_obj_version') 79 @mock.patch('cinder.rpc.LAST_RPC_VERSIONS', {'cinder-volume': '1.3'}) 80 @mock.patch('cinder.rpc.LAST_OBJ_VERSIONS', {'cinder-volume': '1.4', 81 'cinder-scheduler': '1.4'}) 82 def test_reset(self, get_min_obj, get_min_rpc): 83 mgr = self.manager_cls() 84 85 volume_rpcapi = mgr.driver.volume_rpcapi 86 self.assertEqual('1.3', volume_rpcapi.client.version_cap) 87 self.assertEqual('1.4', 88 volume_rpcapi.client.serializer._base.version_cap) 89 get_min_obj.return_value = objects.base.OBJ_VERSIONS.get_current() 90 mgr.reset() 91 92 volume_rpcapi = mgr.driver.volume_rpcapi 93 self.assertEqual(get_min_rpc.return_value, 94 volume_rpcapi.client.version_cap) 95 self.assertEqual(get_min_obj.return_value, 96 volume_rpcapi.client.serializer._base.version_cap) 97 self.assertIsNone(volume_rpcapi.client.serializer._base.manifest) 98 99 @mock.patch('cinder.message.api.API.cleanup_expired_messages') 100 def test_clean_expired_messages(self, mock_clean): 101 102 self.manager._clean_expired_messages(self.context) 103 104 mock_clean.assert_called_once_with(self.context) 105 106 @mock.patch('cinder.scheduler.driver.Scheduler.backend_passes_filters') 107 @mock.patch( 108 'cinder.scheduler.host_manager.BackendState.consume_from_volume') 109 @mock.patch('cinder.volume.rpcapi.VolumeAPI.extend_volume') 110 def test_extend_volume(self, mock_extend, 111 mock_consume, mock_backend_passes): 112 volume = fake_volume.fake_volume_obj(self.context, **{'size': 1}) 113 fake_backend = fake_scheduler.FakeBackendState('host1', {}) 114 mock_backend_passes.return_value = fake_backend 115 116 self.manager.extend_volume(self.context, volume, 2, 'fake_reservation') 117 118 mock_consume.assert_called_once_with({'size': 1}) 119 mock_extend.assert_called_once_with( 120 self.context, volume, 2, 'fake_reservation') 121 122 @ddt.data('available', 'in-use') 123 @mock.patch('cinder.scheduler.driver.Scheduler.backend_passes_filters') 124 @mock.patch( 125 'cinder.scheduler.host_manager.BackendState.consume_from_volume') 126 @mock.patch('cinder.volume.rpcapi.VolumeAPI.extend_volume') 127 @mock.patch('cinder.quota.QUOTAS.rollback') 128 @mock.patch('cinder.message.api.API.create') 129 def test_extend_volume_no_valid_host(self, status, mock_create, 130 mock_rollback, 131 mock_extend, mock_consume, 132 mock_backend_passes): 133 volume = fake_volume.fake_volume_obj(self.context, 134 **{'size': 1, 135 'previous_status': status}) 136 no_valid_backend = exception.NoValidBackend(reason='') 137 mock_backend_passes.side_effect = [no_valid_backend] 138 139 with mock.patch.object(self.manager, 140 '_set_volume_state_and_notify') as mock_notify: 141 self.manager.extend_volume(self.context, volume, 2, 142 'fake_reservation') 143 mock_notify.assert_called_once_with( 144 'extend_volume', {'volume_state': {'status': status, 145 'previous_status': None}}, 146 self.context, no_valid_backend, None) 147 mock_rollback.assert_called_once_with( 148 self.context, 'fake_reservation', project_id=volume.project_id) 149 mock_consume.assert_not_called() 150 mock_extend.assert_not_called() 151 mock_create.assert_called_once_with( 152 self.context, 153 message_field.Action.EXTEND_VOLUME, 154 resource_uuid=volume.id, 155 exception=no_valid_backend) 156 157 @mock.patch('cinder.quota.QuotaEngine.expire') 158 def test_clean_expired_reservation(self, mock_clean): 159 160 self.manager._clean_expired_reservation(self.context) 161 162 mock_clean.assert_called_once_with(self.context) 163 164 @mock.patch('cinder.scheduler.driver.Scheduler.' 165 'update_service_capabilities') 166 def test_update_service_capabilities_empty_dict(self, _mock_update_cap): 167 # Test no capabilities passes empty dictionary 168 service = 'fake_service' 169 host = 'fake_host' 170 171 self.manager.update_service_capabilities(self.context, 172 service_name=service, 173 host=host) 174 _mock_update_cap.assert_called_once_with(service, host, {}, None, None) 175 176 @mock.patch('cinder.scheduler.driver.Scheduler.' 177 'update_service_capabilities') 178 def test_update_service_capabilities_correct(self, _mock_update_cap): 179 # Test capabilities passes correctly 180 service = 'fake_service' 181 host = 'fake_host' 182 capabilities = {'fake_capability': 'fake_value'} 183 184 self.manager.update_service_capabilities(self.context, 185 service_name=service, 186 host=host, 187 capabilities=capabilities) 188 _mock_update_cap.assert_called_once_with(service, host, capabilities, 189 None, None) 190 191 @mock.patch('cinder.scheduler.driver.Scheduler.' 192 'notify_service_capabilities') 193 def test_notify_service_capabilities_no_timestamp(self, _mock_notify_cap): 194 """Test old interface that receives host.""" 195 service = 'volume' 196 host = 'fake_host' 197 capabilities = {'fake_capability': 'fake_value'} 198 199 self.manager.notify_service_capabilities(self.context, 200 service_name=service, 201 host=host, 202 capabilities=capabilities) 203 _mock_notify_cap.assert_called_once_with(service, host, capabilities, 204 None) 205 206 @mock.patch('cinder.scheduler.driver.Scheduler.' 207 'notify_service_capabilities') 208 def test_notify_service_capabilities_timestamp(self, _mock_notify_cap): 209 """Test new interface that receives backend and timestamp.""" 210 service = 'volume' 211 backend = 'fake_cluster' 212 capabilities = {'fake_capability': 'fake_value'} 213 214 timestamp = '1970-01-01T00:00:00.000000' 215 216 self.manager.notify_service_capabilities(self.context, 217 service_name=service, 218 backend=backend, 219 capabilities=capabilities, 220 timestamp=timestamp) 221 _mock_notify_cap.assert_called_once_with(service, backend, 222 capabilities, 223 datetime(1970, 1, 1)) 224 225 @mock.patch('cinder.scheduler.driver.Scheduler.schedule_create_volume') 226 @mock.patch('cinder.message.api.API.create') 227 @mock.patch('cinder.db.volume_update') 228 def test_create_volume_exception_puts_volume_in_error_state( 229 self, _mock_volume_update, _mock_message_create, 230 _mock_sched_create): 231 # Test NoValidBackend exception behavior for create_volume. 232 # Puts the volume in 'error' state and eats the exception. 233 _mock_sched_create.side_effect = exception.NoValidBackend(reason="") 234 volume = fake_volume.fake_volume_obj(self.context) 235 request_spec = {'volume_id': volume.id, 236 'volume': {'id': volume.id, '_name_id': None, 237 'metadata': {}, 'admin_metadata': {}, 238 'glance_metadata': {}}} 239 request_spec_obj = objects.RequestSpec.from_primitives(request_spec) 240 241 self.manager.create_volume(self.context, volume, 242 request_spec=request_spec_obj, 243 filter_properties={}) 244 _mock_volume_update.assert_called_once_with(self.context, 245 volume.id, 246 {'status': 'error'}) 247 _mock_sched_create.assert_called_once_with(self.context, 248 request_spec_obj, {}) 249 250 _mock_message_create.assert_called_once_with( 251 self.context, message_field.Action.SCHEDULE_ALLOCATE_VOLUME, 252 resource_uuid=volume.id, 253 exception=mock.ANY) 254 255 @mock.patch('cinder.scheduler.driver.Scheduler.schedule_create_volume') 256 @mock.patch('eventlet.sleep') 257 def test_create_volume_no_delay(self, _mock_sleep, _mock_sched_create): 258 volume = fake_volume.fake_volume_obj(self.context) 259 260 request_spec = {'volume_id': volume.id} 261 request_spec_obj = objects.RequestSpec.from_primitives(request_spec) 262 263 self.manager.create_volume(self.context, volume, 264 request_spec=request_spec_obj, 265 filter_properties={}) 266 _mock_sched_create.assert_called_once_with(self.context, 267 request_spec_obj, {}) 268 self.assertFalse(_mock_sleep.called) 269 270 @mock.patch('cinder.scheduler.driver.Scheduler.schedule_create_volume') 271 @mock.patch('eventlet.sleep') 272 def test_create_volume_set_worker(self, _mock_sleep, _mock_sched_create): 273 """Make sure that the worker is created when creating a volume.""" 274 volume = tests_utils.create_volume(self.context, status='creating') 275 276 request_spec = {'volume_id': volume.id} 277 278 self.manager.create_volume(self.context, volume, 279 request_spec=request_spec, 280 filter_properties={}) 281 volume.set_worker.assert_called_once_with() 282 283 @mock.patch('cinder.scheduler.driver.Scheduler.schedule_create_volume') 284 @mock.patch('cinder.scheduler.driver.Scheduler.is_ready') 285 @mock.patch('eventlet.sleep') 286 def test_create_volume_delay_scheduled_after_3_tries(self, _mock_sleep, 287 _mock_is_ready, 288 _mock_sched_create): 289 self.manager._startup_delay = True 290 volume = fake_volume.fake_volume_obj(self.context) 291 292 request_spec = {'volume_id': volume.id} 293 request_spec_obj = objects.RequestSpec.from_primitives(request_spec) 294 295 _mock_is_ready.side_effect = [False, False, True] 296 297 self.manager.create_volume(self.context, volume, 298 request_spec=request_spec_obj, 299 filter_properties={}) 300 _mock_sched_create.assert_called_once_with(self.context, 301 request_spec_obj, {}) 302 calls = [mock.call(1)] * 2 303 _mock_sleep.assert_has_calls(calls) 304 self.assertEqual(2, _mock_sleep.call_count) 305 306 @mock.patch('cinder.scheduler.driver.Scheduler.schedule_create_volume') 307 @mock.patch('cinder.scheduler.driver.Scheduler.is_ready') 308 @mock.patch('eventlet.sleep') 309 def test_create_volume_delay_scheduled_in_1_try(self, _mock_sleep, 310 _mock_is_ready, 311 _mock_sched_create): 312 self.manager._startup_delay = True 313 volume = fake_volume.fake_volume_obj(self.context) 314 315 request_spec = {'volume_id': volume.id} 316 request_spec_obj = objects.RequestSpec.from_primitives(request_spec) 317 318 _mock_is_ready.return_value = True 319 320 self.manager.create_volume(self.context, volume, 321 request_spec=request_spec_obj, 322 filter_properties={}) 323 _mock_sched_create.assert_called_once_with(self.context, 324 request_spec_obj, {}) 325 self.assertFalse(_mock_sleep.called) 326 327 @mock.patch('cinder.db.volume_get') 328 @mock.patch('cinder.scheduler.driver.Scheduler.backend_passes_filters') 329 @mock.patch('cinder.db.volume_update') 330 def test_migrate_volume_exception_returns_volume_state( 331 self, _mock_volume_update, _mock_backend_passes, 332 _mock_volume_get): 333 # Test NoValidBackend exception behavior for migrate_volume_to_host. 334 # Puts the volume in 'error_migrating' state and eats the exception. 335 fake_updates = {'migration_status': 'error'} 336 self._test_migrate_volume_exception_returns_volume_state( 337 _mock_volume_update, _mock_backend_passes, _mock_volume_get, 338 'available', fake_updates) 339 340 @mock.patch('cinder.db.volume_get') 341 @mock.patch('cinder.scheduler.driver.Scheduler.backend_passes_filters') 342 @mock.patch('cinder.db.volume_update') 343 def test_migrate_volume_exception_returns_volume_state_maintenance( 344 self, _mock_volume_update, _mock_backend_passes, 345 _mock_volume_get): 346 fake_updates = {'status': 'available', 347 'migration_status': 'error'} 348 self._test_migrate_volume_exception_returns_volume_state( 349 _mock_volume_update, _mock_backend_passes, _mock_volume_get, 350 'maintenance', fake_updates) 351 352 def _test_migrate_volume_exception_returns_volume_state( 353 self, _mock_volume_update, _mock_backend_passes, 354 _mock_volume_get, status, fake_updates): 355 volume = tests_utils.create_volume(self.context, 356 status=status, 357 previous_status='available') 358 fake_volume_id = volume.id 359 request_spec = {'volume_id': fake_volume_id} 360 _mock_backend_passes.side_effect = exception.NoValidBackend(reason="") 361 _mock_volume_get.return_value = volume 362 363 self.manager.migrate_volume_to_host(self.context, volume, 'host', True, 364 request_spec=request_spec, 365 filter_properties={}) 366 _mock_volume_update.assert_called_once_with(self.context, 367 fake_volume_id, 368 fake_updates) 369 _mock_backend_passes.assert_called_once_with(self.context, 'host', 370 request_spec, {}) 371 372 @mock.patch('cinder.db.volume_update') 373 @mock.patch('cinder.db.volume_attachment_get_all_by_volume_id') 374 @mock.patch('cinder.quota.QUOTAS.rollback') 375 def test_retype_volume_exception_returns_volume_state( 376 self, quota_rollback, _mock_vol_attachment_get, _mock_vol_update): 377 # Test NoValidBackend exception behavior for retype. 378 # Puts the volume in original state and eats the exception. 379 volume = tests_utils.create_volume(self.context, 380 status='retyping', 381 previous_status='in-use') 382 instance_uuid = '12345678-1234-5678-1234-567812345678' 383 volume_attach = tests_utils.attach_volume(self.context, volume.id, 384 instance_uuid, None, 385 '/dev/fake') 386 _mock_vol_attachment_get.return_value = [volume_attach] 387 reservations = mock.sentinel.reservations 388 request_spec = {'volume_id': volume.id, 'volume_type': {'id': 3}, 389 'migration_policy': 'on-demand', 390 'quota_reservations': reservations} 391 _mock_vol_update.return_value = {'status': 'in-use'} 392 _mock_find_retype_backend = mock.Mock( 393 side_effect=exception.NoValidBackend(reason="")) 394 orig_retype = self.manager.driver.find_retype_backend 395 self.manager.driver.find_retype_backend = _mock_find_retype_backend 396 397 self.manager.retype(self.context, volume, request_spec=request_spec, 398 filter_properties={}) 399 400 _mock_find_retype_backend.assert_called_once_with(self.context, 401 request_spec, {}, 402 'on-demand') 403 quota_rollback.assert_called_once_with(self.context, reservations) 404 _mock_vol_update.assert_called_once_with(self.context, volume.id, 405 {'status': 'in-use'}) 406 self.manager.driver.find_retype_host = orig_retype 407 408 def test_do_cleanup(self): 409 vol = tests_utils.create_volume(self.context, status='creating') 410 self.manager._do_cleanup(self.context, vol) 411 412 vol.refresh() 413 self.assertEqual('error', vol.status) 414 415 @mock.patch('cinder.scheduler.rpcapi.SchedulerAPI' 416 '.determine_rpc_version_cap', mock.Mock(return_value='2.0')) 417 def test_upgrading_cloud(self): 418 self.assertTrue(self.manager.upgrading_cloud) 419 420 @mock.patch('cinder.scheduler.rpcapi.SchedulerAPI' 421 '.determine_rpc_version_cap') 422 def test_upgrading_cloud_not(self, cap_mock): 423 cap_mock.return_value = self.manager.RPC_API_VERSION 424 self.assertFalse(self.manager.upgrading_cloud) 425 426 def test_cleanup_destination_scheduler(self): 427 service = objects.Service(id=1, host='hostname', 428 binary='cinder-scheduler') 429 result = self.manager._cleanup_destination(None, service) 430 expected = self.manager.sch_api.do_cleanup, None, service.host 431 self.assertEqual(expected, result) 432 433 def test_cleanup_destination_volume(self): 434 service = objects.Service(id=1, host='hostname', cluster_name=None, 435 binary=constants.VOLUME_BINARY) 436 result = self.manager._cleanup_destination(None, service) 437 expected = self.manager.volume_api.do_cleanup, service, service.host 438 self.assertEqual(expected, result) 439 440 def test_cleanup_destination_volume_cluster_cache_hit(self): 441 cluster = objects.Cluster(id=1, name='mycluster', 442 binary=constants.VOLUME_BINARY) 443 service = objects.Service(id=2, host='hostname', 444 cluster_name=cluster.name, 445 binary=constants.VOLUME_BINARY) 446 cluster_cache = {'cinder-volume': {'mycluster': cluster}} 447 result = self.manager._cleanup_destination(cluster_cache, service) 448 expected = self.manager.volume_api.do_cleanup, cluster, cluster.name 449 self.assertEqual(expected, result) 450 451 @mock.patch('cinder.objects.Cluster.get_by_id') 452 def test_cleanup_destination_volume_cluster_cache_miss(self, get_mock): 453 cluster = objects.Cluster(id=1, name='mycluster', 454 binary=constants.VOLUME_BINARY) 455 service = objects.Service(self.context, 456 id=2, host='hostname', 457 cluster_name=cluster.name, 458 binary=constants.VOLUME_BINARY) 459 get_mock.return_value = cluster 460 cluster_cache = collections.defaultdict(dict) 461 result = self.manager._cleanup_destination(cluster_cache, service) 462 expected = self.manager.volume_api.do_cleanup, cluster, cluster.name 463 self.assertEqual(expected, result) 464 465 @mock.patch('cinder.scheduler.manager.SchedulerManager.upgrading_cloud') 466 def test_work_cleanup_upgrading(self, upgrading_mock): 467 cleanup_request = objects.CleanupRequest(host='myhost') 468 upgrading_mock.return_value = True 469 self.assertRaises(exception.UnavailableDuringUpgrade, 470 self.manager.work_cleanup, 471 self.context, 472 cleanup_request) 473 474 @mock.patch('cinder.objects.Cluster.is_up', True) 475 @mock.patch('cinder.objects.Service.is_up', False) 476 @mock.patch('cinder.scheduler.rpcapi.SchedulerAPI.do_cleanup') 477 @mock.patch('cinder.volume.rpcapi.VolumeAPI.do_cleanup') 478 @mock.patch('cinder.objects.ServiceList.get_all') 479 def test_work_cleanup(self, get_mock, vol_clean_mock, sch_clean_mock): 480 args = dict(service_id=1, cluster_name='cluster_name', host='host', 481 binary=constants.VOLUME_BINARY, is_up=False, disabled=True, 482 resource_id=fake.VOLUME_ID, resource_type='Volume') 483 484 cluster = objects.Cluster(id=1, name=args['cluster_name'], 485 binary=constants.VOLUME_BINARY) 486 services = [objects.Service(self.context, 487 id=2, host='hostname', 488 cluster_name=cluster.name, 489 binary=constants.VOLUME_BINARY, 490 cluster=cluster), 491 objects.Service(self.context, 492 id=3, host='hostname', 493 cluster_name=None, 494 binary=constants.SCHEDULER_BINARY), 495 objects.Service(self.context, 496 id=4, host='hostname', 497 cluster_name=None, 498 binary=constants.VOLUME_BINARY)] 499 get_mock.return_value = services 500 501 cleanup_request = objects.CleanupRequest(self.context, **args) 502 res = self.manager.work_cleanup(self.context, cleanup_request) 503 self.assertEqual((services[:2], services[2:]), res) 504 self.assertEqual(1, vol_clean_mock.call_count) 505 self.assertEqual(1, sch_clean_mock.call_count) 506 507 508class SchedulerTestCase(test.TestCase): 509 """Test case for base scheduler driver class.""" 510 511 # So we can subclass this test and re-use tests if we need. 512 driver_cls = driver.Scheduler 513 514 def setUp(self): 515 super(SchedulerTestCase, self).setUp() 516 self.driver = self.driver_cls() 517 self.context = context.RequestContext(fake.USER_ID, fake.PROJECT_ID) 518 self.topic = 'fake_topic' 519 520 @mock.patch('cinder.scheduler.driver.Scheduler.' 521 'update_service_capabilities') 522 def test_update_service_capabilities(self, _mock_update_cap): 523 service_name = 'fake_service' 524 host = 'fake_host' 525 capabilities = {'fake_capability': 'fake_value'} 526 self.driver.update_service_capabilities(service_name, host, 527 capabilities, None) 528 _mock_update_cap.assert_called_once_with(service_name, host, 529 capabilities, None) 530 531 @mock.patch('cinder.scheduler.host_manager.HostManager.' 532 'has_all_capabilities', return_value=False) 533 def test_is_ready(self, _mock_has_caps): 534 ready = self.driver.is_ready() 535 _mock_has_caps.assert_called_once_with() 536 self.assertFalse(ready) 537 538 539class SchedulerDriverBaseTestCase(SchedulerTestCase): 540 """Test schedule driver class. 541 542 Test cases for base scheduler driver class methods 543 that will fail if the driver is changed. 544 """ 545 546 def test_unimplemented_schedule(self): 547 fake_args = (1, 2, 3) 548 fake_kwargs = {'cat': 'meow'} 549 550 self.assertRaises(NotImplementedError, self.driver.schedule, 551 self.context, self.topic, 'schedule_something', 552 *fake_args, **fake_kwargs) 553 554 555class SchedulerDriverModuleTestCase(test.TestCase): 556 """Test case for scheduler driver module methods.""" 557 558 def setUp(self): 559 super(SchedulerDriverModuleTestCase, self).setUp() 560 self.context = context.RequestContext(fake.USER_ID, fake.PROJECT_ID) 561 562 @mock.patch('cinder.db.volume_update') 563 @mock.patch('cinder.objects.volume.Volume.get_by_id') 564 def test_volume_host_update_db(self, _mock_volume_get, _mock_vol_update): 565 volume = fake_volume.fake_volume_obj(self.context) 566 _mock_volume_get.return_value = volume 567 568 driver.volume_update_db(self.context, volume.id, 'fake_host', 569 'fake_cluster') 570 scheduled_at = volume.scheduled_at.replace(tzinfo=None) 571 _mock_vol_update.assert_called_once_with( 572 self.context, volume.id, {'host': 'fake_host', 573 'cluster_name': 'fake_cluster', 574 'scheduled_at': scheduled_at}) 575