From a263fa46f861c091d93782d4796c8302f9c30f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: Mon, 13 Jun 2022 15:20:51 +0200 Subject: [PATCH] Allow unshelve to a specific host (Compute API part) This patch introduce changes to the compute API that will allow PROJECT_ADMIN to unshelve an shelved offloaded server to a specific host. This patch also supports the ability to unpin the availability_zone of an instance that is bound to it. Implements: blueprint unshelve-to-host Change-Id: Ieb4766fdd88c469574fad823e05fe401537cdc30 --- nova/api/openstack/compute/shelve.py | 7 +- nova/compute/api.py | 149 +++-- nova/conductor/manager.py | 6 + nova/exception.py | 12 +- .../unit/api/openstack/compute/test_shelve.py | 8 +- nova/tests/unit/compute/test_shelve.py | 627 +++++++++++++++++- 6 files changed, 747 insertions(+), 62 deletions(-) diff --git a/nova/api/openstack/compute/shelve.py b/nova/api/openstack/compute/shelve.py index 829247cac438..deef3265f521 100644 --- a/nova/api/openstack/compute/shelve.py +++ b/nova/api/openstack/compute/shelve.py @@ -95,14 +95,15 @@ class ShelveController(wsgi.Controller): context.can(shelve_policies.POLICY_ROOT % 'unshelve', target={'project_id': instance.project_id}) - new_az = None + unshelve_args = {} + unshelve_dict = body['unshelve'] support_az = api_version_request.is_supported(req, '2.77') if support_az and unshelve_dict: - new_az = unshelve_dict['availability_zone'] + unshelve_args['new_az'] = unshelve_dict['availability_zone'] try: - self.compute_api.unshelve(context, instance, new_az=new_az) + self.compute_api.unshelve(context, instance, **unshelve_args) except (exception.InstanceIsLocked, exception.UnshelveInstanceInvalidState, exception.MismatchVolumeAZException) as e: diff --git a/nova/compute/api.py b/nova/compute/api.py index 8170fd8f24f8..3b4bf4d26123 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -380,6 +380,8 @@ def block_extended_resource_request(function): class API: """API for interacting with the compute manager.""" + _sentinel = object() + def __init__(self, image_api=None, network_api=None, volume_api=None): self.image_api = image_api or glance.API() self.network_api = network_api or neutron.API() @@ -4391,31 +4393,45 @@ class API: context, instance=instance, clean_shutdown=clean_shutdown, accel_uuids=accel_uuids) - def _validate_unshelve_az(self, context, instance, availability_zone): - """Verify the specified availability_zone during unshelve. - - Verifies that the server is shelved offloaded, the AZ exists and - if [cinder]/cross_az_attach=False, that any attached volumes are in - the same AZ. - - :param context: nova auth RequestContext for the unshelve action - :param instance: Instance object for the server being unshelved - :param availability_zone: The user-requested availability zone in - which to unshelve the server. - :raises: UnshelveInstanceInvalidState if the server is not shelved - offloaded - :raises: InvalidRequest if the requested AZ does not exist - :raises: MismatchVolumeAZException if [cinder]/cross_az_attach=False - and any attached volumes are not in the requested AZ + def _check_offloaded(self, context, instance): + """Check if the status of an instance is SHELVE_OFFLOADED, + if not raise an exception. """ if instance.vm_state != vm_states.SHELVED_OFFLOADED: # NOTE(brinzhang): If the server status is 'SHELVED', it still - # belongs to a host, the availability_zone has not changed. + # belongs to a host, the availability_zone should not change. # Unshelving a shelved offloaded server will go through the # scheduler to find a new host. raise exception.UnshelveInstanceInvalidState( state=instance.vm_state, instance_uuid=instance.uuid) + def _ensure_host_in_az(self, context, host, availability_zone): + """Ensure the host provided belongs to the availability zone, + if not raise an exception. + """ + if availability_zone is not None: + host_az = availability_zones.get_host_availability_zone( + context, + host + ) + if host_az != availability_zone: + raise exception.UnshelveHostNotInAZ( + host=host, availability_zone=availability_zone) + + def _validate_unshelve_az(self, context, instance, availability_zone): + """Verify the specified availability_zone during unshelve. + + Verifies the AZ exists and if [cinder]/cross_az_attach=False, that + any attached volumes are in the same AZ. + + :param context: nova auth RequestContext for the unshelve action + :param instance: Instance object for the server being unshelved + :param availability_zone: The user-requested availability zone in + which to unshelve the server. + :raises: InvalidRequest if the requested AZ does not exist + :raises: MismatchVolumeAZException if [cinder]/cross_az_attach=False + and any attached volumes are not in the requested AZ + """ available_zones = availability_zones.get_availability_zones( context, self.host_api, get_only_available=True) if availability_zone not in available_zones: @@ -4443,31 +4459,88 @@ class API: @block_extended_resource_request @check_instance_lock - @check_instance_state(vm_state=[vm_states.SHELVED, - vm_states.SHELVED_OFFLOADED]) - def unshelve(self, context, instance, new_az=None): - """Restore a shelved instance.""" + @check_instance_state( + vm_state=[vm_states.SHELVED, vm_states.SHELVED_OFFLOADED]) + def unshelve( + self, context, instance, new_az=_sentinel, host=None): + """Restore a shelved instance. + + :param context: the nova request context + :param instance: nova.objects.instance.Instance object + :param new_az: (optional) target AZ. + If None is provided then the current AZ restriction + will be removed from the instance. + If the parameter is not provided then the current + AZ restriction will not be changed. + :param host: (optional) a host to target + """ + # Unshelving a shelved offloaded server will go through the + # scheduler to pick a new host, so we update the + # RequestSpec.availability_zone here. Note that if scheduling + # fails the RequestSpec will remain updated, which is not great. + # Bug open to track this https://bugs.launchpad.net/nova/+bug/1978573 + + az_passed = new_az is not self._sentinel + request_spec = objects.RequestSpec.get_by_instance_uuid( context, instance.uuid) - if new_az: + # We need to check a list of preconditions and validate inputs first + + # Ensure instance is shelve offloaded + if az_passed or host: + self._check_offloaded(context, instance) + + if az_passed and new_az: + # we have to ensure that new AZ is valid self._validate_unshelve_az(context, instance, new_az) - LOG.debug("Replace the old AZ %(old_az)s in RequestSpec " - "with a new AZ %(new_az)s of the instance.", - {"old_az": request_spec.availability_zone, - "new_az": new_az}, instance=instance) - # Unshelving a shelved offloaded server will go through the - # scheduler to pick a new host, so we update the - # RequestSpec.availability_zone here. Note that if scheduling - # fails the RequestSpec will remain updated, which is not great, - # but if we want to change that we need to defer updating the - # RequestSpec until conductor which probably means RPC changes to - # pass the new_az variable to conductor. This is likely low - # priority since the RequestSpec.availability_zone on a shelved - # offloaded server does not mean much anyway and clearly the user - # is trying to put the server in the target AZ. - request_spec.availability_zone = new_az - request_spec.save() + # This will be the AZ of the instance after the unshelve. It can be + # None indicating that the instance is not pinned to any AZ after the + # unshelve + expected_az_after_unshelve = ( + request_spec.availability_zone + if not az_passed else new_az + ) + # host is requested, so we have to see if it exists and does not + # contradict with the AZ of the instance + if host: + # Ensure that the requested host exists otherwise raise + # a ComputeHostNotFound exception + objects.ComputeNode.get_first_node_by_host_for_old_compat( + context, host, use_slave=True) + # A specific host is requested so we need to make sure that it is + # not contradicts with the AZ of the instance + self._ensure_host_in_az( + context, host, expected_az_after_unshelve) + + if new_az is None: + LOG.debug( + 'Unpin instance from AZ "%(old_az)s".', + {'old_az': request_spec.availability_zone}, + instance=instance + ) + + LOG.debug( + 'Unshelving instance with old availability_zone "%(old_az)s" to ' + 'new availability_zone "%(new_az)s" and host "%(host)s".', + { + 'old_az': request_spec.availability_zone, + 'new_az': '%s' % + new_az if az_passed + else 'not provided', + 'host': host, + }, + instance=instance, + ) + # OK every precondition checks out, we just need to tell the scheduler + # where to put the instance + # We have the expected AZ already calculated. So we just need to + # set it in the request_spec to drive the scheduling + request_spec.availability_zone = expected_az_after_unshelve + # if host is requested we also need to tell the scheduler that + if host: + request_spec.requested_destination = objects.Destination(host=host) + request_spec.save() instance.task_state = task_states.UNSHELVING instance.save(expected_task_state=[None]) diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py index 99e5514136e5..f6b0815d1b71 100644 --- a/nova/conductor/manager.py +++ b/nova/conductor/manager.py @@ -1022,6 +1022,12 @@ class ComputeTaskManager: scheduler_utils.populate_filter_properties( filter_properties, selection) (host, node) = (selection.service_host, selection.nodename) + LOG.debug( + "Scheduler selected host: %s, node:%s", + host, + node, + instance=instance + ) instance.availability_zone = ( availability_zones.get_host_availability_zone( context, host)) diff --git a/nova/exception.py b/nova/exception.py index 4588898aae9d..8c04b85d0a04 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1676,9 +1676,15 @@ class MismatchVolumeAZException(Invalid): class UnshelveInstanceInvalidState(InstanceInvalidState): - msg_fmt = _('Specifying an availability zone when unshelving server ' - '%(instance_uuid)s with status "%(state)s" is not supported. ' - 'The server status must be SHELVED_OFFLOADED.') + msg_fmt = _('Specifying an availability zone or a host when unshelving ' + 'server "%(instance_uuid)s" with status "%(state)s" is not ' + 'supported. The server status must be SHELVED_OFFLOADED.') + code = 409 + + +class UnshelveHostNotInAZ(Invalid): + msg_fmt = _('Host "%(host)s" is not in the availability zone ' + '"%(availability_zone)s".') code = 409 diff --git a/nova/tests/unit/api/openstack/compute/test_shelve.py b/nova/tests/unit/api/openstack/compute/test_shelve.py index 68e523be4744..c3fb973c97a7 100644 --- a/nova/tests/unit/api/openstack/compute/test_shelve.py +++ b/nova/tests/unit/api/openstack/compute/test_shelve.py @@ -140,7 +140,9 @@ class UnshelveServerControllerTestV277(test.NoDBTestCase): 'unshelve') as mock_unshelve: self.controller._unshelve(self.req, fakes.FAKE_UUID, body=body) mock_unshelve.assert_called_once_with( - self.req.environ['nova.context'], instance, new_az=None) + self.req.environ['nova.context'], + instance, + ) @mock.patch('nova.compute.api.API.unshelve') @mock.patch('nova.api.openstack.common.get_instance') @@ -158,7 +160,9 @@ class UnshelveServerControllerTestV277(test.NoDBTestCase): APIVersionRequest('2.76')) self.controller._unshelve(self.req, fakes.FAKE_UUID, body=body) mock_unshelve.assert_called_once_with( - self.req.environ['nova.context'], instance, new_az=None) + self.req.environ['nova.context'], + instance, + ) @mock.patch('nova.compute.api.API.unshelve') @mock.patch('nova.api.openstack.common.get_instance') diff --git a/nova/tests/unit/compute/test_shelve.py b/nova/tests/unit/compute/test_shelve.py index a50b4ca4de00..d6ccc712b98a 100644 --- a/nova/tests/unit/compute/test_shelve.py +++ b/nova/tests/unit/compute/test_shelve.py @@ -11,10 +11,10 @@ # under the License. import eventlet -import mock from oslo_utils import fixture as utils_fixture from oslo_utils.fixture import uuidsentinel as uuids from oslo_utils import timeutils +from unittest import mock from nova.compute import api as compute_api from nova.compute import claims @@ -24,6 +24,7 @@ from nova.compute import task_states from nova.compute import utils as compute_utils from nova.compute import vm_states import nova.conf +from nova import context from nova.db.main import api as db from nova import exception from nova.network import neutron as neutron_api @@ -849,9 +850,67 @@ class ShelveComputeAPITestCase(test_compute.BaseTestCase): exclude_states = set() return vm_state - exclude_states + @mock.patch('nova.scheduler.client.report.SchedulerReportClient.' + 'aggregate_add_host') + @mock.patch('nova.availability_zones.get_availability_zones') + def _create_host_inside_az( + self, + ctxt, + host, + az, + mock_az, + mock_aggregate, + ): + + self.api = compute_api.AggregateAPI() + mock_az.return_value = [az] + + cells = objects.CellMappingList.get_all(ctxt) + cell = cells[0] + with context.target_cell(ctxt, cell) as cctxt: + s = objects.Service(context=cctxt, + host=host, + binary='nova-compute', + topic='compute', + report_count=0) + s.create() + + hm = objects.HostMapping(context=ctxt, + cell_mapping=cell, + host=host) + hm.create() + + self._init_aggregate_with_host(None, 'fake_aggregate1', + az, host) + + def _create_request_spec_for_initial_az(self, az): + fake_spec = objects.RequestSpec() + fake_spec.availability_zone = az + return fake_spec + + def _assert_unshelving_and_request_spec_az_and_host( + self, + context, + instance, + fake_spec, + fake_zone, + fake_host, + mock_get_by_instance_uuid, + mock_unshelve + ): + mock_get_by_instance_uuid.assert_called_once_with(context, + instance.uuid) + + mock_unshelve.assert_called_once_with(context, instance, fake_spec) + + self.assertEqual(instance.task_state, task_states.UNSHELVING) + self.assertEqual(fake_spec.availability_zone, fake_zone) + if fake_host: + self.assertEqual(fake_spec.requested_destination.host, fake_host) + def _test_shelve(self, vm_state=vm_states.ACTIVE, boot_from_volume=False, clean_shutdown=True): - # Ensure instance can be shelved. + params = dict(task_state=None, vm_state=vm_state, display_name='vm01') fake_instance = self._create_fake_instance_obj(params=params) instance = fake_instance @@ -988,12 +1047,14 @@ class ShelveComputeAPITestCase(test_compute.BaseTestCase): return instance + @mock.patch.object(objects.RequestSpec, 'save') @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') - def test_unshelve(self, get_by_instance_uuid): + def test_unshelve(self, get_by_instance_uuid, fake_save): # Ensure instance can be unshelved. instance = self._get_specify_state_instance(vm_states.SHELVED) fake_spec = objects.RequestSpec() + fake_spec.availability_zone = None get_by_instance_uuid.return_value = fake_spec with mock.patch.object(self.compute_api.compute_task_api, 'unshelve_instance') as unshelve: @@ -1116,24 +1177,558 @@ class ShelveComputeAPITestCase(test_compute.BaseTestCase): mock_get_bdms.assert_called_once_with(self.context, instance.uuid) mock_get.assert_called_once_with(self.context, uuids.volume_id) - @mock.patch.object(compute_api.API, '_validate_unshelve_az') +# Next tests attempt to check the following behavior +# +----------+---------------------------+-------+----------------------------+ +# | Boot | Unshelve after offload AZ | Host | Result | +# +==========+===========================+=======+============================+ +# | No AZ | No AZ or AZ=null | No | Free scheduling, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | No AZ or AZ=null | Host1 | Schedule to host1, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | AZ="AZ1" | No | Schedule to AZ1, | +# | | | | reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | AZ="AZ1" | Host1 | Verify that host1 in AZ1, | +# | | | | or (1). Schedule to | +# | | | | host1, reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | No AZ | No | Schedule to AZ1, | +# | | | | reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ=null | No | Free scheduling, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | No AZ | Host1 | If host1 is in AZ1, | +# | | | | then schedule to host1, | +# | | | | reqspec.AZ="AZ1", otherwise| +# | | | | reject the request (1) | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ=null | Host1 | Schedule to host1, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ="AZ2" | No | Schedule to AZ2, | +# | | | | reqspec.AZ="AZ2" | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ="AZ2" | Host1 | If host1 in AZ2 then | +# | | | | schedule to host1, | +# | | | | reqspec.AZ="AZ2", | +# | | | | otherwise reject (1) | +# +----------+---------------------------+-------+----------------------------+ +# +# (1) Check at the api and return an error. +# +# +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | No AZ or AZ=null | No | Free scheduling, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') - def test_specified_az_unshelve(self, get_by_instance_uuid, - mock_save, mock_validate_unshelve_az): - # Ensure instance can be unshelved. + def test_unshelve_without_az( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + instance = self._get_specify_state_instance( vm_states.SHELVED_OFFLOADED) - new_az = "west_az" - fake_spec = objects.RequestSpec() - fake_spec.availability_zone = "fake-old-az" - get_by_instance_uuid.return_value = fake_spec + fake_spec = self._create_request_spec_for_initial_az(None) + mock_get_by_instance_uuid.return_value = fake_spec - self.compute_api.unshelve(self.context, instance, new_az=new_az) + self.compute_api.unshelve(context, instance) - mock_save.assert_called_once_with() - self.assertEqual(new_az, fake_spec.availability_zone) + self._assert_unshelving_and_request_spec_az_and_host( + context, + instance, + fake_spec, + None, + None, + mock_get_by_instance_uuid, + mock_unshelve + ) - mock_validate_unshelve_az.assert_called_once_with( - self.context, instance, new_az) +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | No AZ or AZ=null | Host1 | Schedule to host1, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_without_az_to_host( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az(None) + mock_get_by_instance_uuid.return_value = fake_spec + + self.compute_api.unshelve(context, instance, host=fake_host) + + self._assert_unshelving_and_request_spec_az_and_host( + context, + instance, + fake_spec, + None, + fake_host, + mock_get_by_instance_uuid, + mock_unshelve + ) + +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | AZ="AZ1" | No | Schedule to AZ1, | +# | | | | reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_without_az_to_newaz( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az(None) + mock_get_by_instance_uuid.return_value = fake_spec + + self.compute_api.unshelve(context, instance, new_az=fake_zone) + + self._assert_unshelving_and_request_spec_az_and_host( + context, + instance, + fake_spec, + fake_zone, + None, + mock_get_by_instance_uuid, + mock_unshelve + ) + +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | AZ="AZ1" | Host1 | Verify that host1 in AZ1, | +# | | | | or (1). Schedule to | +# | | | | host1, reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_without_az_to_newaz_and_host( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az(None) + mock_get_by_instance_uuid.return_value = fake_spec + + self.compute_api.unshelve( + context, instance, new_az=fake_zone, host=fake_host + ) + + self._assert_unshelving_and_request_spec_az_and_host( + context, + instance, + fake_spec, + fake_zone, + fake_host, + mock_get_by_instance_uuid, + mock_unshelve + ) + + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_without_az_to_newaz_and_host_invalid( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az(None) + mock_get_by_instance_uuid.return_value = fake_spec + + exc = self.assertRaises( + nova.exception.UnshelveHostNotInAZ, + self.compute_api.unshelve, + context, + instance, + new_az='avail_zone1', + host='fake_mini' + ) + + self.assertIn( + exc.message, + 'Host "fake_mini" is not in the availability zone "avail_zone1".' + ) + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | No AZ | No | Schedule to AZ1, | +# | | | | reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_with_az( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az(fake_zone) + mock_get_by_instance_uuid.return_value = fake_spec + + self.compute_api.unshelve(context, instance) + + self._assert_unshelving_and_request_spec_az_and_host( + context, + instance, + fake_spec, + fake_zone, + None, + mock_get_by_instance_uuid, + mock_unshelve + ) + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ=null | No | Free scheduling, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_with_az_to_unpin_az( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az(fake_zone) + mock_get_by_instance_uuid.return_value = fake_spec + + self.compute_api.unshelve(context, instance, new_az=None) + + self._assert_unshelving_and_request_spec_az_and_host( + context, + instance, + fake_spec, + None, + None, + mock_get_by_instance_uuid, + mock_unshelve + ) + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | No AZ | Host1 | If host1 is in AZ1, | +# | | | | then schedule to host1, | +# | | | | reqspec.AZ="AZ1", otherwise| +# | | | | reject the request (1) | +# +----------+---------------------------+-------+----------------------------+ + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_with_az_to_host_in_az( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az(fake_zone) + mock_get_by_instance_uuid.return_value = fake_spec + + self.compute_api.unshelve(context, instance, host=fake_host) + + self._assert_unshelving_and_request_spec_az_and_host( + context, + instance, + fake_spec, + fake_zone, + fake_host, + mock_get_by_instance_uuid, + mock_unshelve + ) + + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_with_az_to_invalid_host( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az(fake_zone) + mock_get_by_instance_uuid.return_value = fake_spec + + exc = self.assertRaises( + nova.exception.UnshelveHostNotInAZ, + self.compute_api.unshelve, + context, + instance, + host='fake_mini' + ) + + self.assertIn( + exc.message, + 'Host "fake_mini" is not in the availability zone "avail_zone1".' + ) + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ=null | Host1 | Schedule to host1, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_with_az_to_host_unpin_az( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az(fake_zone) + mock_get_by_instance_uuid.return_value = fake_spec + + self.compute_api.unshelve( + context, instance, new_az=None, host=fake_host + ) + + self._assert_unshelving_and_request_spec_az_and_host( + context, + instance, + fake_spec, + None, + fake_host, + mock_get_by_instance_uuid, + mock_unshelve + ) + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ="AZ2" | No | Schedule to AZ2, | +# | | | | reqspec.AZ="AZ2" | +# +----------+---------------------------+-------+----------------------------+ + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_with_az_to_newaz( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az('az1') + mock_get_by_instance_uuid.return_value = fake_spec + + self.compute_api.unshelve( + context, instance, new_az=fake_zone + ) + + self._assert_unshelving_and_request_spec_az_and_host( + context, + instance, + fake_spec, + fake_zone, + None, + mock_get_by_instance_uuid, + mock_unshelve + ) + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ="AZ2" | Host1 | If host1 in AZ2 then | +# | | | | schedule to host1, | +# | | | | reqspec.AZ="AZ2", | +# | | | | otherwise reject (1) | +# +----------+---------------------------+-------+----------------------------+ + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_with_az_to_newaz_and_host( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az('az1') + mock_get_by_instance_uuid.return_value = fake_spec + + self.compute_api.unshelve( + context, instance, new_az=fake_zone, host=fake_host + ) + + self._assert_unshelving_and_request_spec_az_and_host( + context, + instance, + fake_spec, + fake_zone, + fake_host, + mock_get_by_instance_uuid, + mock_unshelve + ) + + @mock.patch.object(nova.conductor.ComputeTaskAPI, 'unshelve_instance') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve_with_az_to_newaz_and_invalid_host( + self, + mock_get_by_instance_uuid, + mock_get_all_by_host, + mock_save, + mock_unshelve + ): + + context = self.context.elevated() + fake_host = 'fake_host1' + fake_zone = 'avail_zone1' + self._create_host_inside_az(self.context, fake_host, fake_zone) + + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + fake_spec = self._create_request_spec_for_initial_az('az1') + mock_get_by_instance_uuid.return_value = fake_spec + + exc = self.assertRaises( + nova.exception.UnshelveHostNotInAZ, + self.compute_api.unshelve, + context, + instance, + new_az=fake_zone, + host='fake_mini' + ) + + self.assertIn( + exc.message, + 'Host "fake_mini" is not in the availability zone "avail_zone1".' + )