Merge "Ensure source service is up before resizing/migrating"
This commit is contained in:
commit
557b73d2a5
|
@ -56,7 +56,7 @@ class MigrateServerController(wsgi.Controller):
|
|||
host_name = body['migrate'].get('host')
|
||||
|
||||
instance = common.get_instance(self.compute_api, context, id,
|
||||
expected_attrs=['flavor'])
|
||||
expected_attrs=['flavor', 'services'])
|
||||
|
||||
if common.instance_has_port_with_resource_request(
|
||||
instance.uuid, self.network_api):
|
||||
|
@ -76,7 +76,9 @@ class MigrateServerController(wsgi.Controller):
|
|||
host_name=host_name)
|
||||
except (exception.TooManyInstances, exception.QuotaError) as e:
|
||||
raise exc.HTTPForbidden(explanation=e.format_message())
|
||||
except exception.InstanceIsLocked as e:
|
||||
except (exception.InstanceIsLocked,
|
||||
exception.InstanceNotReady,
|
||||
exception.ServiceUnavailable) as e:
|
||||
raise exc.HTTPConflict(explanation=e.format_message())
|
||||
except exception.InstanceInvalidState as state_error:
|
||||
common.raise_http_conflict_for_instance_invalid_state(state_error,
|
||||
|
|
|
@ -348,7 +348,7 @@ class ServersController(wsgi.Controller):
|
|||
return response
|
||||
|
||||
def _get_server(self, context, req, instance_uuid, is_detail=False,
|
||||
cell_down_support=False):
|
||||
cell_down_support=False, columns_to_join=None):
|
||||
"""Utility function for looking up an instance by uuid.
|
||||
|
||||
:param context: request context for auth
|
||||
|
@ -360,6 +360,8 @@ class ServersController(wsgi.Controller):
|
|||
returning a minimal instance
|
||||
construct if the relevant cell is
|
||||
down.
|
||||
:param columns_to_join: optional list of extra fields to join on the
|
||||
Instance object
|
||||
"""
|
||||
expected_attrs = ['flavor', 'numa_topology']
|
||||
if is_detail:
|
||||
|
@ -369,6 +371,8 @@ class ServersController(wsgi.Controller):
|
|||
expected_attrs.append("trusted_certs")
|
||||
expected_attrs = self._view_builder.get_show_expected_attrs(
|
||||
expected_attrs)
|
||||
if columns_to_join:
|
||||
expected_attrs.extend(columns_to_join)
|
||||
instance = common.get_instance(self.compute_api, context,
|
||||
instance_uuid,
|
||||
expected_attrs=expected_attrs,
|
||||
|
@ -931,7 +935,8 @@ class ServersController(wsgi.Controller):
|
|||
def _resize(self, req, instance_id, flavor_id, auto_disk_config=None):
|
||||
"""Begin the resize process with given instance/flavor."""
|
||||
context = req.environ["nova.context"]
|
||||
instance = self._get_server(context, req, instance_id)
|
||||
instance = self._get_server(context, req, instance_id,
|
||||
columns_to_join=['services'])
|
||||
context.can(server_policies.SERVERS % 'resize',
|
||||
target={'user_id': instance.user_id,
|
||||
'project_id': instance.project_id})
|
||||
|
@ -954,7 +959,9 @@ class ServersController(wsgi.Controller):
|
|||
except exception.QuotaError as error:
|
||||
raise exc.HTTPForbidden(
|
||||
explanation=error.format_message())
|
||||
except exception.InstanceIsLocked as e:
|
||||
except (exception.InstanceIsLocked,
|
||||
exception.InstanceNotReady,
|
||||
exception.ServiceUnavailable) as e:
|
||||
raise exc.HTTPConflict(explanation=e.format_message())
|
||||
except exception.InstanceInvalidState as state_error:
|
||||
common.raise_http_conflict_for_instance_invalid_state(state_error,
|
||||
|
|
|
@ -184,13 +184,34 @@ def reject_instance_state(vm_state=None, task_state=None):
|
|||
return outer
|
||||
|
||||
|
||||
def check_instance_host(function):
|
||||
@six.wraps(function)
|
||||
def wrapped(self, context, instance, *args, **kwargs):
|
||||
if not instance.host:
|
||||
raise exception.InstanceNotReady(instance_id=instance.uuid)
|
||||
return function(self, context, instance, *args, **kwargs)
|
||||
return wrapped
|
||||
def check_instance_host(check_is_up=False):
|
||||
"""Validate the instance.host before performing the operation.
|
||||
|
||||
At a minimum this method will check that the instance.host is set.
|
||||
|
||||
:param check_is_up: If True, check that the instance.host status is UP
|
||||
or MAINTENANCE (disabled but not down).
|
||||
:raises: InstanceNotReady if the instance.host is not set
|
||||
:raises: ServiceUnavailable if check_is_up=True and the instance.host
|
||||
compute service status is not UP or MAINTENANCE
|
||||
"""
|
||||
def outer(function):
|
||||
@six.wraps(function)
|
||||
def wrapped(self, context, instance, *args, **kwargs):
|
||||
if not instance.host:
|
||||
raise exception.InstanceNotReady(instance_id=instance.uuid)
|
||||
if check_is_up:
|
||||
# Make sure the source compute service is not down otherwise we
|
||||
# cannot proceed.
|
||||
host_status = self.get_instance_host_status(instance)
|
||||
if host_status not in (fields_obj.HostStatus.UP,
|
||||
fields_obj.HostStatus.MAINTENANCE):
|
||||
# ComputeServiceUnavailable would make more sense here but
|
||||
# we do not want to leak hostnames to end users.
|
||||
raise exception.ServiceUnavailable()
|
||||
return function(self, context, instance, *args, **kwargs)
|
||||
return wrapped
|
||||
return outer
|
||||
|
||||
|
||||
def check_instance_lock(function):
|
||||
|
@ -2512,14 +2533,14 @@ class API(base.Base):
|
|||
clean_shutdown=clean_shutdown)
|
||||
|
||||
@check_instance_lock
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.ERROR])
|
||||
def stop(self, context, instance, do_cast=True, clean_shutdown=True):
|
||||
"""Stop an instance."""
|
||||
self.force_stop(context, instance, do_cast, clean_shutdown)
|
||||
|
||||
@check_instance_lock
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
@check_instance_state(vm_state=[vm_states.STOPPED])
|
||||
def start(self, context, instance):
|
||||
"""Start an instance."""
|
||||
|
@ -2532,7 +2553,7 @@ class API(base.Base):
|
|||
self.compute_rpcapi.start_instance(context, instance)
|
||||
|
||||
@check_instance_lock
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
@check_instance_state(vm_state=vm_states.ALLOW_TRIGGER_CRASH_DUMP)
|
||||
def trigger_crash_dump(self, context, instance):
|
||||
"""Trigger crash dump in an instance."""
|
||||
|
@ -3787,6 +3808,7 @@ class API(base.Base):
|
|||
|
||||
@check_instance_lock
|
||||
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED])
|
||||
@check_instance_host(check_is_up=True)
|
||||
def resize(self, context, instance, flavor_id=None, clean_shutdown=True,
|
||||
host_name=None, auto_disk_config=None):
|
||||
"""Resize (ie, migrate) a running instance.
|
||||
|
@ -4078,12 +4100,12 @@ class API(base.Base):
|
|||
self._record_action_start(context, instance, instance_actions.UNPAUSE)
|
||||
self.compute_rpcapi.unpause_instance(context, instance)
|
||||
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
def get_diagnostics(self, context, instance):
|
||||
"""Retrieve diagnostics for the given instance."""
|
||||
return self.compute_rpcapi.get_diagnostics(context, instance=instance)
|
||||
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
def get_instance_diagnostics(self, context, instance):
|
||||
"""Retrieve diagnostics for the given instance."""
|
||||
return self.compute_rpcapi.get_instance_diagnostics(context,
|
||||
|
@ -4165,7 +4187,7 @@ class API(base.Base):
|
|||
instance=instance,
|
||||
new_pass=password)
|
||||
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
@reject_instance_state(
|
||||
task_state=[task_states.DELETING, task_states.MIGRATING])
|
||||
def get_vnc_console(self, context, instance, console_type):
|
||||
|
@ -4174,7 +4196,7 @@ class API(base.Base):
|
|||
instance=instance, console_type=console_type)
|
||||
return {'url': connect_info['access_url']}
|
||||
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
@reject_instance_state(
|
||||
task_state=[task_states.DELETING, task_states.MIGRATING])
|
||||
def get_spice_console(self, context, instance, console_type):
|
||||
|
@ -4183,7 +4205,7 @@ class API(base.Base):
|
|||
instance=instance, console_type=console_type)
|
||||
return {'url': connect_info['access_url']}
|
||||
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
@reject_instance_state(
|
||||
task_state=[task_states.DELETING, task_states.MIGRATING])
|
||||
def get_rdp_console(self, context, instance, console_type):
|
||||
|
@ -4192,7 +4214,7 @@ class API(base.Base):
|
|||
instance=instance, console_type=console_type)
|
||||
return {'url': connect_info['access_url']}
|
||||
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
@reject_instance_state(
|
||||
task_state=[task_states.DELETING, task_states.MIGRATING])
|
||||
def get_serial_console(self, context, instance, console_type):
|
||||
|
@ -4201,7 +4223,7 @@ class API(base.Base):
|
|||
instance=instance, console_type=console_type)
|
||||
return {'url': connect_info['access_url']}
|
||||
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
@reject_instance_state(
|
||||
task_state=[task_states.DELETING, task_states.MIGRATING])
|
||||
def get_mks_console(self, context, instance, console_type):
|
||||
|
@ -4210,7 +4232,7 @@ class API(base.Base):
|
|||
instance=instance, console_type=console_type)
|
||||
return {'url': connect_info['access_url']}
|
||||
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
def get_console_output(self, context, instance, tail_length=None):
|
||||
"""Get console output for an instance."""
|
||||
return self.compute_rpcapi.get_console_output(context,
|
||||
|
@ -5086,7 +5108,7 @@ class API(base.Base):
|
|||
|
||||
# We allow creating the snapshot in any vm_state as long as there is
|
||||
# no task being performed on the instance and it has a host.
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
@check_instance_state(vm_state=None)
|
||||
def do_volume_snapshot_create(self, context, instance):
|
||||
self.compute_rpcapi.volume_snapshot_create(context, instance,
|
||||
|
@ -5108,7 +5130,7 @@ class API(base.Base):
|
|||
|
||||
# We allow deleting the snapshot in any vm_state as long as there is
|
||||
# no task being performed on the instance and it has a host.
|
||||
@check_instance_host
|
||||
@check_instance_host()
|
||||
@check_instance_state(vm_state=None)
|
||||
def do_volume_snapshot_delete(self, context, instance):
|
||||
self.compute_rpcapi.volume_snapshot_delete(context, instance,
|
||||
|
|
|
@ -686,6 +686,10 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
|
|||
# NOTE(gibi): tags are not saved through the instance
|
||||
pass
|
||||
|
||||
def _save_services(self, context):
|
||||
# NOTE(mriedem): services are not saved through the instance
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _nullify_flavor_description(flavor_info):
|
||||
"""Helper method to nullify descriptions from a set of primitive
|
||||
|
|
|
@ -99,9 +99,9 @@ class TestComputeTaskNotificationSample(
|
|||
'hw:numa_cpus.0': '0',
|
||||
'hw:numa_mem.0': 512})
|
||||
self._wait_for_notification('instance.create.end')
|
||||
# Force down the compute node
|
||||
# Disable the compute node
|
||||
service_id = self.api.get_service_id('nova-compute')
|
||||
self.admin_api.put_service_force_down(service_id, True)
|
||||
self.admin_api.put_service(service_id, {'status': 'disabled'})
|
||||
|
||||
fake_notifier.reset()
|
||||
|
||||
|
|
|
@ -387,3 +387,65 @@ class EnforceVolumeBackedForZeroDiskFlavorTestCase(
|
|||
server = self.admin_api.post_server({'server': server_req})
|
||||
server = self._wait_for_state_change(server, 'ERROR')
|
||||
self.assertIn('No valid host', server['fault']['message'])
|
||||
|
||||
|
||||
class ResizeCheckInstanceHostTestCase(
|
||||
integrated_helpers.ProviderUsageBaseTestCase):
|
||||
"""Tests for the check_instance_host decorator used during resize/migrate.
|
||||
"""
|
||||
compute_driver = 'fake.MediumFakeDriver'
|
||||
|
||||
def test_resize_source_compute_validation(self, resize=True):
|
||||
flavors = self.api.get_flavors()
|
||||
# Start up a compute on which to create a server.
|
||||
self._start_compute('host1')
|
||||
# Create a server on host1.
|
||||
server = self._build_minimal_create_server_request(
|
||||
name='test_resize_source_compute_validation',
|
||||
image_uuid=fake_image.get_valid_image_id(),
|
||||
flavor_id=flavors[0]['id'],
|
||||
networks='none')
|
||||
server = self.api.post_server({'server': server})
|
||||
server = self._wait_for_state_change(server, 'ACTIVE')
|
||||
# Check if we're cold migrating.
|
||||
if resize:
|
||||
req = {'resize': {'flavorRef': flavors[1]['id']}}
|
||||
else:
|
||||
req = {'migrate': None}
|
||||
# Start up a destination compute.
|
||||
self._start_compute('host2')
|
||||
# First, force down the source compute service.
|
||||
source_service = self.api.get_services(
|
||||
binary='nova-compute', host='host1')[0]
|
||||
self.api.put_service(source_service['id'], {'forced_down': True})
|
||||
# Try the operation and it should fail with a 409 response.
|
||||
ex = self.assertRaises(api_client.OpenStackApiException,
|
||||
self.api.post_server_action, server['id'], req)
|
||||
self.assertEqual(409, ex.response.status_code)
|
||||
self.assertIn('Service is unavailable at this time', six.text_type(ex))
|
||||
# Now bring the source compute service up but disable it. The operation
|
||||
# should be allowed in this case since the service is up.
|
||||
self.api.put_service(source_service['id'],
|
||||
{'forced_down': False, 'status': 'disabled'})
|
||||
self.api.post_server_action(server['id'], req)
|
||||
server = self._wait_for_state_change(server, 'VERIFY_RESIZE')
|
||||
# Revert the resize to get the server back to host1.
|
||||
self.api.post_server_action(server['id'], {'revertResize': None})
|
||||
server = self._wait_for_state_change(server, 'ACTIVE')
|
||||
# Now shelve offload the server so it does not have a host.
|
||||
self.api.post_server_action(server['id'], {'shelve': None})
|
||||
self._wait_for_server_parameter(server,
|
||||
{'status': 'SHELVED_OFFLOADED',
|
||||
'OS-EXT-SRV-ATTR:host': None})
|
||||
# Now try the operation again and it should fail with a different
|
||||
# 409 response.
|
||||
ex = self.assertRaises(api_client.OpenStackApiException,
|
||||
self.api.post_server_action, server['id'], req)
|
||||
self.assertEqual(409, ex.response.status_code)
|
||||
# This error comes from check_instance_state which is processed before
|
||||
# check_instance_host.
|
||||
self.assertIn('while it is in vm_state shelved_offloaded',
|
||||
six.text_type(ex))
|
||||
|
||||
def test_cold_migrate_source_compute_validation(self):
|
||||
self.test_resize_source_compute_validation(resize=False)
|
||||
|
|
|
@ -52,7 +52,7 @@ class CommonMixin(object):
|
|||
if action == '_migrate_live':
|
||||
expected_attrs = ['numa_topology']
|
||||
elif action == '_migrate':
|
||||
expected_attrs = ['flavor']
|
||||
expected_attrs = ['flavor', 'services']
|
||||
|
||||
uuid = uuidutils.generate_uuid()
|
||||
self.mock_get.side_effect = exception.InstanceNotFound(
|
||||
|
@ -75,7 +75,7 @@ class CommonMixin(object):
|
|||
if action == '_migrate_live':
|
||||
expected_attrs = ['numa_topology']
|
||||
elif action == '_migrate':
|
||||
expected_attrs = ['flavor']
|
||||
expected_attrs = ['flavor', 'services']
|
||||
|
||||
if method is None:
|
||||
method = action.replace('_', '')
|
||||
|
@ -139,7 +139,7 @@ class CommonMixin(object):
|
|||
if action == '_migrate_live':
|
||||
expected_attrs = ['numa_topology']
|
||||
elif action == '_migrate':
|
||||
expected_attrs = ['flavor']
|
||||
expected_attrs = ['flavor', 'services']
|
||||
|
||||
if method is None:
|
||||
method = action.replace('_', '')
|
||||
|
@ -180,7 +180,7 @@ class CommonMixin(object):
|
|||
if action == '_migrate_live':
|
||||
expected_attrs = ['numa_topology']
|
||||
elif action == '_migrate':
|
||||
expected_attrs = ['flavor']
|
||||
expected_attrs = ['flavor', 'services']
|
||||
|
||||
if method is None:
|
||||
method = action.replace('_', '')
|
||||
|
|
|
@ -131,9 +131,9 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests):
|
|||
body={'migrate': None})
|
||||
mock_resize.assert_called_once_with(
|
||||
self.context, instance, host_name=self.host_name)
|
||||
self.mock_get.assert_called_once_with(self.context, instance.uuid,
|
||||
expected_attrs=['flavor'],
|
||||
cell_down_support=False)
|
||||
self.mock_get.assert_called_once_with(
|
||||
self.context, instance.uuid, expected_attrs=['flavor', 'services'],
|
||||
cell_down_support=False)
|
||||
|
||||
def test_migrate_too_many_instances(self):
|
||||
exc_info = exception.TooManyInstances(overs='', req='', used=0,
|
||||
|
|
|
@ -138,9 +138,11 @@ class ServerActionsControllerTestV21(test.TestCase):
|
|||
eval(controller_function),
|
||||
self.req, instance['uuid'],
|
||||
body=body_map.get(action))
|
||||
|
||||
expected_attrs = ['flavor', 'numa_topology']
|
||||
if method == 'resize':
|
||||
expected_attrs.append('services')
|
||||
mock_get.assert_called_once_with(self.context, uuid,
|
||||
expected_attrs=['flavor', 'numa_topology'],
|
||||
expected_attrs=expected_attrs,
|
||||
cell_down_support=False)
|
||||
mock_method.assert_called_once_with(self.context, instance,
|
||||
*args, **kwargs)
|
||||
|
|
|
@ -12410,6 +12410,8 @@ class DisabledInstanceTypesTestCase(BaseTestCase):
|
|||
self.assertRaises(exception.FlavorNotFound,
|
||||
self.compute_api.create, self.context, self.inst_type, None)
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=obj_fields.HostStatus.UP))
|
||||
@mock.patch('nova.compute.api.API._validate_flavor_image_nostatus')
|
||||
@mock.patch('nova.objects.RequestSpec')
|
||||
def test_can_resize_to_visible_instance_type(self, mock_reqspec,
|
||||
|
@ -12432,6 +12434,8 @@ class DisabledInstanceTypesTestCase(BaseTestCase):
|
|||
with mock.patch('nova.conductor.api.ComputeTaskAPI.resize_instance'):
|
||||
self.compute_api.resize(self.context, instance, '4')
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=obj_fields.HostStatus.UP))
|
||||
def test_cannot_resize_to_disabled_instance_type(self):
|
||||
instance = self._create_fake_instance_obj()
|
||||
orig_get_flavor_by_flavor_id = \
|
||||
|
|
|
@ -1750,6 +1750,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
mock_inst_save.assert_called_once_with(expected_task_state=[None])
|
||||
mock_get_requested_resources.assert_not_called()
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
@mock.patch('nova.compute.utils.is_volume_backed_instance',
|
||||
return_value=False)
|
||||
@mock.patch('nova.compute.api.API._validate_flavor_image_nostatus')
|
||||
|
@ -1964,6 +1966,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
def test_resize_allow_cross_cell_resize_true(self):
|
||||
self._test_resize(allow_cross_cell_resize=True)
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
@mock.patch('nova.compute.flavors.get_flavor_by_flavor_id')
|
||||
@mock.patch('nova.objects.Quotas.count_as_dict')
|
||||
@mock.patch('nova.objects.Quotas.limit_check_project_and_user')
|
||||
|
@ -2021,6 +2025,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
self._test_migrate(host_name='target_host',
|
||||
allow_cross_cell_resize=True)
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
@mock.patch.object(objects.ComputeNodeList, 'get_all_by_host',
|
||||
side_effect=exception.ComputeHostNotFound(
|
||||
host='nonexistent_host'))
|
||||
|
@ -2030,6 +2036,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
self.compute_api.resize, self.context,
|
||||
fake_inst, host_name='nonexistent_host')
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
@mock.patch.object(objects.Instance, 'save')
|
||||
@mock.patch.object(compute_api.API, '_record_action_start')
|
||||
@mock.patch.object(quotas_obj.Quotas, 'limit_check_project_and_user')
|
||||
|
@ -2055,6 +2063,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
mock_resize.assert_not_called()
|
||||
mock_save.assert_not_called()
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
@mock.patch.object(objects.Instance, 'save')
|
||||
@mock.patch.object(compute_api.API, '_record_action_start')
|
||||
@mock.patch.object(quotas_obj.Quotas, 'limit_check_project_and_user')
|
||||
|
@ -2081,6 +2091,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
mock_resize.assert_not_called()
|
||||
mock_save.assert_not_called()
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
||||
def test_resize_to_zero_disk_flavor_fails(self, get_flavor_by_flavor_id):
|
||||
fake_inst = self._create_instance_obj()
|
||||
|
@ -2095,6 +2107,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
self.compute_api.resize, self.context,
|
||||
fake_inst, flavor_id='flavor-id')
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
@mock.patch('nova.compute.api.API._validate_flavor_image_nostatus')
|
||||
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
||||
@mock.patch('nova.compute.api.API._record_action_start')
|
||||
|
@ -2124,6 +2138,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
|
||||
do_test()
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
@mock.patch.object(objects.Instance, 'save')
|
||||
@mock.patch.object(compute_api.API, '_record_action_start')
|
||||
@mock.patch.object(quotas_obj.Quotas,
|
||||
|
@ -2180,6 +2196,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
mock_record.assert_not_called()
|
||||
mock_resize.assert_not_called()
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
||||
@mock.patch.object(compute_utils, 'upsize_quota_delta')
|
||||
@mock.patch.object(quotas_obj.Quotas, 'count_as_dict')
|
||||
|
@ -2205,6 +2223,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
fake_inst, flavor_id='flavor-id')
|
||||
self.assertFalse(mock_save.called)
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
||||
@mock.patch.object(objects.Quotas, 'count_as_dict')
|
||||
@mock.patch.object(objects.Quotas, 'limit_check_project_and_user')
|
||||
|
@ -3503,6 +3523,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
|||
lambda obj, context, image_id, **kwargs: self.fake_image)
|
||||
return self.fake_image['id']
|
||||
|
||||
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
||||
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
||||
def test_resize_with_disabled_auto_disk_config_fails(self):
|
||||
fake_inst = self._create_instance_with_disabled_disk_config(
|
||||
object=True)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
fixes:
|
||||
- |
|
||||
This release contains a fix for `bug 1856925`_ such that ``resize`` and
|
||||
``migrate`` server actions will be rejected with a 409 ``HTTPConflict``
|
||||
response if the source compute service is down.
|
||||
|
||||
.. _bug 1856925: https://bugs.launchpad.net/nova/+bug/1856925
|
Loading…
Reference in New Issue