Use uuid for id in os-services API

This patch introduces a new microversion to identify services by uuid
instead of id, to ensure uniqueness across cells. GET /os-services
returns uuid in the id field, and uuid must be provided to delete a
service with DELETE /os-services/{service_uuid}.

The old PUT /os-services/* APIs are now capped and replaced
with a new PUT /os-services/{service_uuid} which takes a uuid path
parameter to uniquely identify the service to update. It also restricts
updates to nova-compute services only, since disabling or forcing-down
a non-compute service like nova-scheduler doesn't make sense as it
doesn't do anything.

The new update() method in this microversion also avoids trying to
re-use the existing private action methods like _enable and _disable
since those are predicated on looking up the service by host/binary,
are confusing to follow for code flow, and just don't really make sense
with a pure PUT resource update method.

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

Co-Authored-By: Matt Riedemann <mriedem.os@gmail.com>

Change-Id: I45494a4df7ee4454edb3ef8e7c5817d8c4e9e5ad
This commit is contained in:
Dan Peschman 2017-07-18 13:54:47 -04:00 committed by Matt Riedemann
parent 430ec6504b
commit 2f7bf29d47
33 changed files with 968 additions and 26 deletions

View File

@ -40,7 +40,8 @@ Response
.. rest_parameters:: parameters.yaml
- services: services
- id: service_id_body
- id: service_id_body_2_52
- id: service_id_body_2_53
- binary: binary
- disabled_reason: disabled_reason_body
- host: host_name_body
@ -48,7 +49,7 @@ Response
- status: service_status
- updated_at: updated
- zone: OS-EXT-AZ:availability_zone
- forced_down: forced_down
- forced_down: forced_down_2_11
**Example List Compute Services**
@ -64,6 +65,9 @@ Disables scheduling for a Compute service.
Specify the service by its host name and binary name.
.. note:: Starting with microversion 2.53 this API is superseded by
``PUT /os-services/{service_id}``.
Normal response codes: 200
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404)
@ -105,6 +109,9 @@ Logs information to the Compute service table about why a Compute service was di
Specify the service by its host name and binary name.
.. note:: Starting with microversion 2.53 this API is superseded by
``PUT /os-services/{service_id}``.
Normal response codes: 200
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404)
@ -148,6 +155,9 @@ Enables scheduling for a Compute service.
Specify the service by its host name and binary name.
.. note:: Starting with microversion 2.53 this API is superseded by
``PUT /os-services/{service_id}``.
Normal response codes: 200
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404)
@ -191,6 +201,9 @@ Action ``force-down`` available as of microversion 2.11.
Specify the service by its host name and binary name.
.. note:: Starting with microversion 2.53 this API is superseded by
``PUT /os-services/{service_id}``.
Normal response codes: 200
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404)
@ -202,7 +215,7 @@ Request
- host: host_name_body
- binary: binary
- forced_down: forced_down
- forced_down: forced_down_2_11
**Example Update Forced Down**
@ -217,7 +230,7 @@ Response
- service: service
- binary: binary
- host: host_name_body
- forced_down: forced_down
- forced_down: forced_down_2_11
|
@ -226,6 +239,77 @@ Response
.. literalinclude:: ../../doc/api_samples/os-services/v2.11/service-force-down-put-resp.json
:language: javascript
Update Compute Service
======================
.. rest_method:: PUT /os-services/{service_id}
Update a compute service to enable or disable scheduling, including recording a
reason why a compute service was disabled from scheduling. Set or unset the
``forced_down`` flag for the service.
This API is available starting with microversion 2.53.
Normal response codes: 200
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404)
Request
-------
.. rest_parameters:: parameters.yaml
- service_id: service_id_path_2_53_no_version
- status: service_status_2_53_in
- disabled_reason: disabled_reason_2_53_in
- forced_down: forced_down_2_53_in
**Example Disable Scheduling For A Compute Service (v2.53)**
.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-disable-log-put-req.json
:language: javascript
**Example Enable Scheduling For A Compute Service (v2.53)**
.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-enable-put-req.json
:language: javascript
**Example Update Forced Down (v2.53)**
.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-force-down-put-req.json
:language: javascript
Response
--------
.. rest_parameters:: parameters.yaml
- service: service
- id: service_id_body_2_53_no_version
- binary: binary
- disabled_reason: disabled_reason_body
- host: host_name_body
- state: service_state
- status: service_status
- updated_at: updated
- zone: OS-EXT-AZ:availability_zone
- forced_down: forced_down_2_53_out
**Example Disable Scheduling For A Compute Service (v2.53)**
.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-disable-log-put-resp.json
:language: javascript
**Example Enable Scheduling For A Compute Service (v2.53)**
.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-enable-put-resp.json
:language: javascript
**Example Update Forced Down (v2.53)**
.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-force-down-put-resp.json
:language: javascript
Delete Compute Service
======================
@ -243,7 +327,8 @@ Request
.. rest_parameters:: parameters.yaml
- service_id: service_id_path
- service_id: service_id_path_2_52
- service_id: service_id_path_2_53
Response
--------

View File

@ -125,6 +125,8 @@ console_token:
in: path
required: true
type: string
# Used in the request path for PUT /os-services/disable-log-reason before
# microversion 2.53.
disabled_reason:
description: |
The reason for disabling a service.
@ -283,12 +285,31 @@ server_id_path:
in: path
required: true
type: string
service_id_path:
service_id_path_2_52:
description: |
The id of the service.
.. note:: This may not uniquely identify a service in a multi-cell
deployment.
in: path
required: true
type: integer
max_version: 2.52
service_id_path_2_53:
description: |
The id of the service as a uuid. This uniquely identifies the service in a
multi-cell deployment.
in: path
required: true
type: string
min_version: 2.53
service_id_path_2_53_no_version:
description: |
The id of the service as a uuid. This uniquely identifies the service in a
multi-cell deployment.
in: path
required: true
type: string
snapshot_id_path:
description: |
The UUID of the snapshot.
@ -1898,6 +1919,15 @@ device_tag_nic_attachment:
required: false
type: string
min_version: 2.49
# Optional input parameter in the body for PUT /os-services/{service_id} added
# in microversion 2.53.
disabled_reason_2_53_in:
description: |
The reason for disabling a service. The minimum length is 1 and the
maximum length is 255. This may only be requested with ``status=disabled``.
in: body
required: false
type: string
disabled_reason_body:
description: |
The reason for disabling a service.
@ -2633,7 +2663,9 @@ force_snapshot:
in: body
required: false
type: boolean
forced_down:
# This is both the request and response parameter for
# PUT /os-services/force-down which was added in 2.11.
forced_down_2_11:
description: |
Whether or not this service was forced down manually by an
administrator. This value is useful to know that some 3rd party has
@ -2642,6 +2674,26 @@ forced_down:
required: true
type: boolean
min_version: 2.11
# This is the optional request input parameter for
# PUT /os-services/{service_id} added in 2.53.
forced_down_2_53_in:
description: |
Whether or not this service was forced down manually by an
administrator. This value is useful to know that some 3rd party has
verified the service should be marked down.
in: body
required: false
type: boolean
# This is the response output parameter for
# PUT /os-services/{service_id} added in 2.53.
forced_down_2_53_out:
description: |
Whether or not this service was forced down manually by an
administrator. This value is useful to know that some 3rd party has
verified the service should be marked down.
in: body
required: true
type: boolean
forceDelete:
description: |
The action.
@ -5064,6 +5116,26 @@ service_id_body:
in: body
required: true
type: integer
service_id_body_2_52:
description: |
The id of the service.
in: body
required: true
type: integer
max_version: 2.52
service_id_body_2_53:
description: |
The id of the service as a uuid.
in: body
required: true
type: string
min_version: 2.53
service_id_body_2_53_no_version:
description: |
The id of the service as a uuid.
in: body
required: true
type: string
service_state:
description: |
The state of the service. One of ``up`` or ``down``.
@ -5076,6 +5148,14 @@ service_status:
in: body
required: true
type: string
# This is an optional input parameter to PUT /os-services/{service_id} added
# in microversion 2.53.
service_status_2_53_in:
description: |
The status of the service. One of ``enabled`` or ``disabled``.
in: body
required: false
type: string
services:
description: |
A list of service objects.

View File

@ -0,0 +1,4 @@
{
"status": "disabled",
"disabled_reason": "maintenance"
}

View File

@ -0,0 +1,13 @@
{
"service": {
"id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
"binary": "nova-compute",
"disabled_reason": "maintenance",
"host": "host1",
"state": "up",
"status": "disabled",
"updated_at": "2012-10-29T13:42:05.000000",
"forced_down": false,
"zone": "nova"
}
}

View File

@ -0,0 +1,3 @@
{
"status": "disabled"
}

View File

@ -0,0 +1,13 @@
{
"service": {
"id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
"binary": "nova-compute",
"disabled_reason": null,
"host": "host1",
"state": "up",
"status": "disabled",
"updated_at": "2012-10-29T13:42:05.000000",
"forced_down": false,
"zone": "nova"
}
}

View File

@ -0,0 +1,3 @@
{
"status": "enabled"
}

View File

@ -0,0 +1,13 @@
{
"service": {
"id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
"binary": "nova-compute",
"disabled_reason": null,
"host": "host1",
"state": "up",
"status": "enabled",
"updated_at": "2012-10-29T13:42:05.000000",
"forced_down": false,
"zone": "nova"
}
}

View File

@ -0,0 +1,3 @@
{
"forced_down": true
}

View File

@ -0,0 +1,13 @@
{
"service": {
"id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
"binary": "nova-compute",
"disabled_reason": "test2",
"host": "host1",
"state": "down",
"status": "disabled",
"updated_at": "2012-10-29T13:42:05.000000",
"forced_down": true,
"zone": "nova"
}
}

View File

@ -0,0 +1,48 @@
{
"services": [
{
"id": "c4726392-27de-4ff9-b2e0-5aa1d08a520f",
"binary": "nova-scheduler",
"disabled_reason": "test1",
"host": "host1",
"state": "up",
"status": "disabled",
"updated_at": "2012-10-29T13:42:02.000000",
"forced_down": false,
"zone": "internal"
},
{
"id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
"binary": "nova-compute",
"disabled_reason": "test2",
"host": "host1",
"state": "up",
"status": "disabled",
"updated_at": "2012-10-29T13:42:05.000000",
"forced_down": false,
"zone": "nova"
},
{
"id": "bbd684ff-d3f6-492e-a30a-a12a2d2db0e0",
"binary": "nova-scheduler",
"disabled_reason": null,
"host": "host2",
"state": "down",
"status": "enabled",
"updated_at": "2012-09-19T06:55:34.000000",
"forced_down": false,
"zone": "internal"
},
{
"id": "13aa304e-5340-45a7-a7fb-b6d6e914d272",
"binary": "nova-compute",
"disabled_reason": "test4",
"host": "host2",
"state": "down",
"status": "disabled",
"updated_at": "2012-09-18T08:03:38.000000",
"forced_down": false,
"zone": "nova"
}
]
}

View File

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

View File

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

View File

@ -124,6 +124,9 @@ 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}.
"""
# The minimum and maximum versions of the API supported
@ -132,7 +135,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.52"
_MAX_API_VERSION = "2.53"
DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which related to network, images and baremetal

View File

@ -627,3 +627,33 @@ user documentation.
Adds support for applying tags when creating a server. The tag schema is
the same as in the `2.26`_ microversion.
2.53
----
**os-services**
Services are now identified by uuid instead of database id to ensure
uniqueness across cells. This microversion brings the following changes:
* ``GET /os-services`` returns a uuid in the ``id`` field of the response
* ``DELETE /os-services/{service_uuid}`` requires a service uuid in the path
* The following APIs have been superseded by
``PUT /os-services/{service_uuid}/``:
* ``PUT /os-services/disable``
* ``PUT /os-services/disable-log-reason``
* ``PUT /os-services/enable``
* ``PUT /os-services/force-down``
``PUT /os-services/{service_uuid}`` takes the following fields in the body:
* ``status`` - can be either "enabled" or "disabled" to enable or disable
the given service
* ``disabled_reason`` - specify with status="disabled" to log a reason for
why the service is disabled
* ``forced_down`` - boolean indicating if the service was forced down by
an external service
* ``PUT /os-services/{service_uuid}`` will now return a full service resource
representation like in a ``GET`` response

View File

@ -44,3 +44,24 @@ service_update_v211 = {
'required': ['host', 'binary'],
'additionalProperties': False
}
# The 2.53 body is for updating a service's status and/or forced_down fields.
# There are no required attributes since the service is identified using a
# unique service_id on the request path, and status and/or forced_down can
# be specified in the body. If status=='disabled', then 'disabled_reason' is
# also checked in the body but is not required. Requesting status='enabled' and
# including a 'disabled_reason' results in a 400, but this is checked in code.
service_update_v2_53 = {
'type': 'object',
'properties': {
'status': {
'type': 'string',
'enum': ['enabled', 'disabled'],
},
'disabled_reason': {
'type': 'string', 'minLength': 1, 'maxLength': 255,
},
'forced_down': parameter_types.boolean
},
'additionalProperties': False
}

View File

@ -13,6 +13,7 @@
# under the License.
from oslo_utils import strutils
from oslo_utils import uuidutils
import webob.exc
from nova.api.openstack import api_version_request
@ -20,6 +21,7 @@ from nova.api.openstack.compute.schemas import services
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api import validation
from nova import availability_zones
from nova import compute
from nova import exception
from nova.i18n import _
@ -27,6 +29,8 @@ from nova.policies import services as services_policies
from nova import servicegroup
from nova import utils
UUID_FOR_ID_MIN_VERSION = '2.53'
class ServiceController(wsgi.Controller):
@ -67,7 +71,7 @@ class ServiceController(wsgi.Controller):
return _services
def _get_service_detail(self, svc, additional_fields):
def _get_service_detail(self, svc, additional_fields, req):
alive = self.servicegroup_api.service_is_up(svc)
state = (alive and "up") or "down"
active = 'enabled'
@ -75,9 +79,22 @@ class ServiceController(wsgi.Controller):
active = 'disabled'
updated_time = self.servicegroup_api.get_updated_time(svc)
uuid_for_id = api_version_request.is_supported(
req, min_version=UUID_FOR_ID_MIN_VERSION)
if 'availability_zone' not in svc:
# The service wasn't loaded with the AZ so we need to do it here.
# Yes this looks weird, but set_availability_zones makes a copy of
# the list passed in and mutates the objects within it, so we have
# to pull it back out from the resulting copied list.
svc.availability_zone = (
availability_zones.set_availability_zones(
req.environ['nova.context'],
[svc])[0]['availability_zone'])
service_detail = {'binary': svc['binary'],
'host': svc['host'],
'id': svc['id'],
'id': svc['uuid' if uuid_for_id else 'id'],
'zone': svc['availability_zone'],
'status': active,
'state': state,
@ -91,7 +108,7 @@ class ServiceController(wsgi.Controller):
def _get_services_list(self, req, additional_fields=()):
_services = self._get_services(req)
return [self._get_service_detail(svc, additional_fields)
return [self._get_service_detail(svc, additional_fields, req)
for svc in _services]
def _enable(self, body, context):
@ -179,10 +196,17 @@ class ServiceController(wsgi.Controller):
context = req.environ['nova.context']
context.can(services_policies.BASE_POLICY_NAME)
try:
utils.validate_integer(id, 'id')
except exception.InvalidInput as exc:
raise webob.exc.HTTPBadRequest(explanation=exc.format_message())
if api_version_request.is_supported(
req, min_version=UUID_FOR_ID_MIN_VERSION):
if not uuidutils.is_uuid_like(id):
msg = _('Invalid uuid %s') % id
raise webob.exc.HTTPBadRequest(explanation=msg)
else:
try:
utils.validate_integer(id, 'id')
except exception.InvalidInput as exc:
raise webob.exc.HTTPBadRequest(
explanation=exc.format_message())
try:
service = self.host_api.service_get_by_id(context, id)
@ -215,11 +239,18 @@ class ServiceController(wsgi.Controller):
return {'services': _services}
@wsgi.Controller.api_version('2.1', '2.52')
@extensions.expected_errors((400, 404))
@validation.schema(services.service_update, '2.0', '2.10')
@validation.schema(services.service_update_v211, '2.11')
@validation.schema(services.service_update_v211, '2.11', '2.52')
def update(self, req, id, body):
"""Perform service update"""
"""Perform service update
Before microversion 2.53, the body contains a host and binary value
to identify the service on which to perform the action. There is no
service ID passed on the path, just the action, for example
PUT /os-services/disable.
"""
if api_version_request.is_supported(req, min_version='2.11'):
actions = self.actions.copy()
actions["force-down"] = self._forced_down
@ -227,3 +258,89 @@ class ServiceController(wsgi.Controller):
actions = self.actions
return self._perform_action(req, id, body, actions)
@wsgi.Controller.api_version(UUID_FOR_ID_MIN_VERSION) # noqa F811
@extensions.expected_errors((400, 404))
@validation.schema(services.service_update_v2_53, UUID_FOR_ID_MIN_VERSION)
def update(self, req, id, body):
"""Perform service update
Starting with microversion 2.53, the service uuid is passed in on the
path of the request to uniquely identify the service record on which to
perform a given update, which is defined in the body of the request.
"""
service_id = id
# Validate that the service ID is a UUID.
if not uuidutils.is_uuid_like(service_id):
msg = _('Invalid uuid %s') % service_id
raise webob.exc.HTTPBadRequest(explanation=msg)
# Validate the request context against the policy.
context = req.environ['nova.context']
context.can(services_policies.BASE_POLICY_NAME)
# Get the service by uuid.
try:
service = self.host_api.service_get_by_id(context, service_id)
# At this point the context is targeted to the cell that the
# service was found in so we don't need to do any explicit cell
# targeting below.
except exception.ServiceNotFound as e:
raise webob.exc.HTTPNotFound(explanation=e.format_message())
# Return 400 if service.binary is not nova-compute.
# Before the earlier PUT handlers were made cells-aware, you could
# technically disable a nova-scheduler service, although that doesn't
# really do anything within Nova and is just confusing. Now trying to
# do that will fail as a nova-scheduler service won't have a host
# mapping so you'll get a 404. In this new microversion, we close that
# old gap and make sure you can only enable/disable and set forced_down
# on nova-compute services since those are the only ones that make
# sense to update for those operations.
if service.binary != 'nova-compute':
msg = (_('Updating a %(binary)s service is not supported. Only '
'nova-compute services can be updated.') %
{'binary': service.binary})
raise webob.exc.HTTPBadRequest(explanation=msg)
# Now determine the update to perform based on the body. We are
# intentionally not using _perform_action or the other old-style
# action functions.
if 'status' in body:
# This is a status update for either enabled or disabled.
if body['status'] == 'enabled':
# Fail if 'disabled_reason' was requested when enabling the
# service since those two combined don't make sense.
if body.get('disabled_reason'):
msg = _("Specifying 'disabled_reason' with status "
"'enabled' is invalid.")
raise webob.exc.HTTPBadRequest(explanation=msg)
service.disabled = False
service.disabled_reason = None
elif body['status'] == 'disabled':
service.disabled = True
# The disabled reason is optional.
service.disabled_reason = body.get('disabled_reason')
# This is intentionally not an elif, i.e. it's in addition to the
# status update.
if 'forced_down' in body:
service.forced_down = strutils.bool_from_string(
body['forced_down'], strict=True)
# Check to see if anything was actually updated since the schema does
# not define any required fields.
if not service.obj_what_changed():
msg = _("No updates were requested. Fields 'status' or "
"'forced_down' should be specified.")
raise webob.exc.HTTPBadRequest(explanation=msg)
# Now save our updates to the service record in the database.
service.save()
# Return the full service record details.
additional_fields = ['forced_down']
return {'service': self._get_service_detail(
service, additional_fields, req)}

View File

@ -69,6 +69,20 @@ class _CellProxy(object):
return getattr(self._obj, key)
def __contains__(self, key):
"""Pass-through "in" check to the wrapped object.
This is needed to proxy any types of checks in the calling code
like::
if 'availability_zone' in service:
...
:param key: They key to look for in the wrapped object.
:returns: True if key is in the wrapped object, False otherwise.
"""
return key in self._obj
def obj_to_primitive(self):
obj_p = self._obj.obj_to_primitive()
obj_p['cell_proxy.class_name'] = self.__class__.__name__

View File

@ -50,6 +50,11 @@ services_policies = [
'method': 'PUT',
'path': '/os-services/force-down'
},
{
# Added in microversion 2.53.
'method': 'PUT',
'path': '/os-services/{service_id}'
},
{
'method': 'DELETE',
'path': '/os-services/{service_id}'

View File

@ -0,0 +1,4 @@
{
"status": "disabled",
"disabled_reason": "%(disabled_reason)s"
}

View File

@ -0,0 +1,13 @@
{
"service": {
"id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
"binary": "nova-compute",
"disabled_reason": "maintenance",
"host": "host1",
"state": "up",
"status": "disabled",
"updated_at": "%(strtime)s",
"forced_down": false,
"zone": "nova"
}
}

View File

@ -0,0 +1,13 @@
{
"service": {
"id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
"binary": "nova-compute",
"disabled_reason": null,
"host": "host1",
"state": "up",
"status": "disabled",
"updated_at": "%(strtime)s",
"forced_down": false,
"zone": "nova"
}
}

View File

@ -0,0 +1,13 @@
{
"service": {
"id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
"binary": "nova-compute",
"disabled_reason": null,
"host": "host1",
"state": "up",
"status": "enabled",
"updated_at": "2012-10-29T13:42:05.000000",
"forced_down": false,
"zone": "nova"
}
}

View File

@ -0,0 +1,3 @@
{
"forced_down": %(forced_down)s
}

View File

@ -0,0 +1,13 @@
{
"service": {
"id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
"binary": "nova-compute",
"disabled_reason": "test2",
"host": "host1",
"state": "down",
"status": "disabled",
"updated_at": "%(strtime)s",
"forced_down": true,
"zone": "nova"
}
}

View File

@ -0,0 +1,48 @@
{
"services": [
{
"binary": "nova-scheduler",
"disabled_reason": "test1",
"forced_down": false,
"host": "host1",
"id": "%(id)s",
"state": "up",
"status": "disabled",
"updated_at": "%(strtime)s",
"zone": "internal"
},
{
"binary": "nova-compute",
"disabled_reason": "test2",
"forced_down": false,
"host": "host1",
"id": "%(id)s",
"state": "up",
"status": "disabled",
"updated_at": "%(strtime)s",
"zone": "nova"
},
{
"binary": "nova-scheduler",
"disabled_reason": null,
"forced_down": false,
"host": "host2",
"id": "%(id)s",
"state": "down",
"status": "enabled",
"updated_at": "%(strtime)s",
"zone": "internal"
},
{
"binary": "nova-compute",
"disabled_reason": "test4",
"forced_down": false,
"host": "host2",
"id": "%(id)s",
"state": "down",
"status": "disabled",
"updated_at": "%(strtime)s",
"zone": "nova"
}
]
}

View File

@ -15,6 +15,7 @@
from oslo_utils import fixture as utils_fixture
from nova import exception
from nova.tests.functional.api_sample_tests import api_sample_base
from nova.tests.unit.api.openstack.compute import test_services
@ -104,3 +105,57 @@ class ServicesV211JsonTest(ServicesJsonTest):
'service-force-down-put-req', subs)
self._verify_response('service-force-down-put-resp', subs,
response, 200)
class ServicesV253JsonTest(ServicesV211JsonTest):
microversion = '2.53'
scenarios = [('v2_53', {'api_major_version': 'v2.1'})]
def setUp(self):
super(ServicesV253JsonTest, self).setUp()
def db_service_get_by_uuid(ctxt, service_uuid):
for svc in test_services.fake_services_list:
if svc['uuid'] == service_uuid:
return svc
raise exception.ServiceNotFound(service_id=service_uuid)
self.stub_out('nova.db.service_get_by_uuid', db_service_get_by_uuid)
def test_service_enable(self):
"""Enable an existing service."""
response = self._do_put(
'os-services/%s' % test_services.FAKE_UUID_COMPUTE_HOST1,
'service-enable-put-req', subs={})
self._verify_response('service-enable-put-resp', {}, response, 200)
def test_service_disable(self):
"""Disable an existing service."""
response = self._do_put(
'os-services/%s' % test_services.FAKE_UUID_COMPUTE_HOST1,
'service-disable-put-req', subs={})
self._verify_response('service-disable-put-resp', {}, response, 200)
def test_service_disable_log_reason(self):
"""Disable an existing service and log the reason."""
subs = {'disabled_reason': 'maintenance'}
response = self._do_put(
'os-services/%s' % test_services.FAKE_UUID_COMPUTE_HOST1,
'service-disable-log-put-req', subs)
self._verify_response('service-disable-log-put-resp',
subs, response, 200)
def test_service_delete(self):
"""Delete an existing service."""
response = self._do_delete(
'os-services/%s' % test_services.FAKE_UUID_COMPUTE_HOST1)
self.assertEqual(204, response.status_code)
self.assertEqual("", response.text)
def test_force_down(self):
"""Set forced_down flag"""
subs = {'forced_down': 'true'}
response = self._do_put(
'os-services/%s' % test_services.FAKE_UUID_COMPUTE_HOST1,
'service-force-down-put-req', subs)
self._verify_response('service-force-down-put-resp', subs,
response, 200)

View File

@ -54,6 +54,13 @@ class NotificationSampleTestBase(test.TestCase,
REQUIRES_LOCKING = True
# NOTE(gibi): Notification payloads always reflect the data needed
# for every supported API microversion so we can safe to use the latest
# API version in the tests. This helps the test to use the new API
# features too. This can be overridden by subclasses that need to cap
# at a specific microversion for older APIs.
MAX_MICROVERSION = 'latest'
def setUp(self):
super(NotificationSampleTestBase, self).setUp()
@ -63,11 +70,7 @@ class NotificationSampleTestBase(test.TestCase,
self.api = api_fixture.api
self.admin_api = api_fixture.admin_api
# NOTE(gibi): Notification payloads always reflect the data needed
# for every supported API microversion so we can safe to use the latest
# API version in the tests. This helps the test to use the new API
# features too.
max_version = 'latest'
max_version = self.MAX_MICROVERSION
self.api.microversion = max_version
self.admin_api.microversion = max_version

View File

@ -14,17 +14,22 @@
from oslo_utils import fixture as utils_fixture
from nova import exception
from nova.tests import fixtures
from nova.tests.functional.notification_sample_tests \
import notification_sample_base
from nova.tests.unit.api.openstack.compute import test_services
class TestServiceUpdateNotificationSample(
class TestServiceUpdateNotificationSamplev2_52(
notification_sample_base.NotificationSampleTestBase):
# These tests have to be capped at 2.52 since the PUT format changes in
# the 2.53 microversion.
MAX_MICROVERSION = '2.52'
def setUp(self):
super(TestServiceUpdateNotificationSample, self).setUp()
super(TestServiceUpdateNotificationSamplev2_52, self).setUp()
self.stub_out("nova.db.service_get_by_host_and_binary",
test_services.fake_service_get_by_host_binary)
self.stub_out("nova.db.service_update",
@ -69,3 +74,51 @@ class TestServiceUpdateNotificationSample(
'disabled': True,
'disabled_reason': 'test2',
'uuid': self.service_uuid})
class TestServiceUpdateNotificationSampleLatest(
TestServiceUpdateNotificationSamplev2_52):
"""Tests the PUT /os-services/{service_id} API notifications."""
MAX_MICROVERSION = 'latest'
def setUp(self):
super(TestServiceUpdateNotificationSampleLatest, self).setUp()
def db_service_get_by_uuid(ctxt, service_uuid):
for svc in test_services.fake_services_list:
if svc['uuid'] == service_uuid:
return svc
raise exception.ServiceNotFound(service_id=service_uuid)
self.stub_out('nova.db.service_get_by_uuid', db_service_get_by_uuid)
def test_service_enable(self):
body = {'status': 'enabled'}
self.admin_api.api_put('os-services/%s' % self.service_uuid, body)
self._verify_notification('service-update',
replacements={'uuid': self.service_uuid})
def test_service_disabled(self):
body = {'status': 'disabled'}
self.admin_api.api_put('os-services/%s' % self.service_uuid, body)
self._verify_notification('service-update',
replacements={'disabled': True,
'uuid': self.service_uuid})
def test_service_disabled_log_reason(self):
body = {'status': 'disabled',
'disabled_reason': 'test2'}
self.admin_api.api_put('os-services/%s' % self.service_uuid, body)
self._verify_notification('service-update',
replacements={'disabled': True,
'disabled_reason': 'test2',
'uuid': self.service_uuid})
def test_service_force_down(self):
body = {'forced_down': True}
self.admin_api.api_put('os-services/%s' % self.service_uuid, body)
self._verify_notification('service-update',
replacements={'forced_down': True,
'disabled': True,
'disabled_reason': 'test2',
'uuid': self.service_uuid})

View File

@ -19,6 +19,7 @@ import datetime
import iso8601
import mock
from oslo_utils import fixture as utils_fixture
import six
import webob.exc
from nova.api.openstack import api_version_request as api_version
@ -36,6 +37,10 @@ from nova import test
from nova.tests import fixtures
from nova.tests.unit.api.openstack import fakes
from nova.tests.unit.objects import test_service
from nova.tests import uuidsentinel
# This is tied into the os-services API samples functional tests.
FAKE_UUID_COMPUTE_HOST1 = 'e81d66a4-ddd3-4aba-8a84-171d1cb4d339'
fake_services_list = [
@ -43,6 +48,7 @@ fake_services_list = [
binary='nova-scheduler',
host='host1',
id=1,
uuid=uuidsentinel.svc1,
disabled=True,
topic='scheduler',
updated_at=datetime.datetime(2012, 10, 29, 13, 42, 2),
@ -54,6 +60,7 @@ fake_services_list = [
binary='nova-compute',
host='host1',
id=2,
uuid=FAKE_UUID_COMPUTE_HOST1,
disabled=True,
topic='compute',
updated_at=datetime.datetime(2012, 10, 29, 13, 42, 5),
@ -65,6 +72,7 @@ fake_services_list = [
binary='nova-scheduler',
host='host2',
id=3,
uuid=uuidsentinel.svc3,
disabled=False,
topic='scheduler',
updated_at=datetime.datetime(2012, 9, 19, 6, 55, 34),
@ -76,6 +84,7 @@ fake_services_list = [
binary='nova-compute',
host='host2',
id=4,
uuid=uuidsentinel.svc4,
disabled=True,
topic='compute',
updated_at=datetime.datetime(2012, 9, 18, 8, 3, 38),
@ -88,6 +97,7 @@ fake_services_list = [
binary='nova-osapi_compute',
host='host2',
id=5,
uuid=uuidsentinel.svc5,
disabled=False,
topic=None,
updated_at=None,
@ -99,6 +109,7 @@ fake_services_list = [
binary='nova-metadata',
host='host2',
id=6,
uuid=uuidsentinel.svc6,
disabled=False,
topic=None,
updated_at=None,
@ -927,6 +938,235 @@ class ServicesTestV211(ServicesTestV21):
self.controller.update, req, 'force-down', body=req_body)
class ServicesTestV252(ServicesTestV211):
"""This is a boundary test to ensure that 2.52 behaves the same as 2.11."""
wsgi_api_version = '2.52'
class FakeServiceGroupAPI(object):
def service_is_up(self, *args, **kwargs):
return True
def get_updated_time(self, *args, **kwargs):
return mock.sentinel.updated_time
class ServicesTestV253(test.TestCase):
"""Tests for the 2.53 microversion in the os-services API."""
def setUp(self):
super(ServicesTestV253, self).setUp()
self.controller = services_v21.ServiceController()
self.controller.servicegroup_api = FakeServiceGroupAPI()
self.req = fakes.HTTPRequest.blank(
'', version=services_v21.UUID_FOR_ID_MIN_VERSION)
def assert_services_equal(self, s1, s2):
for k in ('binary', 'host'):
self.assertEqual(s1[k], s2[k])
def test_list_has_uuid_in_id_field(self):
"""Tests that a GET response includes an id field but the value is
the service uuid rather than the id integer primary key.
"""
service_uuids = [s['uuid'] for s in fake_services_list]
with mock.patch.object(
self.controller.host_api, 'service_get_all',
side_effect=fake_service_get_all(fake_services_list)):
resp = self.controller.index(self.req)
for service in resp['services']:
# Make sure a uuid field wasn't returned.
self.assertNotIn('uuid', service)
# Make sure the id field is one of our known uuids.
self.assertIn(service['id'], service_uuids)
# Make sure this service was in our known list of fake services.
expected = next(iter(filter(
lambda s: s['uuid'] == service['id'],
fake_services_list)))
self.assert_services_equal(expected, service)
def test_delete_takes_uuid_for_id(self):
"""Tests that a DELETE request correctly deletes a service when a valid
service uuid is provided for an existing service.
"""
service = self.start_service(
'compute', 'fake-compute-host').service_ref
with mock.patch.object(self.controller.host_api,
'service_delete') as service_delete:
self.controller.delete(self.req, service.uuid)
service_delete.assert_called_once_with(
self.req.environ['nova.context'], service.uuid)
self.assertEqual(204, self.controller.delete.wsgi_code)
def test_delete_uuid_not_found(self):
"""Tests that we get a 404 response when attempting to delete a service
that is not found by the given uuid.
"""
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete, self.req, uuidsentinel.svc2)
def test_delete_invalid_uuid(self):
"""Tests that the service uuid is validated in a DELETE request."""
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.delete, self.req, 1234)
self.assertIn('Invalid uuid', six.text_type(ex))
def test_update_invalid_service_uuid(self):
"""Tests that the service uuid is validated in a PUT request."""
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, self.req, 1234, body={})
self.assertIn('Invalid uuid', six.text_type(ex))
def test_update_policy_failed(self):
"""Tests that policy is checked with microversion 2.53."""
rule_name = "os_compute_api:os-services"
self.policy.set_rules({rule_name: "project_id:non_fake"})
exc = self.assertRaises(
exception.PolicyNotAuthorized,
self.controller.update, self.req, uuidsentinel.service_uuid,
body={})
self.assertEqual(
"Policy doesn't allow %s to be performed." % rule_name,
exc.format_message())
def test_update_service_not_found(self):
"""Tests that we get a 404 response if the service is not found by
the given uuid when handling a PUT request.
"""
self.assertRaises(webob.exc.HTTPNotFound, self.controller.update,
self.req, uuidsentinel.service_uuid, body={})
def test_update_invalid_status(self):
"""Tests that jsonschema validates the status field in the request body
and fails if it's not "enabled" or "disabled".
"""
service = self.start_service(
'compute', 'fake-compute-host').service_ref
self.assertRaises(
exception.ValidationError, self.controller.update, self.req,
service.uuid, body={'status': 'invalid'})
def test_update_disabled_no_reason_then_enable(self):
"""Tests disabling a service with no reason given. Then enables it
to see the change in the response body.
"""
service = self.start_service(
'compute', 'fake-compute-host').service_ref
resp = self.controller.update(self.req, service.uuid,
body={'status': 'disabled'})
expected_resp = {
'service': {
'status': 'disabled',
'state': 'up',
'binary': 'nova-compute',
'host': 'fake-compute-host',
'zone': 'nova', # Comes from CONF.default_availability_zone
'updated_at': mock.sentinel.updated_time,
'disabled_reason': None,
'id': service.uuid,
'forced_down': False
}
}
self.assertDictEqual(expected_resp, resp)
# Now enable the service to see the response change.
req = fakes.HTTPRequest.blank(
'', version=services_v21.UUID_FOR_ID_MIN_VERSION)
resp = self.controller.update(req, service.uuid,
body={'status': 'enabled'})
expected_resp['service']['status'] = 'enabled'
self.assertDictEqual(expected_resp, resp)
def test_update_enable_with_disabled_reason_fails(self):
"""Validates that requesting to both enable a service and set the
disabled_reason results in a 400 BadRequest error.
"""
service = self.start_service(
'compute', 'fake-compute-host').service_ref
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, self.req, service.uuid,
body={'status': 'enabled',
'disabled_reason': 'invalid'})
self.assertIn("Specifying 'disabled_reason' with status 'enabled' "
"is invalid.", six.text_type(ex))
def test_update_disabled_reason_and_forced_down(self):
"""Tests disabling a service with a reason and forcing it down is
reflected back in the response.
"""
service = self.start_service(
'compute', 'fake-compute-host').service_ref
resp = self.controller.update(self.req, service.uuid,
body={'status': 'disabled',
'disabled_reason': 'maintenance',
# Also tests bool_from_string usage
'forced_down': 'yes'})
expected_resp = {
'service': {
'status': 'disabled',
'state': 'up',
'binary': 'nova-compute',
'host': 'fake-compute-host',
'zone': 'nova', # Comes from CONF.default_availability_zone
'updated_at': mock.sentinel.updated_time,
'disabled_reason': 'maintenance',
'id': service.uuid,
'forced_down': True
}
}
self.assertDictEqual(expected_resp, resp)
def test_update_forced_down_invalid_value(self):
"""Tests that passing an invalid value for forced_down results in
a validation error.
"""
service = self.start_service(
'compute', 'fake-compute-host').service_ref
self.assertRaises(exception.ValidationError,
self.controller.update,
self.req, service.uuid,
body={'status': 'disabled',
'disabled_reason': 'maintenance',
'forced_down': 'invalid'})
def test_update_forced_down_invalid_service(self):
"""Tests that you can't update a non-nova-compute service."""
service = self.start_service(
'scheduler', 'fake-scheduler-host').service_ref
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update,
self.req, service.uuid,
body={'forced_down': True})
self.assertEqual('Updating a nova-scheduler service is not supported. '
'Only nova-compute services can be updated.',
six.text_type(ex))
def test_update_empty_body(self):
"""Tests that the caller gets a 400 error if they don't request any
updates.
"""
service = self.start_service('compute').service_ref
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update,
self.req, service.uuid, body={})
self.assertEqual("No updates were requested. Fields 'status' or "
"'forced_down' should be specified.",
six.text_type(ex))
def test_update_only_disabled_reason(self):
"""Tests that the caller gets a 400 error if they only specify
disabled_reason but don't also specify status='disabled'.
"""
service = self.start_service('compute').service_ref
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, self.req, service.uuid,
body={'disabled_reason': 'missing status'})
self.assertEqual("No updates were requested. Fields 'status' or "
"'forced_down' should be specified.",
six.text_type(ex))
class ServicesCellsTestV21(test.TestCase):
def setUp(self):

View File

@ -0,0 +1,10 @@
---
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
`Compute API reference`_ for details.
.. _REST API Version History: https://docs.openstack.org/developer/nova/api_microversion_history.html
.. _Compute API reference: https://developer.openstack.org/api-ref/compute/