Specify availability_zone to unshelve

This adds support, in a new microversion, for specifying an availability
zone to the unshelve server action when the server is shelved offloaded.

Note that the functional test changes are due to those tests using the
"latest" microversion where an empty dict is not allowed for unshelve
with 2.77 so the value is changed from an empty dict to None.

Implements: blueprint support-specifying-az-when-restore-shelved-server
Closes-Bug: #1723880

Change-Id: I4b13483eef42bed91d69eabf1f30762d6866f957
This commit is contained in:
zhangbailin 2019-06-07 15:11:29 +08:00 committed by Matt Riedemann
parent 912a46c9d4
commit 27b6c18c66
22 changed files with 481 additions and 18 deletions

View File

@ -1799,6 +1799,15 @@ availability_zone_state:
in: body
required: true
type: object
availability_zone_unshelve:
description: |
The availability zone name. Specifying an availability zone is only
allowed when the server status is ``SHELVED_OFFLOADED`` otherwise a
409 HTTPConflict response is returned.
in: body
required: false
type: string
min_version: 2.77
available:
description: |
Returns true if the availability zone is available.

View File

@ -138,7 +138,7 @@ If the server status does not change to ``ACTIVE``, the unshelve operation faile
Normal response codes: 202
Error response codes: unauthorized(401), forbidden(403), itemNotFound(404), conflict(409)
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404), conflict(409)
Request
-------
@ -147,6 +147,7 @@ Request
- server_id: server_id_path
- unshelve: unshelve
- availability_zone: availability_zone_unshelve
|
@ -155,6 +156,11 @@ Request
.. literalinclude:: ../../doc/api_samples/os-shelve/os-unshelve.json
:language: javascript
**Example Unshelve server (unshelve Action) (v2.77)**
.. literalinclude:: ../../doc/api_samples/os-shelve/v2.77/os-unshelve.json
:language: javascript
Response
--------

View File

@ -0,0 +1,3 @@
{
"shelve": null
}

View File

@ -0,0 +1,3 @@
{
"unshelve": null
}

View File

@ -0,0 +1,5 @@
{
"unshelve": {
"availability_zone": "us-west"
}
}

View File

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.76",
"version": "2.77",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.76",
"version": "2.77",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -91,6 +91,9 @@ With respect to availability zones, a server is restricted to a zone if:
parameter but the API service is configured for
:oslo.config:option:`default_schedule_zone` then by default the server will
be scheduled to that zone.
3. The shelved offloaded server was unshelved by specifying the
``availability_zone`` with the ``POST /servers/{server_id}/action`` request
using microversion 2.77 or greater.
If the server was not created in a specific zone then it is free to be moved
to other zones, i.e. the :ref:`AvailabilityZoneFilter <AvailabilityZoneFilter>`

View File

@ -201,6 +201,8 @@ REST_API_VERSION_HISTORY = """REST API Version History:
can be viewed through
``GET /servers/{server_id}/os-instance-actions`` and
``GET /servers/{server_id}/os-instance-actions/{request_id}``.
* 2.77 - Add support for specifying ``availability_zone`` to unshelve of a
shelved offload server.
"""
# The minimum and maximum versions of the API supported
@ -209,7 +211,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.76"
_MAX_API_VERSION = "2.77"
DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal

View File

@ -993,3 +993,8 @@ Adds ``power-update`` event name to ``os-server-external-events`` API. The
changes to the power state of an instance caused by this event can be viewed
through ``GET /servers/{server_id}/os-instance-actions`` and
``GET /servers/{server_id}/os-instance-actions/{request_id}``.
2.77
----
API microversion 2.77 adds support for specifying availability zone when
unshelving a shelved offloaded server.

View File

