Add host and hypervisor_hostname flag to create server

Add a new microversion that adds two new params to create
server named 'host' and 'hypervisor_hostname'.

Part of Blueprint: add-host-and-hypervisor-hostname-flag-to-create-server

Change-Id: I3afea20edaf738da253ede44b4a07414ededafd6
This commit is contained in:
zhu.boxiang 2019-07-08 11:24:49 +08:00
parent 6298f96068
commit 564290ab14
25 changed files with 660 additions and 17 deletions

View File

@ -5955,6 +5955,16 @@ server_groups_quota_optional:
in: body
required: false
type: integer
# This is the host in a POST (create instance) request body.
server_host_create:
description: |
The name of the compute service host on which the server is to be created.
The API will return 400 if no compute services are found with the given
host name. By default, it can be specified by administrators only.
in: body
required: false
type: string
min_version: 2.74
server_hostname:
in: body
required: false
@ -5963,6 +5973,16 @@ server_hostname:
The hostname set on the instance when it is booted.
By default, it appears in the response for administrative users only.
min_version: 2.3
# This is the hypervisor_hostname in a POST (create instance) request body.
server_hypervisor_hostname_create:
description: |
The hostname of the hypervisor on which the server is to be created.
The API will return 400 if no hypervisors are found with the given
hostname. By default, it can be specified by administrators only.
in: body
required: false
type: string
min_version: 2.74
server_id:
description: |
The UUID of the server.

View File

@ -397,6 +397,8 @@ Request
- description: server_description
- tags: server_tags_create
- trusted_image_certificates: server_trusted_image_certificates_create_req
- host: server_host_create
- hypervisor_hostname: server_hypervisor_hostname_create
- os:scheduler_hints: os:scheduler_hints
- os:scheduler_hints.build_near_host_ip: os:scheduler_hints_build_near_host_ip
- os:scheduler_hints.cidr: os:scheduler_hints_cidr
@ -427,6 +429,11 @@ Request
.. literalinclude:: ../../doc/api_samples/servers/v2.63/server-create-req.json
:language: javascript
**Example Create Server With Host and Hypervisor Hostname (v2.74)**
.. literalinclude:: ../../doc/api_samples/servers/v2.74/server-create-req-with-host-and-node.json
:language: javascript
Response
--------

View File

@ -0,0 +1,23 @@
{
"server" : {
"adminPass": "MySecretPass",
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"name" : "new-server-test",
"imageRef" : "70a599e0-31e7-49b7-b260-868f441e862b",
"flavorRef" : "6",
"OS-DCF:diskConfig": "AUTO",
"metadata" : {
"My Server Name" : "Apache1"
},
"security_groups": [
{
"name": "default"
}
],
"user_data" : "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==",
"networks": "auto",
"host": "openstack-node-01",
"hypervisor_hostname": "openstack-node-01"
}
}

View File

@ -0,0 +1,22 @@
{
"server" : {
"adminPass": "MySecretPass",
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"name" : "new-server-test",
"imageRef" : "70a599e0-31e7-49b7-b260-868f441e862b",
"flavorRef" : "6",
"OS-DCF:diskConfig": "AUTO",
"metadata" : {
"My Server Name" : "Apache1"
},
"security_groups": [
{
"name": "default"
}
],
"user_data" : "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==",
"networks": "auto",
"host": "openstack-node-01"
}
}

View File

@ -0,0 +1,22 @@
{
"server" : {
"adminPass": "MySecretPass",
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"name" : "new-server-test",
"imageRef" : "70a599e0-31e7-49b7-b260-868f441e862b",
"flavorRef" : "6",
"OS-DCF:diskConfig": "AUTO",
"metadata" : {
"My Server Name" : "Apache1"
},
"security_groups": [
{
"name": "default"
}
],
"user_data" : "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==",
"networks": "auto",
"hypervisor_hostname": "openstack-node-01"
}
}

View File

@ -0,0 +1,22 @@
{
"server": {
"OS-DCF:diskConfig": "AUTO",
"adminPass": "DB2bQBhxvq8a",
"id": "84e2b49d-39a9-4d32-9100-e62161c236db",
"links": [
{
"href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/84e2b49d-39a9-4d32-9100-e62161c236db",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/84e2b49d-39a9-4d32-9100-e62161c236db",
"rel": "bookmark"
}
],
"security_groups": [
{
"name": "default"
}
]
}
}

