From 564290ab145d2710d3e82b5c4871e647e4d516c4 Mon Sep 17 00:00:00 2001 From: "zhu.boxiang" Date: Mon, 8 Jul 2019 11:24:49 +0800 Subject: [PATCH] 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 --- api-ref/source/parameters.yaml | 20 +++ api-ref/source/servers.inc | 7 + .../server-create-req-with-host-and-node.json | 23 ++++ .../server-create-req-with-only-host.json | 22 +++ .../server-create-req-with-only-node.json | 22 +++ .../servers/v2.74/server-create-resp.json | 22 +++ .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- nova/api/openstack/api_version_request.py | 6 +- .../compute/rest_api_version_history.rst | 16 +++ nova/api/openstack/compute/schemas/servers.py | 8 ++ nova/api/openstack/compute/servers.py | 39 +++++- nova/compute/api.py | 30 ++++- nova/policies/servers.py | 26 +++- nova/scheduler/host_manager.py | 23 +++- nova/scheduler/utils.py | 8 ++ ...ver-create-req-with-host-and-node.json.tpl | 23 ++++ .../server-create-req-with-only-host.json.tpl | 22 +++ .../server-create-req-with-only-node.json.tpl | 22 +++ .../servers/v2.74/server-create-resp.json.tpl | 22 +++ .../api_sample_tests/test_servers.py | 45 ++++++- nova/tests/functional/test_servers.py | 127 ++++++++++++++++++ .../api/openstack/compute/test_serversV21.py | 124 +++++++++++++++++ nova/tests/unit/test_policy.py | 1 + ...lag-to-create-server-847ba43abd6be02c.yaml | 15 +++ 25 files changed, 660 insertions(+), 17 deletions(-) create mode 100644 doc/api_samples/servers/v2.74/server-create-req-with-host-and-node.json create mode 100644 doc/api_samples/servers/v2.74/server-create-req-with-only-host.json create mode 100644 doc/api_samples/servers/v2.74/server-create-req-with-only-node.json create mode 100644 doc/api_samples/servers/v2.74/server-create-resp.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-host-and-node.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-only-host.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-only-node.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-resp.json.tpl create mode 100644 releasenotes/notes/add-host-and-hypervisor-hostname-flag-to-create-server-847ba43abd6be02c.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index c1eebf5e79..fee623514a 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -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. diff --git a/api-ref/source/servers.inc b/api-ref/source/servers.inc index 976f353adf..39edb835aa 100644 --- a/api-ref/source/servers.inc +++ b/api-ref/source/servers.inc @@ -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 -------- diff --git a/doc/api_samples/servers/v2.74/server-create-req-with-host-and-node.json b/doc/api_samples/servers/v2.74/server-create-req-with-host-and-node.json new file mode 100644 index 0000000000..43552ed638 --- /dev/null +++ b/doc/api_samples/servers/v2.74/server-create-req-with-host-and-node.json @@ -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" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.74/server-create-req-with-only-host.json b/doc/api_samples/servers/v2.74/server-create-req-with-only-host.json new file mode 100644 index 0000000000..aa0dc613b1 --- /dev/null +++ b/doc/api_samples/servers/v2.74/server-create-req-with-only-host.json @@ -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" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.74/server-create-req-with-only-node.json b/doc/api_samples/servers/v2.74/server-create-req-with-only-node.json new file mode 100644 index 0000000000..ab9ec85350 --- /dev/null +++ b/doc/api_samples/servers/v2.74/server-create-req-with-only-node.json @@ -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" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.74/server-create-resp.json b/doc/api_samples/servers/v2.74/server-create-resp.json new file mode 100644 index 0000000000..7ebe2e20a2 --- /dev/null +++ b/doc/api_samples/servers/v2.74/server-create-resp.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 34ed8b0809..48e874375b 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.73", + "version": "2.74", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index f86347de06..f520e6099e 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.73", + "version": "2.74", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index ab22995075..f60fc77f09 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -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 diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 9f2105e51b..b938d73a4b 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -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. diff --git a/nova/api/openstack/compute/schemas/servers.py b/nova/api/openstack/compute/schemas/servers.py index 1b048e1704..51887dcaae 100644 --- a/nova/api/openstack/compute/schemas/servers.py +++ b/nova/api/openstack/compute/schemas/servers.py @@ -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': { diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index 3480725338..ea1911499b 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -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()) diff --git a/nova/compute/api.py b/nova/compute/api.py index e067b2e637..69b15d4f06 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -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): diff --git a/nova/policies/servers.py b/nova/policies/servers.py index f02a6e75f1..e08a4e70bc 100644 --- a/nova/policies/servers.py +++ b/nova/policies/servers.py @@ -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', diff --git a/nova/scheduler/host_manager.py b/nova/scheduler/host_manager.py index 55ea6b2eda..e6138c1029 100644 --- a/nova/scheduler/host_manager.py +++ b/nova/scheduler/host_manager.py @@ -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}) diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index 5ab0c50093..f6b296a25f 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -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: diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-host-and-node.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-host-and-node.json.tpl new file mode 100644 index 0000000000..6f6512668f --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-host-and-node.json.tpl @@ -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" + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-only-host.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-only-host.json.tpl new file mode 100644 index 0000000000..16a8bbac49 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-only-host.json.tpl @@ -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" + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-only-node.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-only-node.json.tpl new file mode 100644 index 0000000000..5305798a9e --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-req-with-only-node.json.tpl @@ -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" + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-resp.json.tpl new file mode 100644 index 0000000000..4b30e0cfbd --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.74/server-create-resp.json.tpl @@ -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" + } + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/test_servers.py b/nova/tests/functional/api_sample_tests/test_servers.py index e2c9b17c20..1f7fad40e8 100644 --- a/nova/tests/functional/api_sample_tests/test_servers.py +++ b/nova/tests/functional/api_sample_tests/test_servers.py @@ -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): diff --git a/nova/tests/functional/test_servers.py b/nova/tests/functional/test_servers.py index ee8448169c..c666cecc9b 100644 --- a/nova/tests/functional/test_servers.py +++ b/nova/tests/functional/test_servers.py @@ -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. diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index 62e22ffd55..124879fddc 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -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' diff --git a/nova/tests/unit/test_policy.py b/nova/tests/unit/test_policy.py index b91355da5e..2917d701e0 100644 --- a/nova/tests/unit/test_policy.py +++ b/nova/tests/unit/test_policy.py @@ -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", diff --git a/releasenotes/notes/add-host-and-hypervisor-hostname-flag-to-create-server-847ba43abd6be02c.yaml b/releasenotes/notes/add-host-and-hypervisor-hostname-flag-to-create-server-847ba43abd6be02c.yaml new file mode 100644 index 0000000000..5d4dfac0d6 --- /dev/null +++ b/releasenotes/notes/add-host-and-hypervisor-hostname-flag-to-create-server-847ba43abd6be02c.yaml @@ -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.