@ -0,0 +1,37 @@
# Copyright 2019 INSPUR Corporation. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from nova.api.validation import parameter_types
# NOTE(brinzhang): For older microversion there will be no change as
# schema is applied only for >2.77 with unshelve a server API.
# Anything working in old version keep working as it is.
unshelve_v277 = {
'type': 'object',
'properties': {
'unshelve': {
'type': ['object', 'null'],
'properties': {
'availability_zone': parameter_types.name
},
# NOTE: The allowed request body is {'unshelve': null} or
# {'unshelve': {'availability_zone': <string>}}, not allowed
# {'unshelve': {}} as the request body for unshelve.
'required': ['availability_zone'],
'additionalProperties': False,
},
},
'required': ['unshelve'],
'additionalProperties': False,
}

View File

@ -16,8 +16,11 @@
from webob import exc
from nova.api.openstack import api_version_request
from nova.api.openstack import common
from nova.api.openstack.compute.schemas import shelve as shelve_schemas
from nova.api.openstack import wsgi
from nova.api import validation
from nova.compute import api as compute
from nova.compute import vm_states
from nova import exception
@ -72,12 +75,23 @@ class ShelveController(wsgi.Controller):
@wsgi.response(202)
@wsgi.expected_errors((400, 404, 409))
@wsgi.action('unshelve')
# In microversion 2.77 we support specifying 'availability_zone' to
# unshelve a server. But before 2.77 there is no request body
# schema validation (because of body=null).
@validation.schema(shelve_schemas.unshelve_v277, min_version='2.77')
def _unshelve(self, req, id, body):
"""Restore an instance from shelved mode."""
context = req.environ["nova.context"]
context.can(shelve_policies.POLICY_ROOT % 'unshelve')
instance = common.get_instance(self.compute_api, context, id)
new_az = None
unshelve_dict = body['unshelve']
if unshelve_dict and 'availability_zone' in unshelve_dict:
support_az = api_version_request.is_supported(req, '2.77')
if support_az:
new_az = unshelve_dict['availability_zone']
# We could potentially move this check to conductor and avoid the
# extra API call to neutron when we support move operations with ports
# having resource requests.
@ -93,10 +107,14 @@ class ShelveController(wsgi.Controller):
raise exc.HTTPBadRequest(explanation=msg)
try:
self.compute_api.unshelve(context, instance)
except exception.InstanceIsLocked as e:
self.compute_api.unshelve(context, instance, new_az=new_az)
except (exception.InstanceIsLocked,
exception.UnshelveInstanceInvalidState,
exception.MismatchVolumeAZException) 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,
'unshelve',
id)
except exception.InvalidRequest as e:
raise exc.HTTPBadRequest(explanation=e.format_message())

View File