View File

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

View File

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

View File

@ -184,6 +184,10 @@ REST_API_VERSION_HISTORY = """REST API Version History:
``POST /servers/{server_id}/action`` where the action is rebuild.
It also supports ``locked`` as a filter/sort parameter for
``GET /servers/detail`` and ``GET /servers``.
* 2.74 - Add support for specifying ``host`` and/or ``hypervisor_hostname``
in request body to ``POST /servers``. Allow users to specify which
host/node they want their servers to land on and still be
validated by the scheduler.
"""
# The minimum and maximum versions of the API supported
@ -192,7 +196,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.73"
_MAX_API_VERSION = "2.74"
DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal

View File

@ -940,3 +940,19 @@ server and exposes this information via ``GET /servers/detail``,
``POST /servers/{server_id}/action`` where the action is rebuild. It also
supports ``locked`` as a filter/sort parameter for ``GET /servers/detail``
and ``GET /servers``.
2.74
----
API microversion 2.74 adds support for specifying optional ``host``
and/or ``hypervisor_hostname`` parameters in the request body of
``POST /servers``. These request a specific destination host/node
to boot the requested server. These parameters are mutually exclusive
with the special ``availability_zone`` format of ``zone:host:node``.
Unlike ``zone:host:node``, the ``host`` and/or ``hypervisor_hostname``
parameters still allow scheduler filters to be run. If the requested
host/node is unavailable or otherwise unsuitable, earlier failure will
be raised.
There will be also a new policy named
``compute:servers:create:requested_destination``. By default,
it can be specified by administrators only.

View File

@ -356,6 +356,14 @@ base_create_v267['properties']['server']['properties'][
'properties']['volume_type'] = parameter_types.volume_type
# Add host and hypervisor_hostname in server
base_create_v274 = copy.deepcopy(base_create_v267)
base_create_v274['properties']['server'][
'properties']['host'] = parameter_types.hostname
base_create_v274['properties']['server'][
'properties']['hypervisor_hostname'] = parameter_types.hostname
base_update = {
'type': 'object',
'properties': {

View File

@ -561,6 +561,35 @@ class ServersController(wsgi.Controller):
create_kwargs['requested_networks'] = requested_networks
@staticmethod
def _process_hosts_for_create(
context, target, server_dict, create_kwargs, host, node):
"""Processes hosts request parameter for server create
:param context: The nova auth request context
:param target: The target dict for ``context.can`` policy checks
:param server_dict: The POST /servers request body "server" entry
:param create_kwargs: dict that gets populated by this method and
passed to nova.comptue.api.API.create()
:param host: Forced host of availability_zone
:param node: Forced node of availability_zone
:raise: webob.exc.HTTPBadRequest if the request parameters are invalid
:raise: nova.exception.Forbidden if a policy check fails
"""
requested_host = server_dict.get('host')
requested_hypervisor_hostname = server_dict.get('hypervisor_hostname')
if requested_host or requested_hypervisor_hostname:
# If the policy check fails, this will raise Forbidden exception.
context.can(server_policies.REQUESTED_DESTINATION, target=target)
if host or node:
msg = _("One mechanism with host and/or "
"hypervisor_hostname and another mechanism "
"with zone:host:node are mutually exclusive.")
raise exc.HTTPBadRequest(explanation=msg)
create_kwargs['requested_host'] = requested_host
create_kwargs['requested_hypervisor_hostname'] = (
requested_hypervisor_hostname)
@wsgi.response(202)
@wsgi.expected_errors((400, 403, 409))
@validation.schema(schema_servers.base_create_v20, '2.0', '2.0')
@ -573,7 +602,8 @@ class ServersController(wsgi.Controller):
@validation.schema(schema_servers.base_create_v252, '2.52', '2.56')
@validation.schema(schema_servers.base_create_v257, '2.57', '2.62')
@validation.schema(schema_servers.base_create_v263, '2.63', '2.66')
@validation.schema(schema_servers.base_create_v267, '2.67')
@validation.schema(schema_servers.base_create_v267, '2.67', '2.73')
@validation.schema(schema_servers.base_create_v274, '2.74')
def create(self, req, body):
"""Creates a new server for a given user."""
context = req.environ['nova.context']
@ -653,6 +683,10 @@ class ServersController(wsgi.Controller):
if host or node:
context.can(server_policies.SERVERS % 'create:forced_host', {})
if api_version_request.is_supported(req, min_version='2.74'):
self._process_hosts_for_create(context, target, server_dict,
create_kwargs, host, node)
# NOTE(danms): Don't require an answer from all cells here, as
# we assume that if a cell isn't reporting we won't schedule into
# it anyway. A bit of a gamble, but a reasonable one.
@ -750,7 +784,8 @@ class ServersController(wsgi.Controller):
exception.UnableToAutoAllocateNetwork,
exception.MultiattachNotSupportedOldMicroversion,
exception.CertificateValidationFailed,
exception.CreateWithPortResourceRequestOldVersion) as error:
exception.CreateWithPortResourceRequestOldVersion,
exception.ComputeHostNotFound) as error:
raise exc.HTTPBadRequest(explanation=error.format_message())
except INVALID_FLAVOR_IMAGE_EXCEPTIONS as error:
raise exc.HTTPBadRequest(explanation=error.format_message())

