Allow unshelve to a specific host (REST API part)

This adds support to the REST API, in a new microversion, for specifying
a destination host to unshelve server action when the server
is shelved offloaded.
This patch also supports the ability to unpin the availability_zone of an
instance that is bound to it.

Note that the functional test changes are due to those tests using the
"latest" microversion 2.91.

Implements: blueprint unshelve-to-host
Change-Id: I9e95428c208582741e6cd99bd3260d6742fcc6b7
This commit is contained in:
René Ribaud
2022-06-13 15:22:43 +02:00
parent a263fa46f8
commit 09239fc2ea
30 changed files with 1318 additions and 57 deletions

View File

@@ -1858,8 +1858,11 @@ availability_zone_state:
availability_zone_unshelve: availability_zone_unshelve:
description: | description: |
The availability zone name. Specifying an availability zone is only The availability zone name. Specifying an availability zone is only
allowed when the server status is ``SHELVED_OFFLOADED`` otherwise a allowed when the server status is ``SHELVED_OFFLOADED`` otherwise
409 HTTPConflict response is returned. HTTP 409 conflict response is returned.
Since microversion 2.91 ``"availability_zone":null`` allows unpinning the
instance from any availability_zone it is pinned to.
in: body in: body
required: false required: false
type: string type: string
@@ -3690,6 +3693,15 @@ host_status_update_rebuild:
required: false required: false
type: string type: string
min_version: 2.75 min_version: 2.75
host_unshelve:
description: |
The destination host name. Specifying a destination host is by default only
allowed to project_admin, if it not the case HTTP 403 forbidden response
is returned.
in: body
required: false
type: string
min_version: 2.91
host_zone: host_zone:
description: | description: |
The available zone of the host. The available zone of the host.

View File

@@ -121,9 +121,65 @@ Policy defaults enable only users with the administrative role or the owner of t
**Preconditions** **Preconditions**
The server status must be ``SHELVED`` or ``SHELVED_OFFLOADED``. Unshelving a server without parameters requires its status to be ``SHELVED`` or ``SHELVED_OFFLOADED``.
Unshelving a server with availability_zone and/or host parameters requires its status to be only ``SHELVED_OFFLOADED`` otherwise HTTP 409 conflict response is returned.
If a server is locked, you must have administrator privileges to unshelve the server.
As of ``microversion 2.91``, you can unshelve to a specific compute node if you have PROJECT_ADMIN privileges.
This microversion also gives the ability to pin a server to an availability_zone and to unpin a server
from any availability_zone.
When a server is pinned to an availability_zone, the server move operations will keep the server in that
availability_zone. However, when the server is not pinned to any availability_zone, the move operations can
move the server to nodes in different availability_zones.
The behavior according to unshelve parameters will follow the below table.
+----------+---------------------------+----------+--------------------------------+
| Boot | AZ (1) | Host (1) | Result |
+==========+===========================+==========+================================+
| No AZ | No AZ or AZ=null | No | Free scheduling (2) |
+----------+---------------------------+----------+--------------------------------+
| No AZ | No AZ or AZ=null | Host1 | Schedule to Host1. |
| | | | Server remains unpinned. |
+----------+---------------------------+----------+--------------------------------+
| No AZ | AZ="AZ1" | No | Schedule to any host in "AZ1". |
| | | | Server is pined to "AZ1". |
+----------+---------------------------+----------+--------------------------------+
| No AZ | AZ="AZ1" | Host1 | Verify Host1 is in "AZ1", |
| | | | then schedule to Host1, |
| | | | otherwise reject the request. |
| | | | Server is pined to "AZ1". |
+----------+---------------------------+----------+--------------------------------+
| AZ1 | No AZ | No | Schedule to any host in "AZ1". |
| | | | Server remains pined to "AZ1". |
+----------+---------------------------+----------+--------------------------------+
| AZ1 | AZ=null | No | Free scheduling (2). |
| | | | Server is unpinned. |
+----------+---------------------------+----------+--------------------------------+
| AZ1 | No AZ | Host1 | Verify Host1 is in "AZ1", |
| | | | then schedule to Host1, |
| | | | otherwise reject the request. |
| | | | Server remains pined to "AZ1". |
+----------+---------------------------+----------+--------------------------------+
| AZ1 | AZ=null | Host1 | Schedule to Host1. |
| | | | Server is unpinned. |
+----------+---------------------------+----------+--------------------------------+
| AZ1 | AZ="AZ2" | No | Schedule to any host in "AZ2". |
| | | | Server is pined to "AZ2". |
+----------+---------------------------+----------+--------------------------------+
| AZ1 | AZ="AZ2" | Host1 | Verify Host1 is in "AZ2" then |
| | | | schedule to Host1, |
| | | | otherwise reject the request. |
| | | | Server is pined to "AZ2". |
+----------+---------------------------+----------+--------------------------------+
(1) Unshelve body parameters
(2) Schedule to any host available.
If the server is locked, you must have administrator privileges to unshelve the server.
**Asynchronous Postconditions** **Asynchronous Postconditions**
@@ -147,11 +203,30 @@ Request
{"unshelve": null} or {"unshelve": {"availability_zone": <string>}}. {"unshelve": null} or {"unshelve": {"availability_zone": <string>}}.
A request body of {"unshelve": {}} is not allowed. A request body of {"unshelve": {}} is not allowed.
.. note:: Since microversion 2.91, allowed request body schema are
- {"unshelve": null} (Keep compatibility with previous microversions)
or
- {"unshelve": {"availability_zone": <string>}} (Unshelve and pin server to availability_zone)
- {"unshelve": {"availability_zone": null}} (Unshelve and unpin server from any availability zone)
- {"unshelve": {"host": <fqdn>}}
- {"unshelve": {"availability_zone": <string>, "host": <fqdn>}}
- {"unshelve": {"availability_zone": null, "host": <fqdn>}}
Everything else is not allowed, examples:
- {"unshelve": {}}
- {"unshelve": {"host": <fqdn>, "host": <fqdn>}}
- {"unshelve": {"foo": <string>}}
.. rest_parameters:: parameters.yaml .. rest_parameters:: parameters.yaml
- server_id: server_id_path - server_id: server_id_path
- unshelve: unshelve - unshelve: unshelve
- availability_zone: availability_zone_unshelve - availability_zone: availability_zone_unshelve
- host: host_unshelve
| |
@@ -162,9 +237,22 @@ Request
**Example Unshelve server (unshelve Action) (v2.77)** **Example Unshelve server (unshelve Action) (v2.77)**
.. literalinclude:: ../../doc/api_samples/os-shelve/v2.77/os-unshelve.json .. literalinclude:: ../../doc/api_samples/os-shelve/v2.77/os-unshelve-az.json
:language: javascript :language: javascript
**Examples Unshelve server (unshelve Action) (v2.91)**
.. literalinclude:: ../../doc/api_samples/os-shelve/v2.91/os-unshelve-host.json
:language: javascript
.. literalinclude:: ../../doc/api_samples/os-shelve/v2.91/os-unshelve-az-host.json
:language: javascript
.. literalinclude:: ../../doc/api_samples/os-shelve/v2.91/os-unshelve-host-and-unpin-az.json
:language: javascript
.. literalinclude:: ../../doc/api_samples/os-shelve/v2.91/os-unshelve-unpin-az.json
:language: javascript
Response Response
-------- --------