@ -3658,14 +3658,83 @@ class API(base.Base):
self.compute_rpcapi.shelve_offload_instance(context, instance=instance,
clean_shutdown=clean_shutdown)
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
"""
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.
# 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)
available_zones = availability_zones.get_availability_zones(
context, self.host_api, get_only_available=True)
if availability_zone not in available_zones:
msg = _('The requested availability zone is not available')
raise exception.InvalidRequest(msg)
# NOTE(brinzhang): When specifying a availability zone to unshelve
# a shelved offloaded server, and conf cross_az_attach=False, need
# to determine if attached volume AZ matches the user-specified AZ.
if not CONF.cinder.cross_az_attach:
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
context, instance.uuid)
for bdm in bdms:
if bdm.is_volume and bdm.volume_id:
volume = self.volume_api.get(context, bdm.volume_id)
if availability_zone != volume['availability_zone']:
msg = _("The specified availability zone does not "
"match the volume %(vol_id)s attached to the "
"server. Specified availability zone is "
"%(az)s. Volume is in %(vol_zone)s.") % {
"vol_id": volume['id'],
"az": availability_zone,
"vol_zone": volume['availability_zone']}
raise exception.MismatchVolumeAZException(reason=msg)
@check_instance_lock
@check_instance_state(vm_state=[vm_states.SHELVED,
vm_states.SHELVED_OFFLOADED])
def unshelve(self, context, instance):
def unshelve(self, context, instance, new_az=None):
"""Restore a shelved instance."""
request_spec = objects.RequestSpec.get_by_instance_uuid(
context, instance.uuid)
if new_az:
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()
instance.task_state = task_states.UNSHELVING
instance.save(expected_task_state=[None])

View File

@ -1863,6 +1863,19 @@ class UnshelveException(NovaException):
msg_fmt = _("Error during unshelve instance %(instance_id)s: %(reason)s")
class MismatchVolumeAZException(Invalid):
msg_fmt = _("The availability zone between the server and its attached "
"volumes do not match: %(reason)s.")
code = 409
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.')
code = 409
class ImageVCPULimitsRangeExceeded(Invalid):
msg_fmt = _('Image vCPU topology limits (sockets=%(image_sockets)d, '
'cores=%(image_cores)d, threads=%(image_threads)d) exceeds '

View File

@ -0,0 +1,3 @@
{
"%(action)s": null
}

View File

@ -0,0 +1,3 @@
{
"%(action)s": null
}

View File

@ -0,0 +1,5 @@
{
"%(action)s": {
"availability_zone": "%(availability_zone)s"
}
}

View File

@ -47,3 +47,29 @@ class ShelveJsonTest(test_servers.ServersSampleBase):
uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve')
self._test_server_action(uuid, 'os-unshelve', 'unshelve')
class UnshelveJson277Test(test_servers.ServersSampleBase):
sample_dir = "os-shelve"
microversion = '2.77'
scenarios = [('v2_77', {'api_major_version': 'v2.1'})]
USE_NEUTRON = True
def _test_server_action(self, uuid, template, action, subs=None):
subs = subs or {}
subs.update({'action': action})
response = self._do_post('servers/%s/action' % uuid,
template, subs)
self.assertEqual(202, response.status_code)
self.assertEqual("", response.text)
def test_unshelve_with_az(self):
uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve')
self._test_server_action(uuid, 'os-unshelve', 'unshelve',
subs={"availability_zone": "us-west"})
def test_unshelve_no_az(self):
uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve')
self._test_server_action(uuid, 'os-unshelve-null', 'unshelve')

View File

@ -2577,7 +2577,7 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase):
source_hostname, source_rp_uuid)
req = {
'unshelve': {}
'unshelve': None
}
self.api.post_server_action(server['id'], req)
self._wait_for_state_change(self.api, server, 'ACTIVE')
@ -2626,7 +2626,7 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase):
self.admin_api.put_service(source_service_id, {'status': 'disabled'})
req = {
'unshelve': {}
'unshelve': None
}
self.api.post_server_action(server['id'], req)
server = self._wait_for_state_change(self.api, server, 'ACTIVE')
@ -2662,7 +2662,7 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase):
self.admin_api.put_service(source_service_id, {'status': 'disabled'})
req = {
'unshelve': {}
'unshelve': None
}
self.api.post_server_action(server['id'], req)
server = self._wait_for_state_change(self.api, server, 'ACTIVE')
@ -3162,7 +3162,7 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase):
binary='nova-compute')[0]['id']
self.admin_api.put_service(source_service_id, {'status': 'disabled'})
req = {'unshelve': {}}
req = {'unshelve': None}
self.api.post_server_action(created_server['id'], req)
new_server = self._wait_for_state_change(
self.api, created_server, 'ACTIVE')
@ -5906,7 +5906,7 @@ class UnsupportedPortResourceRequestBasedSchedulingTest(
ex = self.assertRaises(
client.OpenStackApiException,
self.api.post_server_action, server['id'], {'unshelve': {}})
self.api.post_server_action, server['id'], {'unshelve': None})
self.assertEqual(400, ex.response.status_code)
self.assertIn(
@ -5939,7 +5939,7 @@ class UnsupportedPortResourceRequestBasedSchedulingTest(
# can exist with such a port.
self._add_resource_request_to_a_bound_port(self.neutron.port_1['id'])
self.api.post_server_action(server['id'], {'unshelve': {}})
self.api.post_server_action(server['id'], {'unshelve': None})
self._wait_for_state_change(self.admin_api, server, 'ACTIVE')

View File

@ -14,10 +14,14 @@
import mock
from oslo_policy import policy as oslo_policy
from oslo_serialization import jsonutils
from oslo_utils.fixture import uuidsentinel
import six
import webob
from nova.api.openstack import api_version_request
from nova.api.openstack.compute import shelve as shelve_v21
from nova.compute import vm_states
from nova import exception
from nova import policy
from nova import test
@ -49,7 +53,7 @@ class ShelvePolicyTestV21(test.NoDBTestCase):
self.stub_out('nova.compute.api.API.unshelve',
fakes.fake_actions_to_locked_server)
self.assertRaises(webob.exc.HTTPConflict, self.controller._unshelve,
self.req, uuidsentinel.fake, {})
self.req, uuidsentinel.fake, body={'unshelve': {}})
@mock.patch('nova.api.openstack.common.get_instance')
def test_shelve_offload_locked_server(self, get_instance_mock):
@ -165,7 +169,7 @@ class ShelvePolicyEnforcementV21(test.NoDBTestCase):
policy.set_rules(oslo_policy.Rules.from_dict(rules))
self.assertRaises(exception.Forbidden, self.controller._unshelve,
self.req, uuidsentinel.fake, {})
self.req, uuidsentinel.fake, body={'unshelve': {}})
def test_unshelve_policy_failed(self):
rule_name = "os_compute_api:os-shelve:unshelve"
@ -177,3 +181,118 @@ class ShelvePolicyEnforcementV21(test.NoDBTestCase):
self.assertEqual(
"Policy doesn't allow %s to be performed." % rule_name,
exc.format_message())
class UnshelveServerControllerTestV277(test.NoDBTestCase):
"""Server controller test for microversion 2.77
Add availability_zone parameter to unshelve a shelved-offloaded server of
2.77 microversion.
"""
wsgi_api_version = '2.77'
def setUp(self):
super(UnshelveServerControllerTestV277, self).setUp()
self.controller = shelve_v21.ShelveController()
self.req = fakes.HTTPRequest.blank('/fake/servers/a/action',
use_admin_context=True,
version=self.wsgi_api_version)
# These tests don't care about ports with QoS bandwidth resources.
self.stub_out('nova.api.openstack.common.'
'instance_has_port_with_resource_request',
lambda *a, **kw: False)
def fake_get_instance(self):
ctxt = self.req.environ['nova.context']
return fake_instance.fake_instance_obj(
ctxt, uuid=fakes.FAKE_UUID, vm_state=vm_states.SHELVED_OFFLOADED)
@mock.patch('nova.api.openstack.common.get_instance')
def test_unshelve_with_az_pre_2_77_failed(self, mock_get_instance):
"""Make sure specifying an AZ before microversion 2.77 is ignored."""
instance = self.fake_get_instance()
mock_get_instance.return_value = instance
body = {
'unshelve': {
'availability_zone': 'us-east'
}}
self.req.body = jsonutils.dump_as_bytes(body)
self.req.api_version_request = (api_version_request.
APIVersionRequest('2.76'))
with mock.patch.object(self.controller.compute_api,
'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)
@mock.patch('nova.compute.api.API.unshelve')
@mock.patch('nova.api.openstack.common.get_instance')
def test_unshelve_with_none_pre_2_77_success(
self, mock_get_instance, mock_unshelve):
"""Make sure we can unshelve server with None
before microversion 2.77.
"""
instance = self.fake_get_instance()
mock_get_instance.return_value = instance
body = {'unshelve': None}
self.req.body = jsonutils.dump_as_bytes(body)
self.req.api_version_request = (api_version_request.
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)
@mock.patch('nova.compute.api.API.unshelve')
@mock.patch('nova.api.openstack.common.get_instance')
def test_unshelve_with_empty_dict_with_v2_77_failed(
self, mock_get_instance, mock_unshelve):
"""Make sure we cannot unshelve server with empty dict."""
instance = self.fake_get_instance()
mock_get_instance.return_value = instance
body = {'unshelve': {}}
self.req.body = jsonutils.dump_as_bytes(body)
exc = self.assertRaises(exception.ValidationError,
self.controller._unshelve,
self.req, fakes.FAKE_UUID,
body=body)
self.assertIn("\'availability_zone\' is a required property",
six.text_type(exc))
def test_invalid_az_name_with_int(self):
body = {
'unshelve': {
'availability_zone': 1234
}}
self.req.body = jsonutils.dump_as_bytes(body)
self.assertRaises(exception.ValidationError,
self.controller._unshelve,
self.req, fakes.FAKE_UUID,
body=body)
def test_no_az_value(self):
body = {
'unshelve': {
'availability_zone': None
}}
self.req.body = jsonutils.dump_as_bytes(body)
self.assertRaises(exception.ValidationError,
self.controller._unshelve,
self.req, fakes.FAKE_UUID,
body=body)
def test_unshelve_with_additional_param(self):
body = {
'unshelve': {
'availability_zone': 'us-east',
'additional_param': 1
}}
self.req.body = jsonutils.dump_as_bytes(body)
exc = self.assertRaises(
exception.ValidationError,
self.controller._unshelve, self.req,
fakes.FAKE_UUID, body=body)
self.assertIn("Additional properties are not allowed",
six.text_type(exc))

View File

@ -15,6 +15,7 @@ from oslo_utils import fixture as utils_fixture
from oslo_utils.fixture import uuidsentinel as uuids
from oslo_utils import timeutils
from nova.compute import api as compute_api
from nova.compute import claims
from nova.compute import instance_actions
from nova.compute import power_state
@ -809,8 +810,7 @@ class ShelveComputeAPITestCase(test_compute.BaseTestCase):
for state in invalid_vm_states:
self._test_shelve_offload_invalid_state(state)
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
def test_unshelve(self, get_by_instance_uuid):
def _get_specify_state_instance(self, vm_state):
# Ensure instance can be unshelved.
instance = self._create_fake_instance_obj()
@ -819,9 +819,16 @@ class ShelveComputeAPITestCase(test_compute.BaseTestCase):
self.compute_api.shelve(self.context, instance)
instance.task_state = None
instance.vm_state = vm_states.SHELVED
instance.vm_state = vm_state
instance.save()
return instance
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
def test_unshelve(self, get_by_instance_uuid):
# Ensure instance can be unshelved.
instance = self._get_specify_state_instance(vm_states.SHELVED)
fake_spec = objects.RequestSpec()
get_by_instance_uuid.return_value = fake_spec
with mock.patch.object(self.compute_api.compute_task_api,
@ -834,3 +841,116 @@ class ShelveComputeAPITestCase(test_compute.BaseTestCase):
self.assertEqual(instance.task_state, task_states.UNSHELVING)
db.instance_destroy(self.context, instance['uuid'])
@mock.patch('nova.availability_zones.get_availability_zones',
return_value=['az1', 'az2'])
@mock.patch.object(objects.RequestSpec, 'save')
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
def test_specified_az_ushelve_invalid_request(self,
get_by_instance_uuid,
mock_save,
mock_availability_zones):
# Ensure instance can be unshelved.
instance = self._get_specify_state_instance(
vm_states.SHELVED_OFFLOADED)
new_az = "fake-new-az"
fake_spec = objects.RequestSpec()
fake_spec.availability_zone = "fake-old-az"
get_by_instance_uuid.return_value = fake_spec
exc = self.assertRaises(exception.InvalidRequest,
self.compute_api.unshelve,
self.context, instance, new_az=new_az)
self.assertEqual("The requested availability zone is not available",
exc.format_message())
@mock.patch('nova.availability_zones.get_availability_zones',
return_value=['az1', 'az2'])
@mock.patch.object(objects.RequestSpec, 'save')
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
def test_specified_az_unshelve_invalid_state(self, get_by_instance_uuid,
mock_save,
mock_availability_zones):
# Ensure instance can be unshelved.
instance = self._get_specify_state_instance(vm_states.SHELVED)
new_az = "az1"
fake_spec = objects.RequestSpec()
fake_spec.availability_zone = "fake-old-az"
get_by_instance_uuid.return_value = fake_spec
self.assertRaises(exception.UnshelveInstanceInvalidState,
self.compute_api.unshelve,
self.context, instance, new_az=new_az)
@mock.patch('nova.objects.BlockDeviceMappingList.get_by_instance_uuid',
new_callable=mock.NonCallableMock)
@mock.patch('nova.availability_zones.get_availability_zones')
def test_validate_unshelve_az_cross_az_attach_true(
self, mock_get_azs, mock_get_bdms):
"""Tests a case where the new AZ to unshelve does not match the volume
attached to the server but cross_az_attach=True so it's not an error.
"""
# Ensure instance can be unshelved.
instance = self._create_fake_instance_obj(
params=dict(vm_state=vm_states.SHELVED_OFFLOADED))
new_az = "west_az"
mock_get_azs.return_value = ["west_az", "east_az"]
self.flags(cross_az_attach=True, group='cinder')
self.compute_api._validate_unshelve_az(self.context, instance, new_az)
mock_get_azs.assert_called_once_with(
self.context, self.compute_api.host_api, get_only_available=True)
@mock.patch('nova.volume.cinder.API.get')
@mock.patch('nova.objects.BlockDeviceMappingList.get_by_instance_uuid')
@mock.patch('nova.availability_zones.get_availability_zones')
def test_validate_unshelve_az_cross_az_attach_false(
self, mock_get_azs, mock_get_bdms, mock_get):
"""Tests a case where the new AZ to unshelve does not match the volume
attached to the server and cross_az_attach=False so it's an error.
"""
# Ensure instance can be unshelved.
instance = self._create_fake_instance_obj(
params=dict(vm_state=vm_states.SHELVED_OFFLOADED))
new_az = "west_az"
mock_get_azs.return_value = ["west_az", "east_az"]
bdms = [objects.BlockDeviceMapping(destination_type='volume',
volume_id=uuids.volume_id)]
mock_get_bdms.return_value = bdms
volume = {'id': uuids.volume_id, 'availability_zone': 'east_az'}
mock_get.return_value = volume
self.flags(cross_az_attach=False, group='cinder')
self.assertRaises(exception.MismatchVolumeAZException,
self.compute_api._validate_unshelve_az,
self.context, instance, new_az)
mock_get_azs.assert_called_once_with(
self.context, self.compute_api.host_api, get_only_available=True)
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')
@mock.patch.object(objects.RequestSpec, 'save')
@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.
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
self.compute_api.unshelve(self.context, instance, new_az=new_az)
mock_save.assert_called_once_with()
self.assertEqual(new_az, fake_spec.availability_zone)
mock_validate_unshelve_az.assert_called_once_with(
self.context, instance, new_az)

View File

@ -0,0 +1,14 @@
---
features:
- |
Microversion 2.77 adds the optional parameter ``availability_zone`` to
the ``unshelve`` server action API.
* Specifying an availability zone is only allowed when the server status
is ``SHELVED_OFFLOADED`` otherwise a 409 HTTPConflict response is
returned.
* If the ``[cinder]/cross_az_attach`` configuration option is False then
the specified availability zone has to be the same as the availability
zone of any volumes attached to the shelved offloaded server, otherwise
a 409 HTTPConflict error response is returned.