View File

@ -998,7 +998,19 @@ class API(base.Base):
block_device_mapping, shutdown_terminate,
instance_group, check_server_group_quota, filter_properties,
key_pair, tags, trusted_certs, supports_multiattach,
network_metadata=None):
network_metadata=None, requested_host=None,
requested_hypervisor_hostname=None):
# NOTE(boxiang): Check whether compute nodes exist by validating
# the host and/or the hypervisor_hostname. Pass the destination
# to the scheduler with host and/or hypervisor_hostname(node).
destination = None
if requested_host or requested_hypervisor_hostname:
self._validate_host_or_node(context, requested_host,
requested_hypervisor_hostname)
destination = objects.Destination()
if requested_host:
destination.host = requested_host
destination.node = requested_hypervisor_hostname
# Check quotas
num_instances = compute_utils.check_num_instances_quota(
context, instance_type, min_count, max_count)
@ -1040,6 +1052,9 @@ class API(base.Base):
if network_metadata:
req_spec.network_metadata = network_metadata
if destination:
req_spec.requested_destination = destination
# Create an instance object, but do not store in db yet.
instance = objects.Instance(context=context)
instance.uuid = instance_uuid
@ -1262,7 +1277,8 @@ class API(base.Base):
reservation_id=None, legacy_bdm=True, shutdown_terminate=False,
check_server_group_quota=False, tags=None,
supports_multiattach=False, trusted_certs=None,
supports_port_resource_request=False):
supports_port_resource_request=False,
requested_host=None, requested_hypervisor_hostname=None):
"""Verify all the input parameters regardless of the provisioning
strategy being performed and schedule the instance(s) for
creation.
@ -1338,7 +1354,8 @@ class API(base.Base):
boot_meta, security_groups, block_device_mapping,
shutdown_terminate, instance_group, check_server_group_quota,
filter_properties, key_pair, tags, trusted_certs,
supports_multiattach, network_metadata)
supports_multiattach, network_metadata,
requested_host, requested_hypervisor_hostname)
instances = []
request_specs = []
@ -1805,7 +1822,8 @@ class API(base.Base):
legacy_bdm=True, shutdown_terminate=False,
check_server_group_quota=False, tags=None,
supports_multiattach=False, trusted_certs=None,
supports_port_resource_request=False):
supports_port_resource_request=False,
requested_host=None, requested_hypervisor_hostname=None):
"""Provision instances, sending instance information to the
scheduler. The scheduler will determine where the instance(s)
go and will handle creating the DB entries.
@ -1848,7 +1866,9 @@ class API(base.Base):
check_server_group_quota=check_server_group_quota,
tags=tags, supports_multiattach=supports_multiattach,
trusted_certs=trusted_certs,
supports_port_resource_request=supports_port_resource_request)
supports_port_resource_request=supports_port_resource_request,
requested_host=requested_host,
requested_hypervisor_hostname=requested_hypervisor_hostname)
def _check_auto_disk_config(self, instance=None, image=None,
**extra_instance_updates):

View File

