diff --git a/api-ref/source/os-hypervisors.inc b/api-ref/source/os-hypervisors.inc index b1346c4627d8..5309bbdf0243 100644 --- a/api-ref/source/os-hypervisors.inc +++ b/api-ref/source/os-hypervisors.inc @@ -25,7 +25,7 @@ the ``policy.json`` file. Normal response codes: 200 -Error response codes: unauthorized(401), forbidden(403) +Error response codes: badRequest(400), unauthorized(401), forbidden(403) Request ------- @@ -34,6 +34,9 @@ Request - limit: hypervisor_limit - marker: hypervisor_marker + - marker: hypervisor_marker_uuid + - hypervisor_hostname_pattern: hypervisor_hostname_pattern_query + - with_servers: hypervisor_with_servers_query Response -------- @@ -43,15 +46,24 @@ Response - hypervisors: hypervisors - hypervisor_hostname: hypervisor_hostname - id: hypervisor_id_body + - id: hypervisor_id_body_uuid - state: hypervisor_state - status: hypervisor_status - hypervisor_links: hypervisor_links + - servers: hypervisor_servers + - servers.uuid: hypervisor_servers_uuid + - servers.name: hypervisor_servers_name **Example List Hypervisors (v2.33): JSON response** .. literalinclude:: ../../doc/api_samples/os-hypervisors/v2.33/hypervisors-list-resp.json :language: javascript +**Example List Hypervisors With Servers (v2.53): JSON response** + +.. literalinclude:: ../../doc/api_samples/os-hypervisors/v2.53/hypervisors-with-servers-resp.json + :language: javascript + List Hypervisors Details ======================== @@ -65,7 +77,7 @@ the ``policy.json`` file. Normal response codes: 200 -Error response codes: unauthorized(401), forbidden(403) +Error response codes: badRequest(400), unauthorized(401), forbidden(403) Request ------- @@ -74,6 +86,9 @@ Request - limit: hypervisor_limit - marker: hypervisor_marker + - marker: hypervisor_marker_uuid + - hypervisor_hostname_pattern: hypervisor_hostname_pattern_query + - with_servers: hypervisor_with_servers_query Response -------- @@ -93,14 +108,19 @@ Response - hypervisor_type: hypervisor_type_body - hypervisor_version: hypervisor_version - id: hypervisor_id_body + - id: hypervisor_id_body_uuid - local_gb: local_gb - local_gb_used: local_gb_used - memory_mb: memory_mb - memory_mb_used: memory_mb_used - running_vms: running_vms + - servers: hypervisor_servers + - servers.uuid: hypervisor_servers_uuid + - servers.name: hypervisor_servers_name - service: hypervisor_service - service.host: host_name_body - - service.id: service_id_body + - service.id: service_id_body_2_52 + - service.id: service_id_body_2_53 - service.disable_reason: service_disable_reason - vcpus: hypervisor_vcpus - vcpus_used: hypervisor_vcpus_used @@ -111,6 +131,11 @@ Response .. literalinclude:: ../../doc/api_samples/os-hypervisors/v2.33/hypervisors-detail-resp.json :language: javascript +**Example List Hypervisors Details (v2.53): JSON response** + +.. literalinclude:: ../../doc/api_samples/os-hypervisors/v2.53/hypervisors-detail-resp.json + :language: javascript + Show Hypervisor Statistics ========================== @@ -163,7 +188,7 @@ the ``policy.json`` file. Normal response codes: 200 -Error response codes: unauthorized(401), forbidden(403), itemNotFound(404) +Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404) Request ------- @@ -171,6 +196,8 @@ Request .. rest_parameters:: parameters.yaml - hypervisor_id: hypervisor_id + - hypervisor_id: hypervisor_id_uuid + - with_servers: hypervisor_with_servers_query Response -------- @@ -190,14 +217,19 @@ Response - hypervisor_type: hypervisor_type_body - hypervisor_version: hypervisor_version - id: hypervisor_id_body + - id: hypervisor_id_body_uuid - local_gb: local_gb - local_gb_used: local_gb_used - memory_mb: memory_mb - memory_mb_used: memory_mb_used - running_vms: running_vms + - servers: hypervisor_servers + - servers.uuid: hypervisor_servers_uuid + - servers.name: hypervisor_servers_name - service: hypervisor_service - service.host: host_name_body - - service.id: service_id_body + - service.id: service_id_body_2_52 + - service.id: service_id_body_2_53 - service.disable_reason: service_disable_reason - vcpus: hypervisor_vcpus - vcpus_used: hypervisor_vcpus_used @@ -207,6 +239,11 @@ Response .. literalinclude:: ../../doc/api_samples/os-hypervisors/v2.28/hypervisors-show-resp.json :language: javascript +**Example Show Hypervisor Details With Servers (v2.53): JSON response** + +.. literalinclude:: ../../doc/api_samples/os-hypervisors/v2.53/hypervisors-show-with-servers-resp.json + :language: javascript + Show Hypervisor Uptime ====================== @@ -220,7 +257,7 @@ the ``policy.json`` file. Normal response codes: 200 -Error response codes: unauthorized(401), forbidden(403), itemNotFound(404), NotImplemented(501) +Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404), NotImplemented(501) Request ------- @@ -228,6 +265,7 @@ Request .. rest_parameters:: parameters.yaml - hypervisor_id: hypervisor_id + - hypervisor_id: hypervisor_id_uuid Response -------- @@ -237,6 +275,7 @@ Response - hypervisor: hypervisor - hypervisor_hostname: hypervisor_hostname - id: hypervisor_id_body + - id: hypervisor_id_body_uuid - state: hypervisor_state - status: hypervisor_status - uptime: uptime @@ -246,13 +285,23 @@ Response .. literalinclude:: ../../doc/api_samples/os-hypervisors/hypervisors-uptime-resp.json :language: javascript +**Example Show Hypervisor Uptime (v2.53): JSON response** + +.. literalinclude:: ../../doc/api_samples/os-hypervisors/v2.53/hypervisors-uptime-resp.json + :language: javascript + Search Hypervisor ================= .. rest_method:: GET /os-hypervisors/{hypervisor_hostname_pattern}/search + max_version: 2.52 Search hypervisor by a given hypervisor host name or portion of it. +.. warning:: This API is deprecated starting with microversion 2.53. Use + `List Hypervisors`_ with the ``hypervisor_hostname_pattern`` query + parameter with microversion 2.53 and later. + Policy defaults enable only users with the administrative role to perform this operation. Cloud providers can change these permissions through the ``policy.json`` file. @@ -275,7 +324,7 @@ Response - hypervisors: hypervisors - hypervisor_hostname: hypervisor_hostname - - id: hypervisor_id_body + - id: hypervisor_id_body_no_version - state: hypervisor_state - status: hypervisor_status @@ -288,10 +337,15 @@ List Hypervisor Servers ======================= .. rest_method:: GET /os-hypervisors/{hypervisor_hostname_pattern}/servers + max_version: 2.52 List all servers belong to each hypervisor whose host name is matching a given hypervisor host name or portion of it. +.. warning:: This API is deprecated starting with microversion 2.53. Use + `List Hypervisors`_ with the ``hypervisor_hostname_pattern`` and + ``with_servers`` query parameters with microversion 2.53 and later. + Policy defaults enable only users with the administrative role to perform this operation. Cloud providers can change these permissions through the ``policy.json`` file. @@ -314,7 +368,7 @@ Response - hypervisors: hypervisors - hypervisor_hostname: hypervisor_hostname - - id: hypervisor_id_body + - id: hypervisor_id_body_no_version - state: hypervisor_state - status: hypervisor_status - servers: servers diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 03e7a01c1cd8..c1c04933d5b7 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -182,6 +182,14 @@ hypervisor_id: in: path required: true type: integer + max_version: 2.52 +hypervisor_id_uuid: + description: | + The ID of the hypervisor as a UUID. + in: path + required: true + type: string + min_version: 2.53 image_id: description: | The UUID of the image. @@ -565,6 +573,19 @@ hostname_query_server: in: query required: false type: string +hypervisor_hostname_pattern_query: + description: | + The hypervisor host name or a portion of it. The hypervisor hosts are + selected with the host name matching this pattern. + + .. note:: ``limit`` and ``marker`` query parameters for paging are + not supported when listing hypervisors using a hostname pattern. + Also, ``links`` will not be returned in the response when using this + query parameter. + in: query + required: false + type: string + min_version: 2.53 hypervisor_limit: description: | Requests a page size of items. Returns a number of items up to a limit value. @@ -584,12 +605,29 @@ hypervisor_marker: required: false type: integer min_version: 2.33 + max_version: 2.52 +hypervisor_marker_uuid: + description: | + The ID of the last-seen item as a UUID. Use the ``limit`` parameter to make + an initial limited request and use the ID of the last-seen item from the + response as the ``marker`` parameter value in a subsequent limited request. + in: query + required: false + type: string + min_version: 2.53 hypervisor_query: description: | Filters the response by a hypervisor type. in: query required: false type: string +hypervisor_with_servers_query: + description: | + Include all servers which belong to each hypervisor in the response output. + in: query + required: false + type: boolean + min_version: 2.53 image_name_query: description: | Filters the response by an image name, as a string. @@ -2969,6 +3007,20 @@ hypervisor_id_body: in: body required: true type: integer + max_version: 2.52 +hypervisor_id_body_no_version: + description: | + The id of the hypervisor. + in: body + required: true + type: integer +hypervisor_id_body_uuid: + description: | + The id of the hypervisor as a UUID. + in: body + required: true + type: string + min_version: 2.53 hypervisor_links: description: | Links to the hypervisors resource. See `API Guide / Links and @@ -2986,6 +3038,27 @@ hypervisor_os_diagnostics: type: string required: true min_version: 2.48 +hypervisor_servers: + description: | + A list of ``server`` objects. + in: body + required: false + type: array + min_version: 2.53 +hypervisor_servers_name: + description: | + The server name. + in: body + required: false + type: string + min_version: 2.53 +hypervisor_servers_uuid: + description: | + The server ID. + in: body + required: false + type: string + min_version: 2.53 hypervisor_service: description: | The hypervisor service object. diff --git a/doc/api_samples/os-hypervisors/v2.53/hypervisors-detail-resp.json b/doc/api_samples/os-hypervisors/v2.53/hypervisors-detail-resp.json new file mode 100644 index 000000000000..843af88e973a --- /dev/null +++ b/doc/api_samples/os-hypervisors/v2.53/hypervisors-detail-resp.json @@ -0,0 +1,49 @@ +{ + "hypervisors": [ + { + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "current_workload": 0, + "status": "enabled", + "state": "up", + "disk_available_least": 0, + "host_ip": "1.1.1.1", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "id": "1bb62a04-c576-402c-8147-9e89757a09e3", + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "host1", + "id": "62f62f6e-a713-4cbe-87d3-3ecf8a1e0f8d", + "disabled_reason": null + }, + "vcpus": 1, + "vcpus_used": 0 + } + ], + "hypervisors_links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/hypervisors/detail?limit=1&marker=1bb62a04-c576-402c-8147-9e89757a09e3", + "rel": "next" + } + ] +} diff --git a/doc/api_samples/os-hypervisors/v2.53/hypervisors-detail-with-servers-resp.json b/doc/api_samples/os-hypervisors/v2.53/hypervisors-detail-with-servers-resp.json new file mode 100644 index 000000000000..c9342cb416e3 --- /dev/null +++ b/doc/api_samples/os-hypervisors/v2.53/hypervisors-detail-with-servers-resp.json @@ -0,0 +1,53 @@ +{ + "hypervisors": [ + { + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "current_workload": 0, + "status": "enabled", + "servers": [ + { + "name": "test_server1", + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + { + "name": "test_server2", + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + ], + "state": "up", + "disk_available_least": 0, + "host_ip": "1.1.1.1", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d", + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "host1", + "id": "5d343e1d-938e-4284-b98b-6a2b5406ba76", + "disabled_reason": null + }, + "vcpus": 1, + "vcpus_used": 0 + } + ] +} diff --git a/doc/api_samples/os-hypervisors/v2.53/hypervisors-list-resp.json b/doc/api_samples/os-hypervisors/v2.53/hypervisors-list-resp.json new file mode 100644 index 000000000000..ec10b4a106e0 --- /dev/null +++ b/doc/api_samples/os-hypervisors/v2.53/hypervisors-list-resp.json @@ -0,0 +1,16 @@ +{ + "hypervisors": [ + { + "hypervisor_hostname": "fake-mini", + "id": "1bb62a04-c576-402c-8147-9e89757a09e3", + "state": "up", + "status": "enabled" + } + ], + "hypervisors_links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/hypervisors?limit=1&marker=1bb62a04-c576-402c-8147-9e89757a09e3", + "rel": "next" + } + ] +} diff --git a/doc/api_samples/os-hypervisors/v2.53/hypervisors-search-resp.json b/doc/api_samples/os-hypervisors/v2.53/hypervisors-search-resp.json new file mode 100644 index 000000000000..9f8258439b2d --- /dev/null +++ b/doc/api_samples/os-hypervisors/v2.53/hypervisors-search-resp.json @@ -0,0 +1,10 @@ +{ + "hypervisors": [ + { + "hypervisor_hostname": "fake-mini", + "id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d", + "state": "up", + "status": "enabled" + } + ] +} diff --git a/doc/api_samples/os-hypervisors/v2.53/hypervisors-show-resp.json b/doc/api_samples/os-hypervisors/v2.53/hypervisors-show-resp.json new file mode 100644 index 000000000000..ba26210541b7 --- /dev/null +++ b/doc/api_samples/os-hypervisors/v2.53/hypervisors-show-resp.json @@ -0,0 +1,41 @@ +{ + "hypervisor": { + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "state": "up", + "status": "enabled", + "current_workload": 0, + "disk_available_least": 0, + "host_ip": "1.1.1.1", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d", + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "043b3cacf6f34c90a7245151fc8ebcda", + "id": "5d343e1d-938e-4284-b98b-6a2b5406ba76", + "disabled_reason": null + }, + "vcpus": 1, + "vcpus_used": 0 + } +} diff --git a/doc/api_samples/os-hypervisors/v2.53/hypervisors-show-with-servers-resp.json b/doc/api_samples/os-hypervisors/v2.53/hypervisors-show-with-servers-resp.json new file mode 100644 index 000000000000..5477cb084d67 --- /dev/null +++ b/doc/api_samples/os-hypervisors/v2.53/hypervisors-show-with-servers-resp.json @@ -0,0 +1,51 @@ +{ + "hypervisor": { + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "state": "up", + "status": "enabled", + "servers": [ + { + "name": "test_server1", + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + { + "name": "test_server2", + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + ], + "current_workload": 0, + "disk_available_least": 0, + "host_ip": "1.1.1.1", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d", + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "043b3cacf6f34c90a7245151fc8ebcda", + "id": "5d343e1d-938e-4284-b98b-6a2b5406ba76", + "disabled_reason": null + }, + "vcpus": 1, + "vcpus_used": 0 + } +} diff --git a/doc/api_samples/os-hypervisors/v2.53/hypervisors-statistics-resp.json b/doc/api_samples/os-hypervisors/v2.53/hypervisors-statistics-resp.json new file mode 100644 index 000000000000..2cfb51e7030a --- /dev/null +++ b/doc/api_samples/os-hypervisors/v2.53/hypervisors-statistics-resp.json @@ -0,0 +1,16 @@ +{ + "hypervisor_statistics": { + "count": 1, + "current_workload": 0, + "disk_available_least": 0, + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "vcpus": 1, + "vcpus_used": 0 + } +} \ No newline at end of file diff --git a/doc/api_samples/os-hypervisors/v2.53/hypervisors-uptime-resp.json b/doc/api_samples/os-hypervisors/v2.53/hypervisors-uptime-resp.json new file mode 100644 index 000000000000..56b97b2bf93c --- /dev/null +++ b/doc/api_samples/os-hypervisors/v2.53/hypervisors-uptime-resp.json @@ -0,0 +1,9 @@ +{ + "hypervisor": { + "hypervisor_hostname": "fake-mini", + "id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d", + "state": "up", + "status": "enabled", + "uptime": " 08:32:11 up 93 days, 18:25, 12 users, load average: 0.20, 0.12, 0.14" + } +} diff --git a/doc/api_samples/os-hypervisors/v2.53/hypervisors-with-servers-resp.json b/doc/api_samples/os-hypervisors/v2.53/hypervisors-with-servers-resp.json new file mode 100644 index 000000000000..d9dca46579fc --- /dev/null +++ b/doc/api_samples/os-hypervisors/v2.53/hypervisors-with-servers-resp.json @@ -0,0 +1,20 @@ +{ + "hypervisors": [ + { + "hypervisor_hostname": "fake-mini", + "id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d", + "state": "up", + "status": "enabled", + "servers": [ + { + "name": "test_server1", + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + { + "name": "test_server2", + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + ] + } + ] +} diff --git a/doc/api_samples/os-hypervisors/v2.53/hypervisors-without-servers-resp.json b/doc/api_samples/os-hypervisors/v2.53/hypervisors-without-servers-resp.json new file mode 100644 index 000000000000..9f8258439b2d --- /dev/null +++ b/doc/api_samples/os-hypervisors/v2.53/hypervisors-without-servers-resp.json @@ -0,0 +1,10 @@ +{ + "hypervisors": [ + { + "hypervisor_hostname": "fake-mini", + "id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d", + "state": "up", + "status": "enabled" + } + ] +} diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 27f7f3f4d436..0be8c905b01d 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -124,9 +124,10 @@ REST_API_VERSION_HISTORY = """REST API Version History: non-admins can see instance action event details except for the traceback field. * 2.52 - Adds support for applying tags when creating a server. - * 2.53 - Service database ids are hidden. The os-services API now returns - a uuid in the id field, and takes a uuid in - DELETE /services/{service_uuid}. + * 2.53 - Service and compute node (hypervisor) database ids are hidden. + The os-services and os-hypervisors APIs now return a uuid in the + id field, and takes a uuid in requests. PUT and GET requests + and responses are also changed. """ # The minimum and maximum versions of the API supported diff --git a/nova/api/openstack/compute/hypervisors.py b/nova/api/openstack/compute/hypervisors.py index fdb8b5d0ea28..1bf20700792d 100644 --- a/nova/api/openstack/compute/hypervisors.py +++ b/nova/api/openstack/compute/hypervisors.py @@ -17,21 +17,29 @@ from oslo_log import log as logging from oslo_serialization import jsonutils +from oslo_utils import strutils +from oslo_utils import uuidutils import webob.exc from nova.api.openstack import api_version_request from nova.api.openstack import common +from nova.api.openstack.compute.schemas import hypervisors as hyper_schema from nova.api.openstack.compute.views import hypervisors as hyper_view from nova.api.openstack import extensions from nova.api.openstack import wsgi +from nova.api import validation +from nova.cells import utils as cells_utils from nova import compute from nova import exception from nova.i18n import _ from nova.policies import hypervisors as hv_policies from nova import servicegroup +from nova import utils LOG = logging.getLogger(__name__) +UUID_FOR_ID_MIN_VERSION = '2.53' + class HypervisorsController(wsgi.Controller): """The Hypervisors API controller for the OpenStack API.""" @@ -46,15 +54,18 @@ class HypervisorsController(wsgi.Controller): def _view_hypervisor(self, hypervisor, service, detail, req, servers=None, **kwargs): alive = self.servicegroup_api.service_is_up(service) + # The 2.53 microversion returns the compute node uuid rather than id. + uuid_for_id = api_version_request.is_supported( + req, min_version=UUID_FOR_ID_MIN_VERSION) hyp_dict = { - 'id': hypervisor.id, + 'id': hypervisor.uuid if uuid_for_id else hypervisor.id, 'hypervisor_hostname': hypervisor.hypervisor_hostname, 'state': 'up' if alive else 'down', 'status': ('disabled' if service.disabled else 'enabled'), } - if detail and not servers: + if detail: for field in ('vcpus', 'memory_mb', 'local_gb', 'vcpus_used', 'memory_mb_used', 'local_gb_used', 'hypervisor_type', 'hypervisor_version', @@ -62,8 +73,9 @@ class HypervisorsController(wsgi.Controller): 'running_vms', 'disk_available_least', 'host_ip'): hyp_dict[field] = getattr(hypervisor, field) + service_id = service.uuid if uuid_for_id else service.id hyp_dict['service'] = { - 'id': service.id, + 'id': service_id, 'host': hypervisor.host, 'disabled_reason': service.disabled_reason, } @@ -108,21 +120,58 @@ class HypervisorsController(wsgi.Controller): context = req.environ['nova.context'] context.can(hv_policies.BASE_POLICY_NAME) - try: - compute_nodes = self.host_api.compute_node_get_all( - context, limit=limit, marker=marker) - except exception.MarkerNotFound: - msg = _('marker [%s] not found') % marker - raise webob.exc.HTTPBadRequest(explanation=msg) + # The 2.53 microversion moves the search and servers routes into + # GET /os-hypervisors and GET /os-hypervisors/detail with query + # parameters. + if api_version_request.is_supported( + req, min_version=UUID_FOR_ID_MIN_VERSION): + hypervisor_match = req.GET.get('hypervisor_hostname_pattern') + with_servers = strutils.bool_from_string( + req.GET.get('with_servers', False), strict=True) + else: + hypervisor_match = None + with_servers = False + + if hypervisor_match is not None: + # We have to check for 'limit' in the request itself because + # the limit passed in is CONF.api.max_limit by default. + if 'limit' in req.GET or marker: + # Paging with hostname pattern isn't supported. + raise webob.exc.HTTPBadRequest( + _('Paging over hypervisors with the ' + 'hypervisor_hostname_pattern query parameter is not ' + 'supported.')) + + # Explicitly do not try to generate links when querying with the + # hostname pattern since the request in the link would fail the + # check above. + links = False + + # Get all compute nodes with a hypervisor_hostname that matches + # the given pattern. If none are found then it's a 404 error. + compute_nodes = self._get_compute_nodes_by_name_pattern( + context, hypervisor_match) + else: + # Get all compute nodes. + try: + compute_nodes = self.host_api.compute_node_get_all( + context, limit=limit, marker=marker) + except exception.MarkerNotFound: + msg = _('marker [%s] not found') % marker + raise webob.exc.HTTPBadRequest(explanation=msg) hypervisors_list = [] for hyp in compute_nodes: try: + instances = None + if with_servers: + instances = self.host_api.instance_get_all_by_host( + context, hyp.host) service = self.host_api.service_get_by_compute_host( context, hyp.host) hypervisors_list.append( self._view_hypervisor( - hyp, service, detail, req)) + hyp, service, detail, req, servers=instances)) except (exception.ComputeHostNotFound, exception.HostMappingNotFound): # The compute service could be deleted which doesn't delete @@ -140,7 +189,21 @@ class HypervisorsController(wsgi.Controller): hypervisors_dict['hypervisors_links'] = hypervisors_links return hypervisors_dict - @wsgi.Controller.api_version("2.33") # noqa + @wsgi.Controller.api_version(UUID_FOR_ID_MIN_VERSION) + @validation.query_schema(hyper_schema.list_query_schema_v253, + UUID_FOR_ID_MIN_VERSION) + @extensions.expected_errors((400, 404)) + def index(self, req): + """Starting with the 2.53 microversion, the id field in the response + is the compute_nodes.uuid value. Also, the search and servers routes + are superseded and replaced with query parameters for listing + hypervisors by a hostname pattern and whether or not to include + hosted servers in the response. + """ + limit, marker = common.get_limit_and_marker(req) + return self._index(req, limit=limit, marker=marker, links=True) + + @wsgi.Controller.api_version("2.33", "2.52") # noqa @extensions.expected_errors((400)) def index(self, req): limit, marker = common.get_limit_and_marker(req) @@ -155,7 +218,21 @@ class HypervisorsController(wsgi.Controller): return self._get_hypervisors(req, detail=False, limit=limit, marker=marker, links=links) - @wsgi.Controller.api_version("2.33") # noqa + @wsgi.Controller.api_version(UUID_FOR_ID_MIN_VERSION) + @validation.query_schema(hyper_schema.list_query_schema_v253, + UUID_FOR_ID_MIN_VERSION) + @extensions.expected_errors((400, 404)) + def detail(self, req): + """Starting with the 2.53 microversion, the id field in the response + is the compute_nodes.uuid value. Also, the search and servers routes + are superseded and replaced with query parameters for listing + hypervisors by a hostname pattern and whether or not to include + hosted servers in the response. + """ + limit, marker = common.get_limit_and_marker(req) + return self._detail(req, limit=limit, marker=marker, links=True) + + @wsgi.Controller.api_version("2.33", "2.52") # noqa @extensions.expected_errors((400)) def detail(self, req): limit, marker = common.get_limit_and_marker(req) @@ -170,12 +247,69 @@ class HypervisorsController(wsgi.Controller): return self._get_hypervisors(req, detail=True, limit=limit, marker=marker, links=links) + @staticmethod + def _validate_id(req, hypervisor_id): + """Validates that the id is a uuid for microversions that require it. + + :param req: The HTTP request object which contains the requested + microversion information. + :param hypervisor_id: The provided hypervisor id. + :raises: webob.exc.HTTPBadRequest if the requested microversion is + greater than or equal to 2.53 and the id is not a uuid. + :raises: webob.exc.HTTPNotFound if the requested microversion is + less than 2.53 and the id is not an integer. + """ + expect_uuid = api_version_request.is_supported( + req, min_version=UUID_FOR_ID_MIN_VERSION) + if expect_uuid: + if not uuidutils.is_uuid_like(hypervisor_id): + msg = _('Invalid uuid %s') % hypervisor_id + raise webob.exc.HTTPBadRequest(explanation=msg) + else: + # This API is supported for cells v1 and as such the id can be + # a cell v1 delimited string, so we have to parse it first. + if cells_utils.CELL_ITEM_SEP in str(hypervisor_id): + hypervisor_id = cells_utils.split_cell_and_item( + hypervisor_id)[1] + try: + utils.validate_integer(hypervisor_id, 'id') + except exception.InvalidInput: + msg = (_("Hypervisor with ID '%s' could not be found.") % + hypervisor_id) + raise webob.exc.HTTPNotFound(explanation=msg) + + @wsgi.Controller.api_version(UUID_FOR_ID_MIN_VERSION) + @validation.query_schema(hyper_schema.show_query_schema_v253, + UUID_FOR_ID_MIN_VERSION) + @extensions.expected_errors((400, 404)) + def show(self, req, id): + """The 2.53 microversion requires that the id is a uuid and as a result + it can also return a 400 response if an invalid uuid is passed. + + The 2.53 microversion also supports the with_servers query parameter + to include a list of servers on the given hypervisor if requested. + """ + with_servers = strutils.bool_from_string( + req.GET.get('with_servers', False), strict=True) + return self._show(req, id, with_servers) + + @wsgi.Controller.api_version("2.1", "2.52") # noqa F811 @extensions.expected_errors(404) def show(self, req, id): + return self._show(req, id) + + def _show(self, req, id, with_servers=False): context = req.environ['nova.context'] context.can(hv_policies.BASE_POLICY_NAME) + + self._validate_id(req, id) + try: hyp = self.host_api.compute_node_get(context, id) + instances = None + if with_servers: + instances = self.host_api.instance_get_all_by_host( + context, hyp.host) service = self.host_api.service_get_by_compute_host( context, hyp.host) except (ValueError, exception.ComputeHostNotFound, @@ -183,12 +317,15 @@ class HypervisorsController(wsgi.Controller): msg = _("Hypervisor with ID '%s' could not be found.") % id raise webob.exc.HTTPNotFound(explanation=msg) return dict(hypervisor=self._view_hypervisor( - hyp, service, True, req)) + hyp, service, True, req, instances)) @extensions.expected_errors((400, 404, 501)) def uptime(self, req, id): context = req.environ['nova.context'] context.can(hv_policies.BASE_POLICY_NAME) + + self._validate_id(req, id) + try: hyp = self.host_api.compute_node_get(context, id) except (ValueError, exception.ComputeHostNotFound): @@ -214,8 +351,14 @@ class HypervisorsController(wsgi.Controller): return dict(hypervisor=self._view_hypervisor(hyp, service, False, req, uptime=uptime)) + @wsgi.Controller.api_version('2.1', '2.52') @extensions.expected_errors(404) def search(self, req, id): + """Prior to microversion 2.53 you could search for hypervisors by a + hostname pattern on a dedicated route. Starting with 2.53, searching + by a hostname pattern is a query parameter in the GET /os-hypervisors + index and detail methods. + """ context = req.environ['nova.context'] context.can(hv_policies.BASE_POLICY_NAME) hypervisors = self._get_compute_nodes_by_name_pattern(context, id) @@ -231,8 +374,15 @@ class HypervisorsController(wsgi.Controller): msg = _("No hypervisor matching '%s' could be found.") % id raise webob.exc.HTTPNotFound(explanation=msg) + @wsgi.Controller.api_version('2.1', '2.52') @extensions.expected_errors(404) def servers(self, req, id): + """Prior to microversion 2.53 you could search for hypervisors by a + hostname pattern and include servers on those hosts in the response on + a dedicated route. Starting with 2.53, searching by a hostname pattern + and including hosted servers is a query parameter in the + GET /os-hypervisors index and detail methods. + """ context = req.environ['nova.context'] context.can(hv_policies.BASE_POLICY_NAME) compute_nodes = self._get_compute_nodes_by_name_pattern(context, id) diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 4df08bcede30..1be4fc8eaa5d 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -656,3 +656,30 @@ user documentation. * ``PUT /os-services/{service_uuid}`` will now return a full service resource representation like in a ``GET`` response + + **os-hypervisors** + + Hypervisors are now identified by uuid instead of database id to ensure + uniqueness across cells. This microversion brings the following changes: + + * ``GET /os-hypervisors/{hypervisor_hostname_pattern}/search`` is deprecated + and replaced with the ``hypervisor_hostname_pattern`` query parameter on + the ``GET /os-hypervisors`` and ``GET /os-hypervisors/detail`` APIs. + Paging with ``hypervisor_hostname_pattern`` is not supported. + * ``GET /os-hypervisors/{hypervisor_hostname_pattern}/servers`` is deprecated + and replaced with the ``with_servers`` query parameter on the + ``GET /os-hypervisors`` and ``GET /os-hypervisors/detail`` APIs. + * ``GET /os-hypervisors/{hypervisor_id}`` supports the ``with_servers`` query + parameter to include hosted server details in the response. + * ``GET /os-hypervisors/{hypervisor_id}`` and + ``GET /os-hypervisors/{hypervisor_id}/uptime`` APIs now take a uuid value + for the ``{hypervisor_id}`` path parameter. + * The ``GET /os-hypervisors`` and ``GET /os-hypervisors/detail`` APIs will + now use a uuid marker for paging across cells. + * The following APIs will now return a uuid value for the hypervisor id and + optionally service id fields in the response: + + * ``GET /os-hypervisors`` + * ``GET /os-hypervisors/detail`` + * ``GET /os-hypervisors/{hypervisor_id}`` + * ``GET /os-hypervisors/{hypervisor_id}/uptime`` diff --git a/nova/api/openstack/compute/schemas/hypervisors.py b/nova/api/openstack/compute/schemas/hypervisors.py new file mode 100644 index 000000000000..38f856dac8ab --- /dev/null +++ b/nova/api/openstack/compute/schemas/hypervisors.py @@ -0,0 +1,43 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.validation import parameter_types + + +list_query_schema_v253 = { + 'type': 'object', + 'properties': { + # The 2.33 microversion added support for paging by limit and marker. + 'limit': parameter_types.single_param( + parameter_types.non_negative_integer), + 'marker': parameter_types.single_param({'type': 'string'}), + # The 2.53 microversion adds support for filtering by hostname pattern + # and requesting hosted servers in the GET /os-hypervisors and + # GET /os-hypervisors/detail response. + 'hypervisor_hostname_pattern': parameter_types.single_param( + parameter_types.hostname), + 'with_servers': parameter_types.single_param( + parameter_types.boolean) + }, + 'additionalProperties': False +} + +show_query_schema_v253 = { + 'type': 'object', + 'properties': { + 'with_servers': parameter_types.single_param( + parameter_types.boolean) + }, + 'additionalProperties': False +} diff --git a/nova/cells/utils.py b/nova/cells/utils.py index b3c29f3821a7..8c3b4563fbd2 100644 --- a/nova/cells/utils.py +++ b/nova/cells/utils.py @@ -34,7 +34,7 @@ PATH_CELL_SEP = '!' # meaningful PATH_CELL_SEP in an invalid way will need to suffice. BLOCK_SYNC_FLAG = '!!' # Separator used between cell name and item -_CELL_ITEM_SEP = '@' +CELL_ITEM_SEP = '@' CONF = nova.conf.CONF @@ -192,12 +192,12 @@ def cell_with_item(cell_name, item): """Turn cell_name and item into @.""" if cell_name is None: return item - return cell_name + _CELL_ITEM_SEP + str(item) + return cell_name + CELL_ITEM_SEP + str(item) def split_cell_and_item(cell_and_item): """Split a combined cell@item and return them.""" - result = cell_and_item.rsplit(_CELL_ITEM_SEP, 1) + result = cell_and_item.rsplit(CELL_ITEM_SEP, 1) if len(result) == 1: return (None, cell_and_item) else: diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-detail-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-detail-resp.json.tpl new file mode 100644 index 000000000000..f218d516f2a2 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-detail-resp.json.tpl @@ -0,0 +1,49 @@ +{ + "hypervisors": [ + { + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "current_workload": 0, + "state": "up", + "status": "enabled", + "disk_available_least": 0, + "host_ip": "%(ip)s", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "id": "%(hypervisor_id)s", + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "%(host_name)s", + "id": "%(service_id)s", + "disabled_reason": null + }, + "vcpus": 1, + "vcpus_used": 0 + } + ], + "hypervisors_links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/hypervisors/detail?limit=1&marker=%(hypervisor_id)s", + "rel": "next" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-detail-with-servers-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-detail-with-servers-resp.json.tpl new file mode 100644 index 000000000000..2fc221df728a --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-detail-with-servers-resp.json.tpl @@ -0,0 +1,53 @@ +{ + "hypervisors": [ + { + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "current_workload": 0, + "state": "up", + "status": "enabled", + "servers": [ + { + "name": "test_server1", + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + { + "name": "test_server2", + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + ], + "disk_available_least": 0, + "host_ip": "%(ip)s", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "id": "%(hypervisor_id)s", + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "%(host_name)s", + "id": "%(service_id)s", + "disabled_reason": null + }, + "vcpus": 1, + "vcpus_used": 0 + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-list-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-list-resp.json.tpl new file mode 100644 index 000000000000..38337772dc15 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-list-resp.json.tpl @@ -0,0 +1,16 @@ +{ + "hypervisors": [ + { + "hypervisor_hostname": "fake-mini", + "id": "%(hypervisor_id)s", + "state": "up", + "status": "enabled" + } + ], + "hypervisors_links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/hypervisors?limit=1&marker=%(hypervisor_id)s", + "rel": "next" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-search-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-search-resp.json.tpl new file mode 100644 index 000000000000..d152e3103d1e --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-search-resp.json.tpl @@ -0,0 +1,10 @@ +{ + "hypervisors": [ + { + "hypervisor_hostname": "fake-mini", + "id": "%(hypervisor_id)s", + "state": "up", + "status": "enabled" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-show-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-show-resp.json.tpl new file mode 100644 index 000000000000..760189ded40b --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-show-resp.json.tpl @@ -0,0 +1,41 @@ +{ + "hypervisor": { + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "current_workload": 0, + "disk_available_least": 0, + "state": "up", + "status": "enabled", + "host_ip": "%(ip)s", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "id": "%(hypervisor_id)s", + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "%(host_name)s", + "id": "%(service_id)s", + "disabled_reason": null + }, + "vcpus": 1, + "vcpus_used": 0 + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-show-with-servers-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-show-with-servers-resp.json.tpl new file mode 100644 index 000000000000..255f60796d7d --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-show-with-servers-resp.json.tpl @@ -0,0 +1,51 @@ +{ + "hypervisor": { + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "current_workload": 0, + "disk_available_least": 0, + "state": "up", + "status": "enabled", + "servers": [ + { + "name": "test_server1", + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + { + "name": "test_server2", + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + ], + "host_ip": "%(ip)s", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "id": "%(hypervisor_id)s", + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "%(host_name)s", + "id": "%(service_id)s", + "disabled_reason": null + }, + "vcpus": 1, + "vcpus_used": 0 + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-statistics-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-statistics-resp.json.tpl new file mode 100644 index 000000000000..2cfb51e7030a --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-statistics-resp.json.tpl @@ -0,0 +1,16 @@ +{ + "hypervisor_statistics": { + "count": 1, + "current_workload": 0, + "disk_available_least": 0, + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "vcpus": 1, + "vcpus_used": 0 + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-uptime-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-uptime-resp.json.tpl new file mode 100644 index 000000000000..7854d754bf87 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-uptime-resp.json.tpl @@ -0,0 +1,9 @@ +{ + "hypervisor": { + "hypervisor_hostname": "fake-mini", + "id": "%(hypervisor_id)s", + "state": "up", + "status": "enabled", + "uptime": " 08:32:11 up 93 days, 18:25, 12 users, load average: 0.20, 0.12, 0.14" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-with-servers-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-with-servers-resp.json.tpl new file mode 100644 index 000000000000..d790fdd02302 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-with-servers-resp.json.tpl @@ -0,0 +1,20 @@ +{ + "hypervisors": [ + { + "hypervisor_hostname": "fake-mini", + "id": "%(hypervisor_id)s", + "state": "up", + "status": "enabled", + "servers": [ + { + "name": "test_server1", + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + { + "name": "test_server2", + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + ] + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-without-servers-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-without-servers-resp.json.tpl new file mode 100644 index 000000000000..d152e3103d1e --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-hypervisors/v2.53/hypervisors-without-servers-resp.json.tpl @@ -0,0 +1,10 @@ +{ + "hypervisors": [ + { + "hypervisor_hostname": "fake-mini", + "id": "%(hypervisor_id)s", + "state": "up", + "status": "enabled" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/test_hypervisors.py b/nova/tests/functional/api_sample_tests/test_hypervisors.py index 0b14343a709d..b52903921106 100644 --- a/nova/tests/functional/api_sample_tests/test_hypervisors.py +++ b/nova/tests/functional/api_sample_tests/test_hypervisors.py @@ -169,3 +169,147 @@ class HypervisorsSampleJson233Tests(api_sample_base.ApiSampleTestBaseV21): } response = self._do_get('os-hypervisors/detail?limit=1&marker=1') self._verify_response('hypervisors-detail-resp', subs, response, 200) + + +class HypervisorsSampleJson253Tests(HypervisorsSampleJson228Tests): + microversion = '2.53' + scenarios = [('v2_53', {'api_major_version': 'v2.1'})] + + def setUp(self): + super(HypervisorsSampleJson253Tests, self).setUp() + self.compute_node_1 = self.compute.service_ref.compute_node + + def generalize_subs(self, subs, vanilla_regexes): + """Give the test a chance to modify subs after the server response + was verified, and before the on-disk doc/api_samples file is checked. + """ + # When comparing the template to the sample we just care that the + # hypervisor id and service id are UUIDs. + subs['hypervisor_id'] = vanilla_regexes['uuid'] + subs['service_id'] = vanilla_regexes['uuid'] + return subs + + def test_hypervisors_list(self): + # Start another compute service to get a 2nd compute for paging tests. + compute_node_2 = self.start_service( + 'compute', host='host2').service_ref.compute_node + marker = self.compute_node_1.uuid + response = self._do_get('os-hypervisors?limit=1&marker=%s' % marker) + subs = {'hypervisor_id': compute_node_2.uuid} + self._verify_response('hypervisors-list-resp', subs, response, 200) + + def test_hypervisors_detail(self): + # Start another compute service to get a 2nd compute for paging tests. + service_2 = self.start_service('compute', host='host2').service_ref + compute_node_2 = service_2.compute_node + marker = self.compute_node_1.uuid + subs = { + 'hypervisor_id': compute_node_2.uuid, + 'service_id': service_2.uuid + } + response = self._do_get('os-hypervisors/detail?limit=1&marker=%s' % + marker) + self._verify_response('hypervisors-detail-resp', subs, response, 200) + + @mock.patch("nova.compute.api.HostAPI.instance_get_all_by_host") + def test_hypervisors_detail_with_servers(self, instance_get_all_by_host): + """List hypervisors with details and with hosted servers.""" + instances = [ + { + "name": "test_server1", + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + { + "name": "test_server2", + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + }] + instance_get_all_by_host.return_value = instances + response = self._do_get('os-hypervisors/detail?with_servers=1') + subs = { + 'hypervisor_id': self.compute_node_1.uuid, + 'service_id': self.compute.service_ref.uuid, + } + self._verify_response('hypervisors-detail-with-servers-resp', + subs, response, 200) + + def test_hypervisors_search(self): + """The search route is deprecated in 2.53 and is now a query parameter + on the GET /os-hypervisors API. + """ + response = self._do_get( + 'os-hypervisors?hypervisor_hostname_pattern=fake') + subs = {'hypervisor_id': self.compute_node_1.uuid} + self._verify_response('hypervisors-search-resp', subs, response, 200) + + @mock.patch("nova.compute.api.HostAPI.instance_get_all_by_host") + def test_hypervisors_with_servers(self, instance_get_all_by_host): + """The servers route is deprecated in 2.53 and is now a query parameter + on the GET /os-hypervisors API. + """ + instances = [ + { + "name": "test_server1", + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + { + "name": "test_server2", + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + }] + instance_get_all_by_host.return_value = instances + response = self._do_get('os-hypervisors?with_servers=true') + subs = {'hypervisor_id': self.compute_node_1.uuid} + self._verify_response('hypervisors-with-servers-resp', subs, + response, 200) + + def test_hypervisors_without_servers(self): + # This is the same as GET /os-hypervisors in 2.53 which is covered by + # test_hypervisors_list already. + pass + + def test_hypervisors_uptime(self): + def fake_get_host_uptime(self, context, hyp): + return (" 08:32:11 up 93 days, 18:25, 12 users, load average:" + " 0.20, 0.12, 0.14") + + self.stub_out('nova.compute.api.HostAPI.get_host_uptime', + fake_get_host_uptime) + hypervisor_id = self.compute_node_1.uuid + response = self._do_get('os-hypervisors/%s/uptime' % hypervisor_id) + subs = { + 'hypervisor_id': hypervisor_id, + } + self._verify_response('hypervisors-uptime-resp', subs, response, 200) + + def test_hypervisors_show(self): + hypervisor_id = self.compute_node_1.uuid + subs = { + 'hypervisor_id': hypervisor_id, + 'service_id': self.compute.service_ref.uuid, + } + response = self._do_get('os-hypervisors/%s' % hypervisor_id) + self._verify_response('hypervisors-show-resp', subs, response, 200) + + @mock.patch("nova.compute.api.HostAPI.instance_get_all_by_host") + def test_hypervisors_show_with_servers(self, instance_get_all_by_host): + """Tests getting details for a specific hypervisor and including the + hosted servers in the response. + """ + instances = [ + { + "name": "test_server1", + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + { + "name": "test_server2", + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + }] + instance_get_all_by_host.return_value = instances + hypervisor_id = self.compute_node_1.uuid + subs = { + 'hypervisor_id': hypervisor_id, + 'service_id': self.compute.service_ref.uuid, + } + response = self._do_get('os-hypervisors/%s?with_servers=1' % + hypervisor_id) + self._verify_response('hypervisors-show-with-servers-resp', subs, + response, 200) diff --git a/nova/tests/unit/api/openstack/compute/test_extended_hypervisors.py b/nova/tests/unit/api/openstack/compute/test_extended_hypervisors.py index 59505c0bb4a9..8bcb79d6fb1d 100644 --- a/nova/tests/unit/api/openstack/compute/test_extended_hypervisors.py +++ b/nova/tests/unit/api/openstack/compute/test_extended_hypervisors.py @@ -48,6 +48,8 @@ class ExtendedHypervisorsTestV21(test.NoDBTestCase): del DETAIL_HYPERS_DICTS[1]['service_id'] del DETAIL_HYPERS_DICTS[0]['host'] del DETAIL_HYPERS_DICTS[1]['host'] + del DETAIL_HYPERS_DICTS[0]['uuid'] + del DETAIL_HYPERS_DICTS[1]['uuid'] DETAIL_HYPERS_DICTS[0].update({'state': 'up', 'status': 'enabled', 'service': dict(id=1, host='compute1', diff --git a/nova/tests/unit/api/openstack/compute/test_hypervisors.py b/nova/tests/unit/api/openstack/compute/test_hypervisors.py index 29e7303d12d5..2932a133972e 100644 --- a/nova/tests/unit/api/openstack/compute/test_hypervisors.py +++ b/nova/tests/unit/api/openstack/compute/test_hypervisors.py @@ -18,6 +18,7 @@ import copy import mock import netaddr from oslo_serialization import jsonutils +import six from webob import exc from nova.api.openstack.compute import hypervisors \ @@ -39,6 +40,7 @@ CPU_INFO = """ TEST_HYPERS = [ dict(id=1, + uuid=uuids.hyper1, service_id=1, host="compute1", vcpus=4, @@ -58,6 +60,7 @@ TEST_HYPERS = [ disk_available_least=100, host_ip=netaddr.IPAddress('1.1.1.1')), dict(id=2, + uuid=uuids.hyper2, service_id=2, host="compute2", vcpus=4, @@ -80,6 +83,7 @@ TEST_HYPERS = [ TEST_SERVICES = [ objects.Service(id=1, + uuid=uuids.service1, host="compute1", binary="nova-compute", topic="compute_topic", @@ -88,6 +92,7 @@ TEST_SERVICES = [ disabled_reason=None, availability_zone="nova"), objects.Service(id=2, + uuid=uuids.service2, host="compute2", binary="nova-compute", topic="compute_topic", @@ -110,12 +115,13 @@ TEST_SERVERS = [dict(name="inst1", uuid=uuids.instance_1, host="compute1"), def fake_compute_node_get_all(context, limit=None, marker=None): - if marker in ['99999']: + if marker in ['99999', uuids.invalid_marker]: raise exception.MarkerNotFound(marker) marker_found = True if marker is None else False output = [] for hyper in TEST_HYPERS_OBJ: - if not marker_found and marker == str(hyper.id): + # Starting with the 2.53 microversion, the marker is a uuid. + if not marker_found and marker in (str(hyper.id), hyper.uuid): marker_found = True elif marker_found: if limit is None or len(output) < int(limit): @@ -129,7 +135,7 @@ def fake_compute_node_search_by_hypervisor(context, hypervisor_re): def fake_compute_node_get(context, compute_id): for hyper in TEST_HYPERS_OBJ: - if hyper.id == int(compute_id): + if hyper.uuid == compute_id or hyper.id == int(compute_id): return hyper raise exception.ComputeHostNotFound(host=compute_id) @@ -177,6 +183,9 @@ def fake_instance_get_all_by_host(context, host): class HypervisorsTestV21(test.NoDBTestCase): api_version = '2.1' + # Allow subclasses to override if the id value in the response is the + # compute node primary key integer id or the uuid. + expect_uuid_for_id = False # copying the objects locally so the cells testcases can provide their own TEST_HYPERS_OBJ = copy.deepcopy(TEST_HYPERS_OBJ) @@ -188,6 +197,8 @@ class HypervisorsTestV21(test.NoDBTestCase): del DETAIL_HYPERS_DICTS[1]['service_id'] del DETAIL_HYPERS_DICTS[0]['host'] del DETAIL_HYPERS_DICTS[1]['host'] + del DETAIL_HYPERS_DICTS[0]['uuid'] + del DETAIL_HYPERS_DICTS[1]['uuid'] DETAIL_HYPERS_DICTS[0].update({'state': 'up', 'status': 'enabled', 'service': dict(id=1, host='compute1', @@ -213,6 +224,15 @@ class HypervisorsTestV21(test.NoDBTestCase): self.controller.servicegroup_api.service_is_up = mock.MagicMock( return_value=True) + def _get_hyper_id(self): + """Helper function to get the proper hypervisor id for a request + + :returns: The first hypervisor's uuid for microversions that expect a + uuid for the id, otherwise the hypervisor's id primary key + """ + return (self.TEST_HYPERS_OBJ[0].uuid if self.expect_uuid_for_id + else self.TEST_HYPERS_OBJ[0].id) + def setUp(self): super(HypervisorsTestV21, self).setUp() self._set_up_controller() @@ -317,7 +337,8 @@ class HypervisorsTestV21(test.NoDBTestCase): result = self.controller.index(req) self.assertEqual(1, len(result['hypervisors'])) expected = { - 'id': compute_nodes[0].id, + 'id': compute_nodes[0].uuid if self.expect_uuid_for_id + else compute_nodes[0].id, 'hypervisor_hostname': compute_nodes[0].hypervisor_hostname, 'state': 'up', 'status': 'enabled', @@ -350,7 +371,8 @@ class HypervisorsTestV21(test.NoDBTestCase): result = self.controller.index(req) self.assertEqual(1, len(result['hypervisors'])) expected = { - 'id': compute_nodes[0].id, + 'id': compute_nodes[0].uuid if self.expect_uuid_for_id + else compute_nodes[0].id, 'hypervisor_hostname': compute_nodes[0].hypervisor_hostname, 'state': 'up', 'status': 'enabled', @@ -461,31 +483,25 @@ class HypervisorsTestV21(test.NoDBTestCase): don't fail when listing hypervisors. """ - # two computes, a matching service only exists for the first one - compute_nodes = objects.ComputeNodeList(objects=[ - objects.ComputeNode(**TEST_HYPERS[0]), - objects.ComputeNode(**TEST_HYPERS[1]) - ]) - - def fake_service_get_by_compute_host(context, host): - return TEST_SERVICES[0] - @mock.patch.object(self.controller.host_api, 'compute_node_get', - fake_service_get_by_compute_host) + return_value=self.TEST_HYPERS_OBJ[0]) @mock.patch.object(self.controller.host_api, 'service_get_by_compute_host') - def _test(self, mock_service): + def _test(self, mock_service, mock_compute_node_get): req = self._get_request(True) mock_service.side_effect = exception.HostMappingNotFound( name='foo') + hyper_id = self._get_hyper_id() self.assertRaises(exc.HTTPNotFound, self.controller.show, - req, compute_nodes[0].id) + req, hyper_id) self.assertTrue(mock_service.called) + mock_compute_node_get.assert_called_once_with(mock.ANY, hyper_id) _test(self) def test_show_noid(self): req = self._get_request(True) - self.assertRaises(exc.HTTPNotFound, self.controller.show, req, '3') + hyperid = uuids.hyper3 if self.expect_uuid_for_id else '3' + self.assertRaises(exc.HTTPNotFound, self.controller.show, req, hyperid) def test_show_non_integer_id(self): req = self._get_request(True) @@ -493,7 +509,8 @@ class HypervisorsTestV21(test.NoDBTestCase): def test_show_withid(self): req = self._get_request(True) - result = self.controller.show(req, self.TEST_HYPERS_OBJ[0].id) + hyper_id = self._get_hyper_id() + result = self.controller.show(req, hyper_id) self.assertEqual(dict(hypervisor=self.DETAIL_HYPERS_DICTS[0]), result) @@ -501,20 +518,22 @@ class HypervisorsTestV21(test.NoDBTestCase): req = self._get_request(False) self.assertRaises(exception.PolicyNotAuthorized, self.controller.show, req, - self.TEST_HYPERS_OBJ[0].id) + self._get_hyper_id()) def test_uptime_noid(self): req = self._get_request(True) - self.assertRaises(exc.HTTPNotFound, self.controller.uptime, req, '3') + hyper_id = uuids.hyper3 if self.expect_uuid_for_id else '3' + self.assertRaises(exc.HTTPNotFound, self.controller.uptime, req, + hyper_id) def test_uptime_notimplemented(self): with mock.patch.object(self.controller.host_api, 'get_host_uptime', side_effect=exc.HTTPNotImplemented() ) as mock_get_uptime: req = self._get_request(True) + hyper_id = self._get_hyper_id() self.assertRaises(exc.HTTPNotImplemented, - self.controller.uptime, req, - self.TEST_HYPERS_OBJ[0].id) + self.controller.uptime, req, hyper_id) self.assertEqual(1, mock_get_uptime.call_count) def test_uptime_implemented(self): @@ -522,7 +541,8 @@ class HypervisorsTestV21(test.NoDBTestCase): return_value="fake uptime" ) as mock_get_uptime: req = self._get_request(True) - result = self.controller.uptime(req, self.TEST_HYPERS_OBJ[0].id) + hyper_id = self._get_hyper_id() + result = self.controller.uptime(req, hyper_id) expected_dict = copy.deepcopy(self.INDEX_HYPER_DICTS[0]) expected_dict.update({'uptime': "fake uptime"}) @@ -544,9 +564,9 @@ class HypervisorsTestV21(test.NoDBTestCase): side_effect=exception.ComputeServiceUnavailable(host='dummy') ) as mock_get_uptime: req = self._get_request(True) + hyper_id = self._get_hyper_id() self.assertRaises(exc.HTTPBadRequest, - self.controller.uptime, req, - self.TEST_HYPERS_OBJ[0].id) + self.controller.uptime, req, hyper_id) mock_get_uptime.assert_called_once_with( mock.ANY, self.TEST_HYPERS_OBJ[0].host) @@ -559,9 +579,9 @@ class HypervisorsTestV21(test.NoDBTestCase): name='dummy')) def _test(mock_get, _, __): req = self._get_request(True) + hyper_id = self._get_hyper_id() self.assertRaises(exc.HTTPNotFound, - self.controller.uptime, req, - self.TEST_HYPERS_OBJ[0].id) + self.controller.uptime, req, hyper_id) self.assertTrue(mock_get.called) _test() @@ -571,9 +591,9 @@ class HypervisorsTestV21(test.NoDBTestCase): side_effect=exception.HostMappingNotFound(name='dummy') ) as mock_get_uptime: req = self._get_request(True) + hyper_id = self._get_hyper_id() self.assertRaises(exc.HTTPNotFound, - self.controller.uptime, req, - self.TEST_HYPERS_OBJ[0].id) + self.controller.uptime, req, hyper_id) mock_get_uptime.assert_called_once_with( mock.ANY, self.TEST_HYPERS_OBJ[0].host) @@ -872,3 +892,398 @@ class HypervisorsTestV233(HypervisorsTestV228): '/v2/1234/os-hypervisors/detail?marker=99999') self.assertRaises(exc.HTTPBadRequest, self.controller.detail, req) + + +class HypervisorsTestV252(HypervisorsTestV233): + """This is a boundary test to make sure 2.52 works like 2.33.""" + api_version = '2.52' + + +class HypervisorsTestV253(HypervisorsTestV252): + api_version = hypervisors_v21.UUID_FOR_ID_MIN_VERSION + expect_uuid_for_id = True + + # This is an expected response for index(). + INDEX_HYPER_DICTS = [ + dict(id=uuids.hyper1, hypervisor_hostname="hyper1", + state='up', status='enabled'), + dict(id=uuids.hyper2, hypervisor_hostname="hyper2", + state='up', status='enabled')] + + def setUp(self): + super(HypervisorsTestV253, self).setUp() + # This is an expected response for detail(). + for index, detail_hyper_dict in enumerate(self.DETAIL_HYPERS_DICTS): + detail_hyper_dict['id'] = TEST_HYPERS[index]['uuid'] + detail_hyper_dict['service']['id'] = TEST_SERVICES[index].uuid + + def test_servers(self): + """Asserts that calling the servers route after 2.48 fails.""" + self.assertRaises(exception.VersionNotFoundForAPIMethod, + self.controller.servers, + self._get_request(True), 'hyper') + + def test_servers_with_no_server(self): + """Tests GET /os-hypervisors?with_servers=1 when there are no + instances on the given host. + """ + with mock.patch.object(self.controller.host_api, + 'instance_get_all_by_host', + return_value=[]) as mock_inst_get_all: + req = self._get_request(use_admin_context=True, + url='/os-hypervisors?with_servers=1') + result = self.controller.index(req) + self.assertEqual(dict(hypervisors=self.INDEX_HYPER_DICTS), result) + # instance_get_all_by_host is called for each hypervisor + self.assertEqual(2, mock_inst_get_all.call_count) + mock_inst_get_all.assert_has_calls(( + mock.call(req.environ['nova.context'], TEST_HYPERS_OBJ[0].host), + mock.call(req.environ['nova.context'], TEST_HYPERS_OBJ[1].host))) + + def test_servers_not_mapped(self): + """Tests that instance_get_all_by_host fails with HostMappingNotFound. + """ + req = self._get_request(use_admin_context=True, + url='/os-hypervisors?with_servers=1') + with mock.patch.object( + self.controller.host_api, 'instance_get_all_by_host', + side_effect=exception.HostMappingNotFound(name='something')): + result = self.controller.index(req) + self.assertEqual(dict(hypervisors=[]), result) + + def test_list_with_servers(self): + """Tests GET /os-hypervisors?with_servers=True""" + instances = [ + objects.InstanceList(objects=[objects.Instance( + id=1, uuid=uuids.hyper1_instance1)]), + objects.InstanceList(objects=[objects.Instance( + id=2, uuid=uuids.hyper2_instance1)])] + with mock.patch.object(self.controller.host_api, + 'instance_get_all_by_host', + side_effect=instances) as mock_inst_get_all: + req = self._get_request(use_admin_context=True, + url='/os-hypervisors?with_servers=True') + result = self.controller.index(req) + index_with_servers = copy.deepcopy(self.INDEX_HYPER_DICTS) + index_with_servers[0]['servers'] = [ + {'name': 'instance-00000001', 'uuid': uuids.hyper1_instance1}] + index_with_servers[1]['servers'] = [ + {'name': 'instance-00000002', 'uuid': uuids.hyper2_instance1}] + self.assertEqual(dict(hypervisors=index_with_servers), result) + # instance_get_all_by_host is called for each hypervisor + self.assertEqual(2, mock_inst_get_all.call_count) + mock_inst_get_all.assert_has_calls(( + mock.call(req.environ['nova.context'], TEST_HYPERS_OBJ[0].host), + mock.call(req.environ['nova.context'], TEST_HYPERS_OBJ[1].host))) + + def test_list_with_servers_invalid_parameter(self): + """Tests using an invalid with_servers query parameter.""" + req = self._get_request(use_admin_context=True, + url='/os-hypervisors?with_servers=invalid') + self.assertRaises( + exception.ValidationError, self.controller.index, req) + + def test_list_with_hostname_pattern_and_paging_parameters(self): + """This is a negative test to validate that trying to list hypervisors + with a hostname pattern and paging parameters results in a 400 error. + """ + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors?hypervisor_hostname_pattern=foo&' + 'limit=1&marker=%s' % uuids.marker) + ex = self.assertRaises(exc.HTTPBadRequest, self.controller.index, req) + self.assertIn('Paging over hypervisors with the ' + 'hypervisor_hostname_pattern query parameter is not ' + 'supported.', six.text_type(ex)) + + def test_servers_with_non_integer_hypervisor_id(self): + """This is a poorly named test, it's really checking the 404 case where + there is no match for the hostname pattern. + """ + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors?with_servers=yes&' + 'hypervisor_hostname_pattern=shenzhen') + with mock.patch.object(self.controller.host_api, + 'compute_node_search_by_hypervisor', + return_value=objects.ComputeNodeList()) as s: + self.assertRaises(exc.HTTPNotFound, self.controller.index, req) + s.assert_called_once_with(req.environ['nova.context'], 'shenzhen') + + def test_servers_non_admin(self): + """There is no reason to test this for 2.53 since the + /os-hypervisors/servers route is deprecated. + """ + pass + + def test_servers_non_id(self): + """There is no reason to test this for 2.53 since the + /os-hypervisors/servers route is deprecated. + """ + pass + + def test_search_old_route(self): + """Asserts that calling the search route after 2.48 fails.""" + self.assertRaises(exception.VersionNotFoundForAPIMethod, + self.controller.search, + self._get_request(True), 'hyper') + + def test_search(self): + """Test listing hypervisors with details and using the + hypervisor_hostname_pattern query string. + """ + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors?hypervisor_hostname_pattern=shenzhen') + with mock.patch.object(self.controller.host_api, + 'compute_node_search_by_hypervisor', + return_value=objects.ComputeNodeList( + objects=[TEST_HYPERS_OBJ[0]])) as s: + result = self.controller.detail(req) + s.assert_called_once_with(req.environ['nova.context'], 'shenzhen') + + expected = { + 'hypervisors': [ + {'cpu_info': {'arch': 'x86_64', + 'features': [], + 'model': '', + 'topology': {'cores': 1, + 'sockets': 1, + 'threads': 1}, + 'vendor': 'fake'}, + 'current_workload': 2, + 'disk_available_least': 100, + 'free_disk_gb': 125, + 'free_ram_mb': 5120, + 'host_ip': netaddr.IPAddress('1.1.1.1'), + 'hypervisor_hostname': 'hyper1', + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'id': TEST_HYPERS_OBJ[0].uuid, + 'local_gb': 250, + 'local_gb_used': 125, + 'memory_mb': 10240, + 'memory_mb_used': 5120, + 'running_vms': 2, + 'service': {'disabled_reason': None, + 'host': 'compute1', + 'id': TEST_SERVICES[0].uuid}, + 'state': 'up', + 'status': 'enabled', + 'vcpus': 4, + 'vcpus_used': 2} + ] + } + # There are no links when using the hypervisor_hostname_pattern + # query string since we can't page using a pattern matcher. + self.assertNotIn('hypervisors_links', result) + self.assertDictEqual(expected, result) + + def test_search_invalid_hostname_pattern_parameter(self): + """Tests passing an invalid hypervisor_hostname_pattern query + parameter. + """ + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors?hypervisor_hostname_pattern=invalid~host') + self.assertRaises( + exception.ValidationError, self.controller.detail, req) + + def test_search_non_exist(self): + """This is a duplicate of test_servers_with_non_integer_hypervisor_id. + """ + pass + + def test_search_non_admin(self): + """There is no reason to test this for 2.53 since the + /os-hypervisors/search route is deprecated. + """ + pass + + def test_search_unmapped(self): + """This is already tested with test_index_compute_host_not_mapped.""" + pass + + def test_show_non_integer_id(self): + """There is no reason to test this for 2.53 since 2.53 requires a + non-integer id (requires a uuid). + """ + pass + + def test_show_integer_id(self): + """Tests that we get a 400 if passed a hypervisor integer id to show(). + """ + req = self._get_request(True) + ex = self.assertRaises(exc.HTTPBadRequest, + self.controller.show, req, '1') + self.assertIn('Invalid uuid 1', six.text_type(ex)) + + def test_show_with_servers_invalid_parameter(self): + """Tests passing an invalid value for the with_servers query parameter + to the show() method to make sure the query parameter is validated. + """ + hyper_id = self._get_hyper_id() + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors/%s?with_servers=invalid' % hyper_id) + ex = self.assertRaises( + exception.ValidationError, self.controller.show, req, hyper_id) + self.assertIn('with_servers', six.text_type(ex)) + + def test_show_with_servers_host_mapping_not_found(self): + """Tests that a 404 is returned if instance_get_all_by_host raises + HostMappingNotFound. + """ + hyper_id = self._get_hyper_id() + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors/%s?with_servers=true' % hyper_id) + with mock.patch.object( + self.controller.host_api, 'instance_get_all_by_host', + side_effect=exception.HostMappingNotFound(name=hyper_id)): + self.assertRaises(exc.HTTPNotFound, self.controller.show, + req, hyper_id) + + def test_show_with_servers(self): + """Tests the show() result when servers are included in the output.""" + instances = objects.InstanceList(objects=[objects.Instance( + id=1, uuid=uuids.hyper1_instance1)]) + hyper_id = self._get_hyper_id() + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors/%s?with_servers=on' % hyper_id) + with mock.patch.object(self.controller.host_api, + 'instance_get_all_by_host', + return_value=instances) as mock_inst_get_all: + result = self.controller.show(req, hyper_id) + show_with_servers = copy.deepcopy(self.DETAIL_HYPERS_DICTS[0]) + show_with_servers['servers'] = [ + {'name': 'instance-00000001', 'uuid': uuids.hyper1_instance1}] + self.assertDictEqual(dict(hypervisor=show_with_servers), result) + # instance_get_all_by_host is called + mock_inst_get_all.assert_called_once_with( + req.environ['nova.context'], TEST_HYPERS_OBJ[0].host) + + def test_uptime_non_integer_id(self): + """There is no reason to test this for 2.53 since 2.53 requires a + non-integer id (requires a uuid). + """ + pass + + def test_uptime_integer_id(self): + """Tests that we get a 400 if passed a hypervisor integer id to + uptime(). + """ + req = self._get_request(True) + ex = self.assertRaises(exc.HTTPBadRequest, + self.controller.uptime, req, '1') + self.assertIn('Invalid uuid 1', six.text_type(ex)) + + def test_detail_pagination(self): + """Tests details paging with uuid markers.""" + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors/detail?limit=1&marker=%s' % + TEST_HYPERS_OBJ[0].uuid) + result = self.controller.detail(req) + link = ('http://localhost/v2/hypervisors/detail?limit=1&marker=%s' % + TEST_HYPERS_OBJ[1].uuid) + expected = { + 'hypervisors': [ + {'cpu_info': {'arch': 'x86_64', + 'features': [], + 'model': '', + 'topology': {'cores': 1, + 'sockets': 1, + 'threads': 1}, + 'vendor': 'fake'}, + 'current_workload': 2, + 'disk_available_least': 100, + 'free_disk_gb': 125, + 'free_ram_mb': 5120, + 'host_ip': netaddr.IPAddress('2.2.2.2'), + 'hypervisor_hostname': 'hyper2', + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'id': TEST_HYPERS_OBJ[1].uuid, + 'local_gb': 250, + 'local_gb_used': 125, + 'memory_mb': 10240, + 'memory_mb_used': 5120, + 'running_vms': 2, + 'service': {'disabled_reason': None, + 'host': 'compute2', + 'id': TEST_SERVICES[1].uuid}, + 'state': 'up', + 'status': 'enabled', + 'vcpus': 4, + 'vcpus_used': 2} + ], + 'hypervisors_links': [{'href': link, 'rel': 'next'}] + } + self.assertEqual(expected, result) + + def test_detail_pagination_with_invalid_marker(self): + """Tests detail paging with an invalid marker (not found).""" + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors/detail?marker=%s' % uuids.invalid_marker) + self.assertRaises(exc.HTTPBadRequest, + self.controller.detail, req) + + def test_index_pagination(self): + """Tests index paging with uuid markers.""" + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors?limit=1&marker=%s' % + TEST_HYPERS_OBJ[0].uuid) + result = self.controller.index(req) + link = ('http://localhost/v2/hypervisors?limit=1&marker=%s' % + TEST_HYPERS_OBJ[1].uuid) + expected = { + 'hypervisors': [{ + 'hypervisor_hostname': 'hyper2', + 'id': TEST_HYPERS_OBJ[1].uuid, + 'state': 'up', + 'status': 'enabled' + }], + 'hypervisors_links': [{'href': link, 'rel': 'next'}] + } + self.assertEqual(expected, result) + + def test_index_pagination_with_invalid_marker(self): + """Tests index paging with an invalid marker (not found).""" + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors?marker=%s' % uuids.invalid_marker) + self.assertRaises(exc.HTTPBadRequest, + self.controller.index, req) + + def test_list_duplicate_query_parameters_validation(self): + """Tests that the list query parameter schema enforces only a single + entry for any query parameter. + """ + params = { + 'limit': 1, + 'marker': uuids.marker, + 'hypervisor_hostname_pattern': 'foo', + 'with_servers': 'true' + } + for param, value in params.items(): + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors?%s=%s&%s=%s' % + (param, value, param, value)) + self.assertRaises(exception.ValidationError, + self.controller.index, req) + + def test_show_duplicate_query_parameters_validation(self): + """Tests that the show query parameter schema enforces only a single + entry for any query parameter. + """ + req = self._get_request( + use_admin_context=True, + url='/os-hypervisors/%s?with_servers=1&with_servers=1' % + uuids.hyper1) + self.assertRaises(exception.ValidationError, + self.controller.show, req, uuids.hyper1) diff --git a/nova/tests/unit/cells/test_cells_utils.py b/nova/tests/unit/cells/test_cells_utils.py index a6c815b30fee..c2eca53f7e1f 100644 --- a/nova/tests/unit/cells/test_cells_utils.py +++ b/nova/tests/unit/cells/test_cells_utils.py @@ -147,7 +147,7 @@ class CellsUtilsTestCase(test.NoDBTestCase): cell = cells_utils.PATH_CELL_SEP.join(path) item = 'host_5' together = cells_utils.cell_with_item(cell, item) - self.assertEqual(cells_utils._CELL_ITEM_SEP.join([cell, item]), + self.assertEqual(cells_utils.CELL_ITEM_SEP.join([cell, item]), together) # Test normal usage diff --git a/releasenotes/notes/bp-service-hyper-uuid-in-api-cc7b9f21cc458e1b.yaml b/releasenotes/notes/bp-service-hyper-uuid-in-api-cc7b9f21cc458e1b.yaml index 3efd1aa3ad93..30e88dcbef7d 100644 --- a/releasenotes/notes/bp-service-hyper-uuid-in-api-cc7b9f21cc458e1b.yaml +++ b/releasenotes/notes/bp-service-hyper-uuid-in-api-cc7b9f21cc458e1b.yaml @@ -1,9 +1,9 @@ --- features: - | - Microversion 2.53 changes service IDs to UUIDs to ensure uniqueness across - cells. Prior to this, ID collisions were possible in multi-cell - deployments. See the `REST API Version History`_ and + Microversion 2.53 changes service and hypervisor IDs to UUIDs to ensure + uniqueness across cells. Prior to this, ID collisions were possible in + multi-cell deployments. See the `REST API Version History`_ and `Compute API reference`_ for details. .. _REST API Version History: https://docs.openstack.org/developer/nova/api_microversion_history.html