View File

@@ -0,0 +1,6 @@
{
"unshelve": {
"availability_zone": "nova",
"host": "host01"
}
}

View File

@@ -0,0 +1,6 @@
{
"unshelve": {
"availability_zone": null,
"host": "host01"
}
}

View File

@@ -0,0 +1,5 @@
{
"unshelve": {
"host": "host01"
}
}

View File

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

View File

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

View File

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

View File

@@ -247,6 +247,8 @@ REST_API_VERSION_HISTORY = """REST API Version History:
updating or rebuilding an instance. The updating or rebuilding an instance. The
``OS-EXT-SRV-ATTR:hostname`` attribute is now returned in various ``OS-EXT-SRV-ATTR:hostname`` attribute is now returned in various
server responses regardless of policy configuration. server responses regardless of policy configuration.
* 2.91 - Add support to unshelve instance to a specific host and
to pin/unpin AZ.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
@@ -255,7 +257,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = '2.1' _MIN_API_VERSION = '2.1'
_MAX_API_VERSION = '2.90' _MAX_API_VERSION = '2.91'
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal # Almost all proxy APIs which are related to network, images and baremetal

View File

@@ -1202,3 +1202,12 @@ hostname based on the display name.
In addition, the ``OS-EXT-SRV-ATTR:hostname`` field for all server In addition, the ``OS-EXT-SRV-ATTR:hostname`` field for all server
responses is now visible to all users. Previously this was an admin-only field. responses is now visible to all users. Previously this was an admin-only field.
.. _microversion 2.91:
2.91
----
Add support to unshelve instance to a specific host.
Add support to pin a server to an availability zone or unpin a server from any availability zone.

View File

@@ -15,7 +15,7 @@
from nova.api.validation import parameter_types from nova.api.validation import parameter_types
# NOTE(brinzhang): For older microversion there will be no change as # NOTE(brinzhang): For older microversion there will be no change as
# schema is applied only for >2.77 with unshelve a server API. # schema is applied only for version < 2.91 with unshelve a server API.
# Anything working in old version keep working as it is. # Anything working in old version keep working as it is.
unshelve_v277 = { unshelve_v277 = {
'type': 'object', 'type': 'object',
@@ -35,3 +35,55 @@ unshelve_v277 = {
'required': ['unshelve'], 'required': ['unshelve'],
'additionalProperties': False, 'additionalProperties': False,
} }
# NOTE(rribaud):
# schema is applied only for version >= 2.91 with unshelve a server API.
# Add host parameter to specify to unshelve to this specific host.
#
# Schema has been redefined for better clarity instead of extend 2.77.
#
# API can be called with the following body:
#
# - {"unshelve": null} (Keep compatibility with previous microversions)
#
# or
#
# - {"unshelve": {"availability_zone": <string>}}
# - {"unshelve": {"availability_zone": null}} (Unpin availability zone)
# - {"unshelve": {"host": <fqdn>}}
# - {"unshelve": {"availability_zone": <string>, "host": <fqdn>}}
# - {"unshelve": {"availability_zone": null, "host": <fqdn>}}
#
#
# Everything else is not allowed, examples:
#
# - {"unshelve": {}}
# - {"unshelve": {"host": <fqdn>, "host": <fqdn>}}
# - {"unshelve": {"foo": <string>}}
unshelve_v291 = {
"type": "object",
"properties": {
"unshelve": {
"oneOf": [
{
"type": ["object"],
"properties": {
"availability_zone": {
"oneOf": [
{"type": ["null"]},
{"type": "string"}]
},
"host": {
"type": "string"
}
},
"additionalProperties": False
},
{"type": ["null"]}
]
}
},
"required": ["unshelve"],
"additionalProperties": False
}

View File

@@ -68,7 +68,6 @@ class ShelveController(wsgi.Controller):
context.can(shelve_policies.POLICY_ROOT % 'shelve_offload', context.can(shelve_policies.POLICY_ROOT % 'shelve_offload',
target={'user_id': instance.user_id, target={'user_id': instance.user_id,
'project_id': instance.project_id}) 'project_id': instance.project_id})
try: try:
self.compute_api.shelve_offload(context, instance) self.compute_api.shelve_offload(context, instance)
except exception.InstanceIsLocked as e: except exception.InstanceIsLocked as e:
@@ -87,33 +86,59 @@ class ShelveController(wsgi.Controller):
# In microversion 2.77 we support specifying 'availability_zone' to # In microversion 2.77 we support specifying 'availability_zone' to
# unshelve a server. But before 2.77 there is no request body # unshelve a server. But before 2.77 there is no request body
# schema validation (because of body=null). # schema validation (because of body=null).
@validation.schema(shelve_schemas.unshelve_v277, min_version='2.77') @validation.schema(
shelve_schemas.unshelve_v277,
min_version='2.77',
max_version='2.90'
)
# In microversion 2.91 we support specifying 'host' to
# unshelve an instance to a specific hostself.
# 'availability_zone' = None is supported as well to unpin the
# availability zone of an instance bonded to this availability_zone
@validation.schema(shelve_schemas.unshelve_v291, min_version='2.91')
def _unshelve(self, req, id, body): def _unshelve(self, req, id, body):
"""Restore an instance from shelved mode.""" """Restore an instance from shelved mode."""
context = req.environ["nova.context"] context = req.environ["nova.context"]
instance = common.get_instance(self.compute_api, context, id) instance = common.get_instance(self.compute_api, context, id)
context.can(shelve_policies.POLICY_ROOT % 'unshelve', context.can(
target={'project_id': instance.project_id}) shelve_policies.POLICY_ROOT % 'unshelve',
target={'project_id': instance.project_id}
)
unshelve_args = {} unshelve_args = {}
unshelve_dict = body['unshelve'] unshelve_dict = body.get('unshelve')
support_az = api_version_request.is_supported(req, '2.77') support_az = api_version_request.is_supported(
if support_az and unshelve_dict: req, '2.77')
unshelve_args['new_az'] = unshelve_dict['availability_zone'] support_host = api_version_request.is_supported(
req, '2.91')
if unshelve_dict:
if support_az and 'availability_zone' in unshelve_dict:
unshelve_args['new_az'] = (
unshelve_dict['availability_zone']
)
if support_host:
unshelve_args['host'] = unshelve_dict.get('host')
try: try:
self.compute_api.unshelve(context, instance, **unshelve_args) self.compute_api.unshelve(
except (exception.InstanceIsLocked, context,
instance,
**unshelve_args,
)
except (
exception.InstanceIsLocked,
exception.UnshelveInstanceInvalidState, exception.UnshelveInstanceInvalidState,
exception.MismatchVolumeAZException) as e: exception.UnshelveHostNotInAZ,
exception.MismatchVolumeAZException,
) as e:
raise exc.HTTPConflict(explanation=e.format_message()) raise exc.HTTPConflict(explanation=e.format_message())
except exception.InstanceInvalidState as state_error: except exception.InstanceInvalidState as state_error:
common.raise_http_conflict_for_instance_invalid_state(state_error, common.raise_http_conflict_for_instance_invalid_state(
'unshelve', state_error, 'unshelve', id)
id)
except ( except (
exception.InvalidRequest, exception.InvalidRequest,
exception.ExtendedResourceRequestOldCompute, exception.ExtendedResourceRequestOldCompute,
exception.ComputeHostNotFound,
) as e: ) as e:
raise exc.HTTPBadRequest(explanation=e.format_message()) raise exc.HTTPBadRequest(explanation=e.format_message())

View File

@@ -75,6 +75,7 @@ from nova.objects import quotas as quotas_obj
from nova.objects import service as service_obj from nova.objects import service as service_obj
from nova.pci import request as pci_request from nova.pci import request as pci_request
from nova.policies import servers as servers_policies from nova.policies import servers as servers_policies
from nova.policies import shelve as shelve_policies
import nova.policy import nova.policy
from nova import profiler from nova import profiler
from nova import rpc from nova import rpc
@@ -4504,6 +4505,14 @@ class API:
# host is requested, so we have to see if it exists and does not # host is requested, so we have to see if it exists and does not
# contradict with the AZ of the instance # contradict with the AZ of the instance
if host: if host:
# Make sure only admin can unshelve to a specific host.
context.can(
shelve_policies.POLICY_ROOT % 'unshelve_to_host',
target={
'user_id': instance.user_id,
'project_id': instance.project_id
}
)
# Ensure that the requested host exists otherwise raise # Ensure that the requested host exists otherwise raise
# a ComputeHostNotFound exception # a ComputeHostNotFound exception
objects.ComputeNode.get_first_node_by_host_for_old_compat( objects.ComputeNode.get_first_node_by_host_for_old_compat(

View File

@@ -44,6 +44,18 @@ shelve_policies = [
} }
], ],
scope_types=['project']), scope_types=['project']),
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'unshelve_to_host',
check_str=base.PROJECT_ADMIN,
description="Unshelve (restore) shelve offloaded server to a "
"specific host",
operations=[
{
'method': 'POST',
'path': '/servers/{server_id}/action (unshelve)'
}
],
scope_types=['project']),
policy.DocumentedRuleDefault( policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'shelve_offload', name=POLICY_ROOT % 'shelve_offload',
check_str=base.PROJECT_ADMIN, check_str=base.PROJECT_ADMIN,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,10 +15,25 @@
import nova.conf import nova.conf
from nova import objects
from nova.tests.functional.api_sample_tests import test_servers from nova.tests.functional.api_sample_tests import test_servers
from oslo_utils.fixture import uuidsentinel
from unittest import mock
CONF = nova.conf.CONF CONF = nova.conf.CONF
fake_aggregate = {
'deleted': 0,
'deleted_at': None,
'created_at': None,
'updated_at': None,
'id': 123,
'uuid': uuidsentinel.fake_aggregate,
'name': 'us-west',
'hosts': ['host01'],
'metadetails': {'availability_zone': 'us-west'},
}
class ShelveJsonTest(test_servers.ServersSampleBase): class ShelveJsonTest(test_servers.ServersSampleBase):
# The 'os_compute_api:os-shelve:shelve_offload' policy is admin-only # The 'os_compute_api:os-shelve:shelve_offload' policy is admin-only
@@ -30,9 +45,11 @@ class ShelveJsonTest(test_servers.ServersSampleBase):
# Don't offload instance, so we can test the offload call. # Don't offload instance, so we can test the offload call.
CONF.set_override('shelved_offload_time', -1) CONF.set_override('shelved_offload_time', -1)
def _test_server_action(self, uuid, template, action): 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, response = self._do_post('servers/%s/action' % uuid,
template, {'action': action}) template, subs)
self.assertEqual(202, response.status_code) self.assertEqual(202, response.status_code)
self.assertEqual("", response.text) self.assertEqual("", response.text)
@@ -51,26 +68,288 @@ class ShelveJsonTest(test_servers.ServersSampleBase):
self._test_server_action(uuid, 'os-unshelve', 'unshelve') self._test_server_action(uuid, 'os-unshelve', 'unshelve')
class UnshelveJson277Test(test_servers.ServersSampleBase): class UnshelveJson277Test(ShelveJsonTest):
ADMIN_API = False
sample_dir = "os-shelve" sample_dir = "os-shelve"
microversion = '2.77' microversion = '2.77'
scenarios = [('v2_77', {'api_major_version': 'v2.1'})] scenarios = [('v2_77', {'api_major_version': 'v2.1'})]
def setUp(self):
super(UnshelveJson277Test, self).setUp()
# Almost all next tests require the instance to be shelve offloaded.
# So shelve offload the instance and skip the shelve_offload_test
# below.
CONF.set_override('shelved_offload_time', 0)
def test_shelve_offload(self):
# Skip this test as the instance is already shelve offloaded.
pass
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-az',
'unshelve',
subs={"availability_zone": "us-west"}
)
class UnshelveJson291Test(UnshelveJson277Test):
ADMIN_API = True
sample_dir = "os-shelve"
microversion = '2.91'
scenarios = [('v2_91', {'api_major_version': 'v2.1'})]
def _test_server_action_invalid(
self, uuid, template, action, subs=None, msg=None):
subs = subs or {}
subs.update({'action': action})
response = self._do_post('servers/%s/action' % uuid,
template, subs)
self.assertEqual(400, response.status_code)
self.assertIn(msg, response.text)
def test_unshelve_with_non_valid_host(self):
"""Ensure an exception rise if host is invalid and
a http 400 error
"""
uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve')
self._test_server_action_invalid(
uuid, 'os-unshelve-host',
'unshelve',
subs={'host': 'host01'},
msg='Compute host host01 could not be found.')
@mock.patch('nova.objects.aggregate._get_by_host_from_db')
@mock.patch('nova.objects.ComputeNodeList.get_all_by_host')
def test_unshelve_with_valid_host(
self, compute_node_get_all_by_host, mock_api_get_by_host):
"""Ensure we can unshelve to a host
"""
# Put compute in the correct az
mock_api_get_by_host.return_value = [fake_aggregate]
uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve')
fake_computes = objects.ComputeNodeList(
objects=[
objects.ComputeNode(
host='host01',
uuid=uuidsentinel.host1,
hypervisor_hostname='host01')
]
)
compute_node_get_all_by_host.return_value = fake_computes
self._test_server_action(
uuid,
'os-unshelve-host',
'unshelve',
subs={'host': 'host01'}
)
@mock.patch('nova.objects.aggregate._get_by_host_from_db')
@mock.patch('nova.objects.ComputeNodeList.get_all_by_host')
def test_unshelve_with_az_and_host(
self, compute_node_get_all_by_host, mock_api_get_by_host):
"""Ensure we can unshelve to a host and az
"""
# Put compute in the correct az
mock_api_get_by_host.return_value = [fake_aggregate]
uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve')
fake_computes = objects.ComputeNodeList(
objects=[
objects.ComputeNode(
host='host01',
uuid=uuidsentinel.host1,
hypervisor_hostname='host01')
]
)
compute_node_get_all_by_host.return_value = fake_computes
self._test_server_action(
uuid,
'os-unshelve-host',
'unshelve',
subs={'host': 'host01', 'availability_zone': 'us-west'},
)
@mock.patch('nova.objects.aggregate._get_by_host_from_db')
@mock.patch('nova.objects.ComputeNodeList.get_all_by_host')
def test_unshelve_with_unpin_az_and_host(
self, compute_node_get_all_by_host, mock_api_get_by_host):
"""Ensure we can unshelve to a host and az
"""
# Put compute in the correct az
mock_api_get_by_host.return_value = [fake_aggregate]
uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve')
fake_computes = objects.ComputeNodeList(
objects=[
objects.ComputeNode(
host='host01',
uuid=uuidsentinel.host1,
hypervisor_hostname='host01')
]
)
compute_node_get_all_by_host.return_value = fake_computes
self._test_server_action(
uuid,
'os-unshelve-host-and-unpin-az',
'unshelve',
subs={'host': 'host01'},
)
@mock.patch('nova.objects.aggregate._get_by_host_from_db')
@mock.patch('nova.objects.ComputeNodeList.get_all_by_host')
def test_unshelve_with_unpin_az(
self, compute_node_get_all_by_host, mock_api_get_by_host):
"""Ensure we can unpin an az
"""
# Put compute in the correct az
mock_api_get_by_host.return_value = [fake_aggregate]
uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve')
fake_computes = objects.ComputeNodeList(
objects=[
objects.ComputeNode(
host='host01',
uuid=uuidsentinel.host1,
hypervisor_hostname='host01')
]
)
compute_node_get_all_by_host.return_value = fake_computes
self._test_server_action(
uuid,
'os-unshelve-unpin-az',
'unshelve',
subs={'host': 'host01'},
)
class UnshelveJson291NonAdminTest(UnshelveJson291Test):
# Use non admin api credentials.
ADMIN_API = False
sample_dir = "os-shelve"
microversion = '2.91'
scenarios = [('v2_91', {'api_major_version': 'v2.1'})]
def _test_server_action_invalid(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(403, response.status_code)
self.assertIn(
"Policy doesn\'t allow os_compute_api:os-shelve:unshelve_to_host" +
" to be performed.", response.text)
def _test_server_action(self, uuid, template, action, subs=None): def _test_server_action(self, uuid, template, action, subs=None):
subs = subs or {} subs = subs or {}
subs.update({'action': action}) subs.update({'action': action})
response = self._do_post('servers/%s/action' % uuid, response = self._do_post('servers/%s/action' % uuid,
template, subs) template, subs)
self.assertEqual(202, response.status_code) self.assertEqual(202, response.status_code)
self.assertEqual("", response.text) self.assertEqual('', response.text)
def test_unshelve_with_az(self): def test_unshelve_with_non_valid_host(self):
"""Ensure an exception rise if user is not admin.
a http 403 error
"""
uuid = self._post_server() uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve') self._test_server_action(uuid, 'os-shelve', 'shelve')
self._test_server_action(uuid, 'os-unshelve', 'unshelve', self._test_server_action_invalid(
subs={"availability_zone": "us-west"}) uuid,
'os-unshelve-host',
'unshelve',
subs={'host': 'host01'}
)
@mock.patch('nova.objects.aggregate._get_by_host_from_db')
@mock.patch('nova.objects.ComputeNodeList.get_all_by_host')
def test_unshelve_with_unpin_az_and_host(
self, compute_node_get_all_by_host, mock_api_get_by_host):
# Put compute in the correct az
mock_api_get_by_host.return_value = [fake_aggregate]
def test_unshelve_no_az(self):
uuid = self._post_server() uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve') self._test_server_action(uuid, 'os-shelve', 'shelve')
self._test_server_action(uuid, 'os-unshelve-null', 'unshelve') fake_computes = objects.ComputeNodeList(
objects=[
objects.ComputeNode(
host='host01',
uuid=uuidsentinel.host1,
hypervisor_hostname='host01')
]
)
compute_node_get_all_by_host.return_value = fake_computes
self._test_server_action_invalid(
uuid,
'os-unshelve-host-and-unpin-az',
'unshelve',
subs={'host': 'host01'},
)
@mock.patch('nova.objects.aggregate._get_by_host_from_db')
@mock.patch('nova.objects.ComputeNodeList.get_all_by_host')
def test_unshelve_with_valid_host(
self, compute_node_get_all_by_host, mock_api_get_by_host):
# Put compute in the correct az
mock_api_get_by_host.return_value = [fake_aggregate]
uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve')
fake_computes = objects.ComputeNodeList(
objects=[
objects.ComputeNode(
host='host01',
uuid=uuidsentinel.host1,
hypervisor_hostname='host01')
]
)
compute_node_get_all_by_host.return_value = fake_computes
self._test_server_action_invalid(
uuid,
'os-unshelve-host',
'unshelve',
subs={'host': 'host01'}
)
@mock.patch('nova.objects.aggregate._get_by_host_from_db')
@mock.patch('nova.objects.ComputeNodeList.get_all_by_host')
def test_unshelve_with_az_and_host(
self, compute_node_get_all_by_host, mock_api_get_by_host):
"""Ensure we can unshelve to a host and az
"""
# Put compute in the correct az
mock_api_get_by_host.return_value = [fake_aggregate]
uuid = self._post_server()
self._test_server_action(uuid, 'os-shelve', 'shelve')
fake_computes = objects.ComputeNodeList(
objects=[
objects.ComputeNode(
host='host01',
uuid=uuidsentinel.host1,
hypervisor_hostname='host01')
]
)
compute_node_get_all_by_host.return_value = fake_computes
self._test_server_action_invalid(
uuid,
'os-unshelve-host',
'unshelve',
subs={'host': 'host01', 'availability_zone': 'us-west'},
)

View File

@@ -10,12 +10,16 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from nova.api.openstack.compute import hosts
from nova.compute import instance_actions
from nova import context from nova import context
from nova import objects from nova import objects
from nova import test from nova import test
from nova.tests import fixtures as nova_fixtures from nova.tests import fixtures as nova_fixtures
from nova.tests.functional.api import client as api_client
from nova.tests.functional import fixtures as func_fixtures from nova.tests.functional import fixtures as func_fixtures
from nova.tests.functional import integrated_helpers from nova.tests.functional import integrated_helpers
from nova.tests.unit.api.openstack import fakes
class TestAvailabilityZoneScheduling( class TestAvailabilityZoneScheduling(
@@ -36,6 +40,9 @@ class TestAvailabilityZoneScheduling(
self.api = api_fixture.admin_api self.api = api_fixture.admin_api
self.api.microversion = 'latest' self.api.microversion = 'latest'
self.controller = hosts.HostController()
self.req = fakes.HTTPRequest.blank('', use_admin_context=True)
self.start_service('conductor') self.start_service('conductor')
self.start_service('scheduler') self.start_service('scheduler')
@@ -68,18 +75,18 @@ class TestAvailabilityZoneScheduling(
self.api.api_post( self.api.api_post(
'/os-aggregates/%s/action' % aggregate['id'], add_host_body) '/os-aggregates/%s/action' % aggregate['id'], add_host_body)
def _create_server(self, name): def _create_server(self, name, zone=None):
# Create a server, it doesn't matter which host it ends up in. # Create a server, it doesn't matter which host it ends up in.
server = super(TestAvailabilityZoneScheduling, self)._create_server( server = super(TestAvailabilityZoneScheduling, self)._create_server(
flavor_id=self.flavor1, flavor_id=self.flavor1,
networks='none',) networks='none',
original_host = server['OS-EXT-SRV-ATTR:host'] az=zone,
# Assert the server has the AZ set (not None or 'nova'). )
expected_zone = 'zone1' if original_host == 'host1' else 'zone2'
self.assertEqual(expected_zone, server['OS-EXT-AZ:availability_zone'])
return server return server
def _assert_instance_az(self, server, expected_zone): def _assert_instance_az_and_host(
self, server, expected_zone, expected_host=None):
# Check AZ
# Check the API. # Check the API.
self.assertEqual(expected_zone, server['OS-EXT-AZ:availability_zone']) self.assertEqual(expected_zone, server['OS-EXT-AZ:availability_zone'])
# Check the DB. # Check the DB.
@@ -88,6 +95,51 @@ class TestAvailabilityZoneScheduling(
ctxt, self.cell_mappings[test.CELL1_NAME]) as cctxt: ctxt, self.cell_mappings[test.CELL1_NAME]) as cctxt:
instance = objects.Instance.get_by_uuid(cctxt, server['id']) instance = objects.Instance.get_by_uuid(cctxt, server['id'])
self.assertEqual(expected_zone, instance.availability_zone) self.assertEqual(expected_zone, instance.availability_zone)
# Check host
if expected_host:
self.assertEqual(expected_host, server['OS-EXT-SRV-ATTR:host'])
def _assert_request_spec_az(self, ctxt, server, az):
request_spec = objects.RequestSpec.get_by_instance_uuid(
ctxt, server['id'])
self.assertEqual(request_spec.availability_zone, az)
def _assert_server_with_az_unshelved_to_specified_az(self, server, az):
"""Ensure a server with an az constraints is unshelved in the
corresponding az.
"""
host_to_disable = 'host1' if az == 'zone1' else 'host2'
self._shelve_server(server, expected_state='SHELVED_OFFLOADED')
compute_service_id = self.api.get_services(
host=host_to_disable, binary='nova-compute')[0]['id']
self.api.put_service(compute_service_id, {'status': 'disabled'})
req = {
'unshelve': None
}
self.api.post_server_action(server['id'], req)
server = self._wait_for_action_fail_completion(
server, instance_actions.UNSHELVE, 'schedule_instances')
self.assertIn('Error', server['result'])
self.assertIn('No valid host', server['details'])
def _shelve_unshelve_server(self, ctxt, server, req):
self._shelve_server(server, expected_state='SHELVED_OFFLOADED')
self.api.post_server_action(server['id'], req)
server = self._wait_for_server_parameter(
server,
{'status': 'ACTIVE', },
)
return self.api.get_server(server['id'])
def other_az_than(self, az):
return 'zone2' if az == 'zone1' else 'zone1'
def other_host_than(self, host):
return 'host2' if host == 'host1' else 'host1'
def test_live_migrate_implicit_az(self): def test_live_migrate_implicit_az(self):
"""Tests live migration of an instance with an implicit AZ. """Tests live migration of an instance with an implicit AZ.
@@ -111,7 +163,8 @@ class TestAvailabilityZoneScheduling(
still not restricted to its current zone even if it says it is in one. still not restricted to its current zone even if it says it is in one.
""" """
server = self._create_server('test_live_migrate_implicit_az') server = self._create_server('test_live_migrate_implicit_az')
original_host = server['OS-EXT-SRV-ATTR:host'] original_az = server['OS-EXT-AZ:availability_zone']
expected_zone = self.other_az_than(original_az)
# Attempt to live migrate the instance; again, we don't specify a host # Attempt to live migrate the instance; again, we don't specify a host
# because there are only two hosts so the scheduler would only be able # because there are only two hosts so the scheduler would only be able
@@ -132,8 +185,379 @@ class TestAvailabilityZoneScheduling(
# the database because the API will return the AZ from the host # the database because the API will return the AZ from the host
# aggregate if instance.host is not None. # aggregate if instance.host is not None.
server = self.api.get_server(server['id']) server = self.api.get_server(server['id'])
expected_zone = 'zone2' if original_host == 'host1' else 'zone1' self._assert_instance_az_and_host(server, expected_zone)
self._assert_instance_az(server, expected_zone)
def test_create_server(self):
"""Create a server without an AZ constraint and make sure asking a new
request spec will not have the request_spec.availability_zone set.
"""
ctxt = context.get_admin_context()
server = self._create_server('server01')
self._assert_request_spec_az(ctxt, server, None)
def test_create_server_to_zone(self):
"""Create a server with an AZ constraint and make sure asking a new
request spec will have the request_spec.availability_zone to the
required zone.
"""
ctxt = context.get_admin_context()
server = self._create_server('server01', 'zone2')
server = self.api.get_server(server['id'])
self._assert_instance_az_and_host(server, 'zone2')
self._assert_request_spec_az(ctxt, server, 'zone2')
def test_cold_migrate_cross_az(self):
"""Test a cold migration cross AZ.
"""
server = self._create_server('server01')
original_host = server['OS-EXT-SRV-ATTR:host']
original_az = server['OS-EXT-AZ:availability_zone']
expected_host = self.other_host_than(original_host)
expected_zone = self.other_az_than(original_az)
self._migrate_server(server)
self._confirm_resize(server)
server = self.api.get_server(server['id'])
self._assert_instance_az_and_host(server, expected_zone, expected_host)
# 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 |
# +----------+---------------------------+-------+----------------------------+
def test_unshelve_server_without_az_contraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01')
req = {
'unshelve': None
}
self._shelve_unshelve_server(ctxt, server, req)
self._assert_request_spec_az(ctxt, server, None)
def test_unshelve_unpin_az_server_without_az_contraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01')
req = {
'unshelve': {'availability_zone': None}
}
self._shelve_unshelve_server(ctxt, server, req)
self._assert_request_spec_az(ctxt, server, None)
# +----------+---------------------------+-------+----------------------------+
# | No AZ | No AZ or AZ=null | Host1 | Schedule to host1, |
# | | | | reqspec.AZ=None |
# +----------+---------------------------+-------+----------------------------+
def test_unshelve_to_host_server_without_az_contraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01')
original_host = server['OS-EXT-SRV-ATTR:host']
original_az = server['OS-EXT-AZ:availability_zone']
dest_hostname = self.other_host_than(original_host)
expected_zone = self.other_az_than(original_az)
req = {
'unshelve': {'host': dest_hostname}
}
server = self._shelve_unshelve_server(ctxt, server, req)
self._assert_instance_az_and_host(server, expected_zone, dest_hostname)
self._assert_request_spec_az(ctxt, server, None)
def test_unshelve_to_host_and_unpin_server_without_az_contraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01')
original_host = server['OS-EXT-SRV-ATTR:host']
original_az = server['OS-EXT-AZ:availability_zone']
dest_hostname = self.other_host_than(original_host)
expected_zone = self.other_az_than(original_az)
req = {
'unshelve': {
'host': dest_hostname,
'availability_zone': None,
}
}
server = self._shelve_unshelve_server(ctxt, server, req)
self._assert_instance_az_and_host(server, expected_zone, dest_hostname)
self._assert_request_spec_az(ctxt, server, None)
# +----------+---------------------------+-------+----------------------------+
# | No AZ | AZ="AZ1" | No | Schedule to AZ1, |
# | | | | reqspec.AZ="AZ1" |
# +----------+---------------------------+-------+----------------------------+
def test_unshelve_to_az_server_without_az_constraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01')
original_host = server['OS-EXT-SRV-ATTR:host']
original_az = server['OS-EXT-AZ:availability_zone']
dest_hostname = 'host2' if original_host == 'host1' else 'host1'
dest_az = self.other_az_than(original_az)
req = {
'unshelve': {'availability_zone': dest_az}
}
server = self._shelve_unshelve_server(ctxt, server, req)
self._assert_instance_az_and_host(server, dest_az, dest_hostname)
self._assert_request_spec_az(ctxt, server, dest_az)
self._assert_server_with_az_unshelved_to_specified_az(
server, dest_az)
# +----------+---------------------------+-------+----------------------------+
# | No AZ | AZ="AZ1" | Host1 | Verify that host1 in AZ1, |
# | | | | or (3). Schedule to |
# | | | | host1, reqspec.AZ="AZ1" |
# +----------+---------------------------+-------+----------------------------+
def test_unshelve_to_az_and_host_server_without_az_constraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01')
original_host = server['OS-EXT-SRV-ATTR:host']
original_az = server['OS-EXT-AZ:availability_zone']
dest_hostname = 'host2' if original_host == 'host1' else 'host1'
dest_az = self.other_az_than(original_az)
req = {
'unshelve': {'host': dest_hostname, 'availability_zone': dest_az}
}
server = self._shelve_unshelve_server(ctxt, server, req)
self._assert_instance_az_and_host(server, dest_az, dest_hostname)
self._assert_request_spec_az(ctxt, server, dest_az)
self._assert_server_with_az_unshelved_to_specified_az(
server, dest_az)
def test_unshelve_to_wrong_az_and_host_server_without_az_constraint(self):
server = self._create_server('server01')
original_host = server['OS-EXT-SRV-ATTR:host']
original_az = server['OS-EXT-AZ:availability_zone']
dest_hostname = 'host2' if original_host == 'host1' else 'host1'
req = {
'unshelve': {'host': dest_hostname,
'availability_zone': original_az}
}
self._shelve_server(server, expected_state='SHELVED_OFFLOADED')
exc = self.assertRaises(
api_client.OpenStackApiException,
self.api.post_server_action,
server['id'],
req
)
self.assertEqual(409, exc.response.status_code)
self.assertIn(
'Host \\\"{}\\\" is not in the availability zone \\\"{}\\\".'
.format(dest_hostname, original_az),
exc.response.text
)
# +----------+---------------------------+-------+----------------------------+
# | AZ1 | No AZ | No | Schedule to AZ1, |
# | | | | reqspec.AZ="AZ1" |
# +----------+---------------------------+-------+----------------------------+
def test_unshelve_a_server_with_az_contraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01', 'zone2')
req = {
'unshelve': None
}
self._shelve_unshelve_server(ctxt, server, req)
self._assert_request_spec_az(ctxt, server, 'zone2')
self._assert_server_with_az_unshelved_to_specified_az(
server, 'zone2')
# +----------+---------------------------+-------+----------------------------+
# | AZ1 | AZ=null | No | Free scheduling, |
# | | | | reqspec.AZ=None |
# +----------+---------------------------+-------+----------------------------+
def test_unshelve_to_unpin_az_a_server_with_az_constraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01', 'zone2')
req = {
'unshelve': {'availability_zone': None}
}
server = self._shelve_unshelve_server(ctxt, server, req)
self._assert_request_spec_az(ctxt, server, None)
# +----------+---------------------------+-------+----------------------------+
# | AZ1 | No AZ | Host1 | If host1 is in AZ1, |
# | | | | then schedule to host1, |
# | | | | reqspec.AZ="AZ1", otherwise|
# | | | | reject the request (3) |
# +----------+---------------------------+-------+----------------------------+
def test_unshelve_to_host_server_with_az_contraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01', 'zone1')
req = {
'unshelve': {'host': 'host1'}
}
server = self._shelve_unshelve_server(ctxt, server, req)
self._assert_instance_az_and_host(server, 'zone1', 'host1')
self._assert_request_spec_az(ctxt, server, 'zone1')
self._assert_server_with_az_unshelved_to_specified_az(
server, 'zone1')
def test_unshelve_to_host_wrong_az_server_with_az_contraint(self):
server = self._create_server('server01', 'zone1')
req = {
'unshelve': {'host': 'host2'}
}
self._shelve_server(server, expected_state='SHELVED_OFFLOADED')
exc = self.assertRaises(
api_client.OpenStackApiException,
self.api.post_server_action,
server['id'],
req
)
self.assertEqual(409, exc.response.status_code)
self.assertIn(
'Host \\\"host2\\\" is not in the availability '
'zone \\\"zone1\\\".',
exc.response.text
)
# +----------+---------------------------+-------+----------------------------+
# | AZ1 | AZ=null | Host1 | Schedule to host1, |
# | | | | reqspec.AZ=None |
# +----------+---------------------------+-------+----------------------------+
def test_unshelve_to_host_and_unpin_server_with_az_contraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01', 'zone1')
req = {
'unshelve': {'host': 'host2',
'availability_zone': None,
}
}
server = self._shelve_unshelve_server(ctxt, server, req)
self._assert_instance_az_and_host(server, 'zone2', 'host2')
self._assert_request_spec_az(ctxt, server, None)
# +----------+---------------------------+-------+----------------------------+
# | AZ1 | AZ="AZ2" | No | Schedule to AZ2, |
# | | | | reqspec.AZ="AZ2" |
# +----------+---------------------------+-------+----------------------------+
def test_unshelve_to_az_a_server_with_az_constraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01', 'zone1')
req = {
'unshelve': {'availability_zone': 'zone2'}
}
server = self._shelve_unshelve_server(ctxt, server, req)
self._assert_instance_az_and_host(server, 'zone2', 'host2')
self._assert_request_spec_az(ctxt, server, 'zone2')
self._assert_server_with_az_unshelved_to_specified_az(
server, 'zone2')
# +----------+---------------------------+-------+----------------------------+
# | AZ1 | AZ="AZ2" | Host1 | If host1 in AZ2 then |
# | | | | schedule to host1, |
# | | | | reqspec.AZ="AZ2", |
# | | | | otherwise reject (3) |
# +----------+---------------------------+-------+----------------------------+
def test_unshelve_to_host_and_az_a_server_with_az_constraint(self):
ctxt = context.get_admin_context()
server = self._create_server('server01', 'zone1')
req = {
'unshelve': {'host': 'host2',
'availability_zone': 'zone2',
}
}
server = self._shelve_unshelve_server(ctxt, server, req)
self._assert_instance_az_and_host(server, 'zone2', 'host2')
self._assert_request_spec_az(ctxt, server, 'zone2')
self._assert_server_with_az_unshelved_to_specified_az(
server, 'zone2')
def test_unshelve_to_host_and_wrong_az_a_server_with_az_constraint(self):
server = self._create_server('server01', 'zone1')
req = {
'unshelve': {'host': 'host2',
'availability_zone': 'zone1',
}
}
self._shelve_server(server, expected_state='SHELVED_OFFLOADED')
exc = self.assertRaises(
api_client.OpenStackApiException,
self.api.post_server_action,
server['id'],
req
)
self.assertEqual(409, exc.response.status_code)
self.assertIn(
'Host \\\"host2\\\" is not in the availability '
'zone \\\"zone1\\\".',
exc.response.text
)
def test_resize_revert_across_azs(self): def test_resize_revert_across_azs(self):
"""Creates two compute service hosts in separate AZs. Creates a server """Creates two compute service hosts in separate AZs. Creates a server
@@ -152,9 +576,9 @@ class TestAvailabilityZoneScheduling(
# Now the server should be in the other AZ. # Now the server should be in the other AZ.
new_zone = 'zone2' if original_host == 'host1' else 'zone1' new_zone = 'zone2' if original_host == 'host1' else 'zone1'
self._assert_instance_az(server, new_zone) self._assert_instance_az_and_host(server, new_zone)
# Revert the resize and the server should be back in the original AZ. # Revert the resize and the server should be back in the original AZ.
self.api.post_server_action(server['id'], {'revertResize': None}) self.api.post_server_action(server['id'], {'revertResize': None})
server = self._wait_for_state_change(server, 'ACTIVE') server = self._wait_for_state_change(server, 'ACTIVE')
self._assert_instance_az(server, original_az) self._assert_instance_az_and_host(server, original_az)

View File

@@ -2519,6 +2519,57 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase):
self._delete_and_check_allocations(server) self._delete_and_check_allocations(server)
def test_shelve_unshelve_to_host(self):
source_hostname = self.compute1.host
dest_hostname = self.compute2.host
source_rp_uuid = self._get_provider_uuid_by_host(source_hostname)
dest_rp_uuid = \
self._get_provider_uuid_by_host(dest_hostname)
server = self._boot_then_shelve_and_check_allocations(
source_hostname, source_rp_uuid)
self._shelve_offload_and_check_allocations(server, source_rp_uuid)
req = {
'unshelve': {'host': dest_hostname}
}
self.api.post_server_action(server['id'], req)
self._wait_for_server_parameter(
server, {'OS-EXT-SRV-ATTR:host': dest_hostname, 'status': 'ACTIVE'}
)
self.assertFlavorMatchesUsage(dest_rp_uuid, self.flavor1)
# the server has an allocation on only the dest node
self.assertFlavorMatchesAllocation(
self.flavor1, server['id'], dest_rp_uuid)
self._delete_and_check_allocations(server)
def test_shelve_unshelve_to_host_instance_not_offloaded(self):
source_hostname = self.compute1.host
dest_hostname = self.compute2.host
source_rp_uuid = self._get_provider_uuid_by_host(source_hostname)
server = self._boot_then_shelve_and_check_allocations(
source_hostname, source_rp_uuid)
req = {
'unshelve': {'host': dest_hostname}
}
ex = self.assertRaises(
client.OpenStackApiException,
self.api.post_server_action,
server['id'], req
)
self.assertEqual(409, ex.response.status_code)
self.assertIn(
"The server status must be SHELVED_OFFLOADED",
ex.response.text)
def _shelve_offload_and_check_allocations(self, server, source_rp_uuid): def _shelve_offload_and_check_allocations(self, server, source_rp_uuid):
req = { req = {
'shelveOffload': {} 'shelveOffload': {}

View File

@@ -134,10 +134,12 @@ class UnshelveServerControllerTestV277(test.NoDBTestCase):
'availability_zone': 'us-east' 'availability_zone': 'us-east'
}} }}
self.req.body = jsonutils.dump_as_bytes(body) self.req.body = jsonutils.dump_as_bytes(body)
self.req.api_version_request = (api_version_request. self.req.api_version_request = (
APIVersionRequest('2.76')) api_version_request.APIVersionRequest('2.76')
with mock.patch.object(self.controller.compute_api, )
'unshelve') as mock_unshelve: with mock.patch.object(
self.controller.compute_api, 'unshelve'
) as mock_unshelve:
self.controller._unshelve(self.req, fakes.FAKE_UUID, body=body) self.controller._unshelve(self.req, fakes.FAKE_UUID, body=body)
mock_unshelve.assert_called_once_with( mock_unshelve.assert_called_once_with(
self.req.environ['nova.context'], self.req.environ['nova.context'],
@@ -197,10 +199,11 @@ class UnshelveServerControllerTestV277(test.NoDBTestCase):
'availability_zone': None 'availability_zone': None
}} }}
self.req.body = jsonutils.dump_as_bytes(body) self.req.body = jsonutils.dump_as_bytes(body)
self.assertRaises(exception.ValidationError, self.assertRaises(
exception.ValidationError,
self.controller._unshelve, self.controller._unshelve,
self.req, fakes.FAKE_UUID, self.req,
body=body) fakes.FAKE_UUID, body=body)
def test_unshelve_with_additional_param(self): def test_unshelve_with_additional_param(self):
body = { body = {
@@ -214,3 +217,235 @@ class UnshelveServerControllerTestV277(test.NoDBTestCase):
self.controller._unshelve, self.req, self.controller._unshelve, self.req,
fakes.FAKE_UUID, body=body) fakes.FAKE_UUID, body=body)
self.assertIn("Additional properties are not allowed", str(exc)) self.assertIn("Additional properties are not allowed", str(exc))
class UnshelveServerControllerTestV291(test.NoDBTestCase):
"""Server controller test for microversion 2.91
Add host parameter to unshelve a shelved-offloaded server of
2.91 microversion.
"""
wsgi_api_version = '2.91'
def setUp(self):
super(UnshelveServerControllerTestV291, self).setUp()
self.controller = shelve_v21.ShelveController()
self.req = fakes.HTTPRequest.blank(
'/%s/servers/a/action' % fakes.FAKE_PROJECT_ID,
use_admin_context=True, version=self.wsgi_api_version)
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_91(self, mock_get_instance):
"""Make sure specifying an AZ before microversion 2.91
is still working.
"""
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.77'))
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='us-east',
)
@mock.patch('nova.api.openstack.common.get_instance')
def test_unshelve_without_parameters_2_91(self, mock_get_instance):
"""Make sure not specifying parameters with microversion 2.91
is working.
"""
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.91'))
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,
)
@mock.patch('nova.api.openstack.common.get_instance')
def test_unshelve_with_az_2_91(self, mock_get_instance):
"""Make sure specifying an AZ with microversion 2.91
is working.
"""
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.91'))
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='us-east',
host=None,
)
@mock.patch('nova.api.openstack.common.get_instance')
def test_unshelve_with_az_none_2_91(self, mock_get_instance):
"""Make sure specifying an AZ to none (unpin server)
is working.
"""
instance = self.fake_get_instance()
mock_get_instance.return_value = instance
body = {
'unshelve': {
'availability_zone': None,
}}
self.req.body = jsonutils.dump_as_bytes(body)
self.req.api_version_request = (
api_version_request.APIVersionRequest('2.91'))
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,
host=None,
)
@mock.patch('nova.api.openstack.common.get_instance')
def test_unshelve_with_host_2_91(self, mock_get_instance):
"""Make sure specifying a host with microversion 2.91
is working.
"""
instance = self.fake_get_instance()
mock_get_instance.return_value = instance
body = {
'unshelve': {
'host': 'server02',
}}
self.req.body = jsonutils.dump_as_bytes(body)
self.req.api_version_request = (
api_version_request.APIVersionRequest('2.91'))
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,
host='server02',
)
@mock.patch('nova.compute.api.API.unshelve')
@mock.patch('nova.api.openstack.common.get_instance')
def test_unshelve_with_az_and_host_with_v2_91(
self, mock_get_instance, mock_unshelve):
"""Make sure specifying a host and an availability_zone with
microversion 2.91 is working.
"""
instance = self.fake_get_instance()
mock_get_instance.return_value = instance
body = {
'unshelve': {
'availability_zone': 'us-east',
'host': 'server01',
}}
self.req.body = jsonutils.dump_as_bytes(body)
self.req.api_version_request = (
api_version_request.APIVersionRequest('2.91'))
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='us-east',
host='server01',
)
def test_invalid_az_name_with_int(self):
body = {
'unshelve': {
'host': 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': {
'host': 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_invalid_host_fqdn_with_int(self):
body = {
'unshelve': {
'host': 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_host(self):
body = {
'unshelve': {
'host': 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': {
'host': 'server01',
'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("Invalid input for field/attribute unshelve.", str(exc))

View File

@@ -358,6 +358,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
"os_compute_api:os-services:update", "os_compute_api:os-services:update",
"os_compute_api:os-services:delete", "os_compute_api:os-services:delete",
"os_compute_api:os-shelve:shelve_offload", "os_compute_api:os-shelve:shelve_offload",
"os_compute_api:os-shelve:unshelve_to_host",
"os_compute_api:os-availability-zone:detail", "os_compute_api:os-availability-zone:detail",
"os_compute_api:os-assisted-volume-snapshots:create", "os_compute_api:os-assisted-volume-snapshots:create",
"os_compute_api:os-assisted-volume-snapshots:delete", "os_compute_api:os-assisted-volume-snapshots:delete",

View File

@@ -0,0 +1,10 @@
---
features:
- |
Microversion 2.91 adds the optional parameter ``host`` to
the ``unshelve`` server action API.
Specifying a destination host is only
allowed to admin users and server status must be ``SHELVED_OFFLOADED``
otherwise a HTTP 400 (bad request) response is returned.
It also allows to set ``availability_zone`` to None to unpin a server
from an availability_zone.