@ -20,6 +20,7 @@ RULE_AOO = base.RULE_ADMIN_OR_OWNER
SERVERS = 'os_compute_api:servers:%s'
NETWORK_ATTACH_EXTERNAL = 'network:attach_external_network'
ZERO_DISK_FLAVOR = SERVERS % 'create:zero_disk_flavor'
REQUESTED_DESTINATION = 'compute:servers:create:requested_destination'
rules = [
policy.DocumentedRuleDefault(
@ -115,7 +116,30 @@ rules = [
policy.DocumentedRuleDefault(
SERVERS % 'create:forced_host',
base.RULE_ADMIN_API,
"Create a server on the specified host",
"""
Create a server on the specified host and/or node.
In this case, the server is forced to launch on the specified
host and/or node by bypassing the scheduler filters unlike the
``compute:servers:create:requested_destination`` rule.
""",
[
{
'method': 'POST',
'path': '/servers'
}
]),
policy.DocumentedRuleDefault(
REQUESTED_DESTINATION,
base.RULE_ADMIN_API,
"""
Create a server on the requested compute service host and/or
hypervisor_hostname.
In this case, the requested host and/or hypervisor_hostname is
validated by the scheduler filters unlike the
``os_compute_api:servers:create:forced_host`` rule.
""",
[
{
'method': 'POST',

View File

@ -540,10 +540,29 @@ class HostManager(object):
"'force_nodes' value of '%s'", forced_nodes_str)
def _get_hosts_matching_request(hosts, requested_destination):
"""Get hosts through matching the requested destination.
We will both set host and node to requested destination object
and host will never be None and node will be None in some cases.
Starting with API 2.74 microversion, we also can specify the
host/node to select hosts to launch a server:
- If only host(or only node)(or both host and node) is supplied
and we get one node from get_compute_nodes_by_host_or_node which
is called in resources_from_request_spec function,
the destination will be set both host and node.
- If only host is supplied and we get more than one node from
get_compute_nodes_by_host_or_node which is called in
resources_from_request_spec function, the destination will only
include host.
"""
(host, node) = (requested_destination.host,
requested_destination.node)
requested_nodes = [x for x in hosts
if x.host == host and x.nodename == node]
if node:
requested_nodes = [x for x in hosts
if x.host == host and x.nodename == node]
else:
requested_nodes = [x for x in hosts
if x.host == host]
if requested_nodes:
LOG.info('Host filter only checking host %(host)s and '
'node %(node)s', {'host': host, 'node': node})

View File

@ -532,6 +532,14 @@ def resources_from_request_spec(ctxt, spec_obj, host_manager):
{'host': target_host, 'node': target_node})
raise exception.NoValidHost(reason=reason)
if len(nodes) == 1:
if 'requested_destination' in spec_obj and destination:
# When we only supply hypervisor_hostname in api to create a
# server, the destination object will only include the node.
# Here when we get one node, we set both host and node to
# destination object. So we can reduce the number of HostState
# objects to run through the filters.
destination.host = nodes[0].host
destination.node = nodes[0].hypervisor_hostname
grp = res_req.get_request_group(None)
grp.in_tree = nodes[0].uuid
else:

View File

@ -0,0 +1,23 @@
{
"server" : {
"adminPass": "MySecretPass",
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"name" : "%(name)s",
"imageRef" : "%(image_id)s",
"flavorRef" : "6",
"OS-DCF:diskConfig": "AUTO",
"metadata" : {
"My Server Name" : "Apache1"
},
"security_groups": [
{
"name": "default"
}
],
"user_data" : "%(user_data)s",
"networks": "auto",
"host": "openstack-node-01",
"hypervisor_hostname": "openstack-node-01"
}
}

View File

@ -0,0 +1,22 @@
{
"server" : {
"adminPass": "MySecretPass",
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"name" : "%(name)s",
"imageRef" : "%(image_id)s",
"flavorRef" : "6",
"OS-DCF:diskConfig": "AUTO",
"metadata" : {
"My Server Name" : "Apache1"
},
"security_groups": [
{
"name": "default"
}
],
"user_data" : "%(user_data)s",
"networks": "auto",
"host": "openstack-node-01"
}
}

View File

@ -0,0 +1,22 @@
{
"server" : {
"adminPass": "MySecretPass",
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"name" : "%(name)s",
"imageRef" : "%(image_id)s",
"flavorRef" : "6",
"OS-DCF:diskConfig": "AUTO",
"metadata" : {
"My Server Name" : "Apache1"
},
"security_groups": [
{
"name": "default"
}
],
"user_data" : "%(user_data)s",
"networks": "auto",
"hypervisor_hostname": "openstack-node-01"
}
}

View File

@ -0,0 +1,22 @@
{
"server": {
"OS-DCF:diskConfig": "AUTO",
"adminPass": "%(password)s",
"id": "%(id)s",
"links": [
{
"href": "%(versioned_compute_endpoint)s/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/servers/%(uuid)s",
"rel": "bookmark"
}
],
"security_groups": [
{
"name": "default"
}
]
}
}

View File

@ -43,9 +43,9 @@ class ServersSampleBase(api_sample_base.ApiSampleTestBaseV21):
('2.57', None, 'server-create-req-v257')
]
def _get_request_name(self, use_common):
def _get_request_name(self, use_common, sample_name=None):
if not use_common:
return 'server-create-req'
return sample_name or 'server-create-req'
api_version = self.microversion or '2.1'
for min, max, name in self.common_req_names:
@ -54,7 +54,7 @@ class ServersSampleBase(api_sample_base.ApiSampleTestBaseV21):
return name
def _post_server(self, use_common_server_api_samples=True, name=None,
extra_subs=None):
extra_subs=None, sample_name=None):
# param use_common_server_api_samples: Boolean to set whether tests use
# common sample files for server post request and response.
# Default is True which means _get_sample_path method will fetch the
@ -86,8 +86,13 @@ class ServersSampleBase(api_sample_base.ApiSampleTestBaseV21):
try:
self.__class__._use_common_server_api_samples = (
use_common_server_api_samples)
# If using common samples, we could only put samples under
# api_samples/servers. We will put a lot of samples when we
# have more and more microversions.
# Callers can specify the sample_name param so that we can add
# samples into api_samples/servers/v2.xx.
response = self._do_post('servers', self._get_request_name(
use_common_server_api_samples), subs)
use_common_server_api_samples, sample_name), subs)
status = self._verify_response('server-create-resp', subs,
response, 202)
return status
@ -568,6 +573,38 @@ class ServersSampleJson273Test(ServersSampleBase):
self._verify_response('server-update-resp', subs, response, 200)
class ServersSampleJson274Test(ServersSampleBase):
"""Supporting host and/or hypervisor_hostname is an admin API
to create servers.
"""
ADMIN_API = True
SUPPORTS_CELLS = True
microversion = '2.74'
scenarios = [('v2_74', {'api_major_version': 'v2.1'})]
# Do not put an availability_zone in the API sample request since it would
# be confusing with the requested host/hypervisor_hostname and forced
# host/node zone:host:node case.
availability_zones = []
def _setup_compute_service(self):
return self.start_service('compute', host='openstack-node-01')
def setUp(self):
super(ServersSampleJson274Test, self).setUp()
def test_servers_post_with_only_host(self):
self._post_server(use_common_server_api_samples=False,
sample_name='server-create-req-with-only-host')
def test_servers_post_with_only_node(self):
self._post_server(use_common_server_api_samples=False,
sample_name='server-create-req-with-only-node')
def test_servers_post_with_host_and_node(self):
self._post_server(use_common_server_api_samples=False,
sample_name='server-create-req-with-host-and-node')
class ServersUpdateSampleJsonTest(ServersSampleBase):
def test_update_server(self):

