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:
parent
912a46c9d4
commit
27b6c18c66
@ -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.
|
||||
|
@ -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
|
||||
--------
|
||||
|
3
doc/api_samples/os-shelve/v2.77/os-shelve.json
Normal file
3
doc/api_samples/os-shelve/v2.77/os-shelve.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"shelve": null
|
||||
}
|
3
doc/api_samples/os-shelve/v2.77/os-unshelve-null.json
Normal file
3
doc/api_samples/os-shelve/v2.77/os-unshelve-null.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"unshelve": null
|
||||
}
|
5
doc/api_samples/os-shelve/v2.77/os-unshelve.json
Normal file
5
doc/api_samples/os-shelve/v2.77/os-unshelve.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"unshelve": {
|
||||
"availability_zone": "us-west"
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.76",
|
||||
"version": "2.77",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.76",
|
||||
"version": "2.77",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -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>`
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
37
nova/api/openstack/compute/schemas/shelve.py
Normal file
37
nova/api/openstack/compute/schemas/shelve.py
Normal 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,
|
||||
}
|
@ -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())
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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 '
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"%(action)s": null
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"%(action)s": null
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"%(action)s": {
|
||||
"availability_zone": "%(availability_zone)s"
|
||||
}
|
||||
}
|
@ -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')
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user