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:
parent
6298f96068
commit
564290ab14
@ -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.
|
||||
|
@ -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
|
||||
--------
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
22
doc/api_samples/servers/v2.74/server-create-resp.json
Normal file
22
doc/api_samples/servers/v2.74/server-create-resp.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.73",
|
||||
"version": "2.74",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.73",
|
||||
"version": "2.74",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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': {
|
||||
|
@ -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())
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
|
@ -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})
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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):
|
||||
|
@ -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.
|
||||
|
@ -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'
|
||||
|
@ -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",
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user