View File

@ -3014,6 +3014,133 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase):
self._test_resize_to_same_host_instance_fails(
'_finish_resize', 'compute_finish_resize')
def _server_created_with_host(self):
hostname = self.compute1.host
server_req = self._build_minimal_create_server_request(
self.api, "some-server", flavor_id=self.flavor1["id"],
image_uuid="155d900f-4e14-4e4c-a73d-069cbf4541e6",
networks='none')
server_req['host'] = hostname
created_server = self.api.post_server({"server": server_req})
server = self._wait_for_state_change(
self.api, created_server, "ACTIVE")
return server
def test_live_migration_after_server_created_with_host(self):
"""Test after creating server with requested host, and then
do live-migration for the server. The requested host will not
effect the new moving operation.
"""
dest_hostname = self.compute2.host
created_server = self._server_created_with_host()
post = {
'os-migrateLive': {
'host': None,
'block_migration': 'auto'
}
}
self.api.post_server_action(created_server['id'], post)
new_server = self._wait_for_server_parameter(
self.api, created_server, {'status': 'ACTIVE'})
inst_dest_host = new_server["OS-EXT-SRV-ATTR:host"]
self.assertEqual(dest_hostname, inst_dest_host)
def test_evacuate_after_server_created_with_host(self):
"""Test after creating server with requested host, and then
do evacuation for the server. The requested host will not
effect the new moving operation.
"""
dest_hostname = self.compute2.host
created_server = self._server_created_with_host()
source_compute_id = self.admin_api.get_services(
host=created_server["OS-EXT-SRV-ATTR:host"],
binary='nova-compute')[0]['id']
self.compute1.stop()
# force it down to avoid waiting for the service group to time out
self.admin_api.put_service(
source_compute_id, {'forced_down': 'true'})
post = {
'evacuate': {}
}
self.api.post_server_action(created_server['id'], post)
expected_params = {'OS-EXT-SRV-ATTR:host': dest_hostname,
'status': 'ACTIVE'}
new_server = self._wait_for_server_parameter(self.api, created_server,
expected_params)
inst_dest_host = new_server["OS-EXT-SRV-ATTR:host"]
self.assertEqual(dest_hostname, inst_dest_host)
def test_resize_and_confirm_after_server_created_with_host(self):
"""Test after creating server with requested host, and then
do resize for the server. The requested host will not
effect the new moving operation.
"""
dest_hostname = self.compute2.host
created_server = self._server_created_with_host()
# resize server
self.flags(allow_resize_to_same_host=False)
resize_req = {
'resize': {
'flavorRef': self.flavor2['id']
}
}
self.api.post_server_action(created_server['id'], resize_req)
self._wait_for_state_change(self.api, created_server, 'VERIFY_RESIZE')
# Confirm the resize
post = {'confirmResize': None}
self.api.post_server_action(
created_server['id'], post, check_response_status=[204])
new_server = self._wait_for_state_change(self.api, created_server,
'ACTIVE')
inst_dest_host = new_server["OS-EXT-SRV-ATTR:host"]
self.assertEqual(dest_hostname, inst_dest_host)
def test_shelve_unshelve_after_server_created_with_host(self):
"""Test after creating server with requested host, and then
do shelve and unshelve for the server. The requested host
will not effect the new moving operation.
"""
dest_hostname = self.compute2.host
created_server = self._server_created_with_host()
self.flags(shelved_offload_time=-1)
req = {'shelve': {}}
self.api.post_server_action(created_server['id'], req)
self._wait_for_state_change(self.api, created_server, 'SHELVED')
req = {'shelveOffload': {}}
self.api.post_server_action(created_server['id'], req)
self._wait_for_server_parameter(
self.api, created_server, {'status': 'SHELVED_OFFLOADED',
'OS-EXT-SRV-ATTR:host': None,
'OS-EXT-AZ:availability_zone': ''})
# unshelve after shelve offload will do scheduling. this test case
# wants to test the scenario when the scheduler select a different host
# to ushelve the instance. So we disable the original host.
source_service_id = self.admin_api.get_services(
host=created_server["OS-EXT-SRV-ATTR:host"],
binary='nova-compute')[0]['id']
self.admin_api.put_service(source_service_id, {'status': 'disabled'})
req = {'unshelve': {}}
self.api.post_server_action(created_server['id'], req)
new_server = self._wait_for_state_change(
self.api, created_server, 'ACTIVE')
inst_dest_host = new_server["OS-EXT-SRV-ATTR:host"]
self.assertEqual(dest_hostname, inst_dest_host)
def _test_resize_reschedule_uses_host_lists(self, fails, num_alts=None):
"""Test that when a resize attempt fails, the retry comes from the
supplied host_list, and does not call the scheduler.

View File

@ -6767,6 +6767,130 @@ class ServersControllerCreateTestV267(ServersControllerCreateTest):
self.assertIn('is too long', six.text_type(ex))
class ServersControllerCreateTestV274(ServersControllerCreateTest):
def setUp(self):
super(ServersControllerCreateTestV274, self).setUp()
self.req.environ['nova.context'] = fakes.FakeRequestContext(
user_id='fake_user',
project_id='fake',
is_admin=True)
self.mock_get = self.useFixture(
fixtures.MockPatch('nova.scheduler.client.report.'
'SchedulerReportClient.get')).mock
def _generate_req(self, host=None, node=None, az=None,
api_version='2.74'):
if host:
self.body['server']['host'] = host
if node:
self.body['server']['hypervisor_hostname'] = node
if az:
self.body['server']['availability_zone'] = az
self.req.body = jsonutils.dump_as_bytes(self.body)
self.req.api_version_request = \
api_version_request.APIVersionRequest(api_version)
def test_create_instance_with_invalid_host(self):
self._generate_req(host='node-invalid')
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
self.req, body=self.body)
self.assertIn('Compute host node-invalid could not be found.',
six.text_type(ex))
def test_create_instance_with_non_string_host(self):
self._generate_req(host=123)
ex = self.assertRaises(exception.ValidationError,
self.controller.create,
self.req, body=self.body)
self.assertIn("Invalid input for field/attribute host.",
six.text_type(ex))
def test_create_instance_with_invalid_hypervisor_hostname(self):
get_resp = mock.Mock()
get_resp.status_code = 404
self.mock_get.return_value = get_resp
self._generate_req(node='node-invalid')
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
self.req, body=self.body)
self.assertIn('Compute host node-invalid could not be found.',
six.text_type(ex))
def test_create_instance_with_non_string_hypervisor_hostname(self):
get_resp = mock.Mock()
get_resp.status_code = 404
self.mock_get.return_value = get_resp
self._generate_req(node=123)
ex = self.assertRaises(exception.ValidationError,
self.controller.create,
self.req, body=self.body)
self.assertIn("Invalid input for field/attribute hypervisor_hostname.",
six.text_type(ex))
def test_create_instance_with_invalid_host_and_hypervisor_hostname(self):
self._generate_req(host='host-invalid', node='node-invalid')
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
self.req, body=self.body)
self.assertIn('Compute host host-invalid could not be found.',
six.text_type(ex))
def test_create_instance_with_non_string_host_and_hypervisor_hostname(
self):
self._generate_req(host=123, node=123)
ex = self.assertRaises(exception.ValidationError,
self.controller.create,
self.req, body=self.body)
self.assertIn("Invalid input for field/attribute",
six.text_type(ex))
def test_create_instance_pre_274(self):
self._generate_req(host='host', node='node', api_version='2.73')
ex = self.assertRaises(exception.ValidationError,
self.controller.create,
self.req, body=self.body)
self.assertIn("Invalid input for field/attribute server.",
six.text_type(ex))
def test_create_instance_mutual(self):
self._generate_req(host='host', node='node', az='nova:host:node')
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
self.req, body=self.body)
self.assertIn("mutually exclusive", six.text_type(ex))
def test_create_instance_invalid_policy(self):
self._generate_req(host='host', node='node')
# non-admin
self.req.environ['nova.context'] = fakes.FakeRequestContext(
user_id='fake_user',
project_id='fake',
is_admin=False)
ex = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.create,
self.req, body=self.body)
self.assertIn("Policy doesn't allow compute:servers:create:"
"requested_destination to be performed.",
six.text_type(ex))
def test_create_instance_private_flavor(self):
# Here we use admin context, so if we do not pass it or
# we do not anything, the test case will be failed.
pass
class ServersControllerCreateTestWithMock(test.TestCase):
image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
flavor_ref = 'http://localhost/123/flavors/3'

View File

@ -286,6 +286,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
self.admin_only_rules = (
"network:attach_external_network",
"os_compute_api:servers:create:forced_host",
"compute:servers:create:requested_destination",
"os_compute_api:servers:detail:get_all_tenants",
"os_compute_api:servers:index:get_all_tenants",
"os_compute_api:servers:allow_all_filters",

View File

@ -0,0 +1,15 @@
---
features:
- |
API microversion 2.74 adds support for specifying optional ``host``
and/or ``hypervisor_hostname`` parameters in the request body of
``POST /servers``. These request a specific destination host/node
to boot the requested server. These parameters are mutually exclusive
with the special ``availability_zone`` format of ``zone:host:node``.
Unlike ``zone:host:node``, the ``host`` and/or ``hypervisor_hostname``
parameters still allow scheduler filters to be run. If the requested
host/node is unavailable or otherwise unsuitable, earlier failure will
be raised.
There will be also a new policy named
``compute:servers:create:requested_destination``. By default,
it can be specified by administrators only.