Handle uuids in os-hypervisors API

There are quite a few changes here as this is not only handling
uuids for the hypervisor id but it's also a refactor in several
APIs for consistency.

The main changes are detailed in the REST API Version History
doc in this change, but to summarize the changes:

* Hypervisor and service IDs are handled as the UUIDs for those
  resources; this is necessary for accurately working with these
  resources across multiple cells.
* The 'servers' and 'search' routes are deprecated and folded into
  the index and detail methods as query parameters, validated using
  json schema.
* The show method will also be able to return the list of servers
  hosted on the given hypervisor using the with_servers query
  parameter.
* The marker used when paging over lists of hypervisors is the
  compute node UUID.
* Using the hypervisor_hostname_pattern query parameter will not
  work with paging parameters.
* API reference docs are updated for the detailed changes.
* Functional and unit tests are provided for all changes.

Part of blueprint service-hyper-uuid-in-api

Change-Id: I828350c179df8bcfa4739910abeafaba2f96982b
This commit is contained in:
Matt Riedemann 2017-07-18 15:38:07 -04:00
parent 2f7bf29d47
commit 622bfb2e95
32 changed files with 1520 additions and 61 deletions

View File

@ -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

View File

@ -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.
@ -2965,6 +3003,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
@ -2982,6 +3034,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.

View File

@ -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"
}
]
}

View File

@ -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
}
]
}

View File

@ -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"
}
]
}

View File

@ -0,0 +1,10 @@
{
"hypervisors": [
{
"hypervisor_hostname": "fake-mini",
"id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d",
"state": "up",
"status": "enabled"
}
]
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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"
}
]
}
]
}

View File

@ -0,0 +1,10 @@
{
"hypervisors": [
{
"hypervisor_hostname": "fake-mini",
"id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d",
"state": "up",
"status": "enabled"
}
]
}

View File

@ -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

View File

@ -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)

View File

@ -657,3 +657,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``

View File

@ -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
}

View File

@ -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 <cell_name>@<item>."""
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:

View File

@ -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"
}
]
}

View File

@ -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
}
]
}

View File

@ -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"
}
]
}

View File

@ -0,0 +1,10 @@
{
"hypervisors": [
{
"hypervisor_hostname": "fake-mini",
"id": "%(hypervisor_id)s",
"state": "up",
"status": "enabled"
}
]
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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"
}
]
}
]
}

View File

@ -0,0 +1,10 @@
{
"hypervisors": [
{
"hypervisor_hostname": "fake-mini",
"id": "%(hypervisor_id)s",
"state": "up",
"status": "enabled"
}
]
}

View File

@ -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)

View File

@ -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',

View File

@ -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)

View File

@ -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

View File

@ -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