placement: generation in provider aggregate APIs

Placement API microversion 1.19 enhances the payloads for the `GET
/resource_providers/{uuid}/aggregates` response and the `PUT
/resource_providers/{uuid}/aggregates` request and response to be
identical, and to include the ``resource_provider_generation``. As with
other generation-aware APIs, if the ``resource_provider_generation``
specified in the `PUT` request does not match the generation known by
the server, a 409 Conflict error is returned.

Change-Id: I86416e35da1798cdf039b42c9ed7629f0f9c75fc
blueprint: placement-aggregate-generation
This commit is contained in:
Eric Fried 2018-02-27 12:29:11 +00:00
parent eb637b9de7
commit 3216f078d4
16 changed files with 247 additions and 52 deletions

View File

@ -11,23 +11,33 @@
# under the License.
"""Aggregate handlers for Placement API."""
from oslo_db import exception as db_exc
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
from oslo_utils import timeutils
import webob
from nova.api.openstack.placement import microversion
from nova.api.openstack.placement.objects import resource_provider as rp_obj
from nova.api.openstack.placement.schemas import aggregate as schema
from nova.api.openstack.placement import util
from nova.api.openstack.placement import wsgi_wrapper
from nova import exception
from nova.i18n import _
def _send_aggregates(req, aggregate_uuids):
_INCLUDE_GENERATION_VERSION = (1, 19)
def _send_aggregates(req, resource_provider, aggregate_uuids):
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
response = req.response
response.status = 200
payload = _serialize_aggregates(aggregate_uuids)
if want_version.matches(min_version=_INCLUDE_GENERATION_VERSION):
payload['resource_provider_generation'] = resource_provider.generation
response.body = encodeutils.to_utf8(
jsonutils.dumps(_serialize_aggregates(aggregate_uuids)))
jsonutils.dumps(payload))
response.content_type = 'application/json'
if want_version.matches((1, 15)):
req.response.cache_control = 'no-cache'
@ -60,7 +70,7 @@ def get_aggregates(req):
context, uuid)
aggregate_uuids = resource_provider.get_aggregates()
return _send_aggregates(req, aggregate_uuids)
return _send_aggregates(req, resource_provider, aggregate_uuids)
@wsgi_wrapper.PlacementWsgify
@ -68,10 +78,32 @@ def get_aggregates(req):
@microversion.version_handler('1.1')
def set_aggregates(req):
context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
consider_generation = want_version.matches(
min_version=_INCLUDE_GENERATION_VERSION)
put_schema = schema.PUT_AGGREGATES_SCHEMA_V1_1
if consider_generation:
put_schema = schema.PUT_AGGREGATES_SCHEMA_V1_19
uuid = util.wsgi_path_item(req.environ, 'uuid')
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
context, uuid)
aggregate_uuids = util.extract_json(req.body, schema.PUT_AGGREGATES_SCHEMA)
resource_provider.set_aggregates(aggregate_uuids)
data = util.extract_json(req.body, put_schema)
if consider_generation:
# Check for generation conflict
rp_gen = data['resource_provider_generation']
if resource_provider.generation != rp_gen:
raise webob.exc.HTTPConflict(
_("Resource provider's generation already changed. Please "
"update the generation and try again."))
aggregate_uuids = data['aggregates']
else:
aggregate_uuids = data
try:
resource_provider.set_aggregates(
aggregate_uuids, increment_generation=consider_generation)
except (exception.ConcurrentUpdateDetected,
db_exc.DBDuplicateEntry) as exc:
raise webob.exc.HTTPConflict(
_('Update conflict: %(error)s') % {'error': exc})
return _send_aggregates(req, aggregate_uuids)
return _send_aggregates(req, resource_provider, aggregate_uuids)

View File

@ -61,6 +61,8 @@ VERSIONS = [
'1.17', # Add 'required' query parameter to GET /allocation_candidates and
# return traits in the provider summary.
'1.18', # Support ?required=<traits> queryparam on GET /resource_providers
'1.19', # Include generation and conflict detection in provider aggregates
# APIs
]

View File

@ -432,7 +432,9 @@ def _get_aggregates_by_provider_id(context, rp_id):
@db_api.api_context_manager.writer
def _set_aggregates(context, rp_id, provided_aggregates):
def _set_aggregates(context, resource_provider, provided_aggregates,
increment_generation=False):
rp_id = resource_provider.id
# When aggregate uuids are persisted no validation is done
# to ensure that they refer to something that has meaning
# elsewhere. It is assumed that code which makes use of the
@ -489,6 +491,10 @@ def _set_aggregates(context, rp_id, provided_aggregates):
select_agg_id)
context.session.execute(insert_aggregates)
if increment_generation:
resource_provider.generation = _increment_provider_generation(
context, resource_provider)
@db_api.api_context_manager.reader
def _get_traits_by_provider_id(context, rp_id):
@ -757,13 +763,17 @@ class ResourceProvider(base.VersionedObject, base.TimestampedObject):
"""Get the aggregate uuids associated with this resource provider."""
return _get_aggregates_by_provider_id(self._context, self.id)
def set_aggregates(self, aggregate_uuids):
def set_aggregates(self, aggregate_uuids, increment_generation=False):
"""Set the aggregate uuids associated with this resource provider.
If an aggregate does not exist, one will be created using the
provided uuid.
The resource provider generation is incremented if and only if the
increment_generation parameter is True.
"""
_set_aggregates(self._context, self.id, aggregate_uuids)
_set_aggregates(self._context, self, aggregate_uuids,
increment_generation=increment_generation)
def set_traits(self, traits):
"""Replaces the set of traits associated with the resource provider

View File

@ -233,3 +233,13 @@ based on other query parameters.
Trait names which are empty, do not exist, or are otherwise invalid will result
in a 400 error.
1.19 Include generation and conflict detection in provider aggregates APIs
--------------------------------------------------------------------------
Enhance the payloads for the `GET /resource_providers/{uuid}/aggregates`
response and the `PUT /resource_providers/{uuid}/aggregates` request and
response to be identical, and to include the ``resource_provider_generation``.
As with other generation-aware APIs, if the ``resource_provider_generation``
specified in the `PUT` request does not match the generation known by the
server, a 409 Conflict error is returned.

View File

@ -10,9 +10,10 @@
# License for the specific language governing permissions and limitations
# under the License.
"""Aggregate schemas for Placement API."""
import copy
PUT_AGGREGATES_SCHEMA = {
_AGGREGATES_LIST_SCHEMA = {
"type": "array",
"items": {
"type": "string",
@ -20,3 +21,22 @@ PUT_AGGREGATES_SCHEMA = {
},
"uniqueItems": True
}
PUT_AGGREGATES_SCHEMA_V1_1 = copy.deepcopy(_AGGREGATES_LIST_SCHEMA)
PUT_AGGREGATES_SCHEMA_V1_19 = {
"type": "object",
"properties": {
"aggregates": copy.deepcopy(_AGGREGATES_LIST_SCHEMA),
"resource_provider_generation": {
"type": "integer",
}
},
"required": [
"aggregates",
"resource_provider_generation",
],
"additionalProperties": False,
}

View File

@ -57,22 +57,39 @@ tests:
response_json_paths:
$.errors[0].title: Not Found
- name: put same aggregates twice
PUT: $LAST_URL
data:
- *agg_1
- *agg_1
status: 400
response_strings:
- has non-unique elements
response_json_paths:
$.errors[0].title: Bad Request
- name: put some aggregates
- name: put some aggregates - old payload and new microversion
PUT: $LAST_URL
data:
- *agg_1
- *agg_2
status: 400
response_strings:
- JSON does not validate
response_json_paths:
$.errors[0].title: Bad Request
- name: put some aggregates - new payload and old microversion
PUT: $LAST_URL
request_headers:
openstack-api-version: placement 1.18
data:
resource_provider_generation: 0
aggregates:
- *agg_1
- *agg_2
status: 400
response_strings:
- JSON does not validate
response_json_paths:
$.errors[0].title: Bad Request
- name: put some aggregates - new payload and new microversion
PUT: $LAST_URL
data:
resource_provider_generation: 0
aggregates:
- *agg_1
- *agg_2
status: 200
response_headers:
content-type: /application/json/
@ -82,6 +99,7 @@ tests:
response_json_paths:
$.aggregates[0]: *agg_1
$.aggregates[1]: *agg_2
$.resource_provider_generation: 1
- name: get those aggregates
GET: $LAST_URL
@ -92,9 +110,18 @@ tests:
response_json_paths:
$.aggregates.`len`: 2
- name: clear those aggregates - generation conflict
PUT: $LAST_URL
data:
resource_provider_generation: 0
aggregates: []
status: 409
- name: clear those aggregates
PUT: $LAST_URL
data: []
data:
resource_provider_generation: 1
aggregates: []
status: 200
response_json_paths:
$.aggregates: []
@ -113,7 +140,7 @@ tests:
response_json_paths:
$.errors[0].title: Bad Request
- name: put invalid json not array
- name: put invalid json no generation
PUT: $LAST_URL
data:
aggregates:
@ -128,11 +155,26 @@ tests:
- name: put invalid json not uuids
PUT: $LAST_URL
data:
- harry
- sally
aggregates:
- harry
- sally
resource_provider_generation: 2
status: 400
response_strings:
- JSON does not validate
- "is not a 'uuid'"
response_json_paths:
$.errors[0].title: Bad Request
- name: put same aggregates twice
PUT: $LAST_URL
data:
aggregates:
- *agg_1
- *agg_1
resource_provider_generation: 2
status: 400
response_strings:
- has non-unique elements
response_json_paths:
$.errors[0].title: Bad Request

View File

@ -39,13 +39,13 @@ tests:
response_json_paths:
$.errors[0].title: Not Acceptable
- name: latest microversion is 1.18
- name: latest microversion is 1.19
GET: /
request_headers:
openstack-api-version: placement latest
response_headers:
vary: /OpenStack-API-Version/
openstack-api-version: placement 1.18
openstack-api-version: placement 1.19
- name: other accept header bad version
GET: /

View File

@ -34,7 +34,9 @@ tests:
- name: associate an aggregate with rp1
PUT: /resource_providers/893337e9-1e55-49f0-bcfe-6a2f16fbf2f7/aggregates
data:
- 83a3d69d-8920-48e2-8914-cadfd8fa2f91
aggregates:
- 83a3d69d-8920-48e2-8914-cadfd8fa2f91
resource_provider_generation: 0
status: 200
- name: get by aggregates one result
@ -58,7 +60,9 @@ tests:
- name: associate an aggregate with rp2
PUT: /resource_providers/5202c48f-c960-4eec-bde3-89c4f22a17b9/aggregates
data:
- 83a3d69d-8920-48e2-8914-cadfd8fa2f91
aggregates:
- 83a3d69d-8920-48e2-8914-cadfd8fa2f91
resource_provider_generation: 0
status: 200
- name: get by aggregates two result
@ -71,7 +75,9 @@ tests:
- name: associate another aggregate with rp2
PUT: /resource_providers/5202c48f-c960-4eec-bde3-89c4f22a17b9/aggregates
data:
- 99652f11-9f77-46b9-80b7-4b1989be9f8c
aggregates:
- 99652f11-9f77-46b9-80b7-4b1989be9f8c
resource_provider_generation: 1
status: 200
- name: get by both aggregates two
@ -83,7 +89,9 @@ tests:
- name: clear aggregates on rp1
PUT: /resource_providers/893337e9-1e55-49f0-bcfe-6a2f16fbf2f7/aggregates
data: []
data:
aggregates: []
resource_provider_generation: 1
status: 200
- name: get by both aggregates one

View File

@ -146,7 +146,9 @@ tests:
- name: associate an aggregate with rp1
PUT: /resource_providers/$ENVIRON['RP_UUID']/aggregates
data:
- 83a3d69d-8920-48e2-8914-cadfd8fa2f91
aggregates:
- 83a3d69d-8920-48e2-8914-cadfd8fa2f91
resource_provider_generation: $HISTORY['list resource providers providing disk and vcpu resources'].$RESPONSE['$.resource_providers[0].generation']
status: 200
- name: get by aggregates with resources

View File

@ -92,12 +92,16 @@ tests:
- name: aggregate shared
PUT: /resource_providers/d450bd39-3b01-4355-9ea1-594f96594cf1/aggregates
data:
- f3dc0f36-97d4-4daf-be0c-d71466da9c85
aggregates:
- f3dc0f36-97d4-4daf-be0c-d71466da9c85
resource_provider_generation: 1
- name: aggregate cn1
PUT: /resource_providers/8d830468-6395-46b0-b56a-f934a1d60bbe/aggregates
data:
- f3dc0f36-97d4-4daf-be0c-d71466da9c85
aggregates:
- f3dc0f36-97d4-4daf-be0c-d71466da9c85
resource_provider_generation: 1
# no shared trait
- name: get resources no shared
@ -114,7 +118,7 @@ tests:
- name: set trait shared
PUT: /resource_providers/d450bd39-3b01-4355-9ea1-594f96594cf1/traits
data:
resource_provider_generation: 1
resource_provider_generation: 2
traits:
- MISC_SHARES_VIA_AGGREGATE

View File

@ -58,7 +58,8 @@ identified by `{uuid}`.
Normal Response Codes: 200
Error response codes: itemNotFound(404)
Error response codes: itemNotFound(404) if the provider does not exist. (If the
provider has no aggregates, the result is 200 with an empty aggregate list.)
Request
-------
@ -67,19 +68,33 @@ Request
- uuid: resource_provider_uuid_path
Response
--------
Response (microversions 1.1 - 1.18)
-----------------------------------
.. rest_parameters:: parameters.yaml
- aggregates: aggregates
Response Example
----------------
Response Example (microversions 1.1 - 1.18)
-------------------------------------------
.. literalinclude:: ./samples/aggregates/get-aggregates.json
:language: javascript
Response (microversions 1.19 - )
--------------------------------
.. rest_parameters:: parameters.yaml
- aggregates: aggregates
- resource_provider_generation: resource_provider_generation_v1_19
Response Example (microversions 1.19 - )
----------------------------------------
.. literalinclude:: ./samples/aggregates/get-aggregates-1.19.json
:language: javascript
Update resource provider aggregates
===================================
@ -89,31 +104,47 @@ Associate a list of aggregates with the resource provider identified by `{uuid}`
Normal Response Codes: 200
Error response codes: badRequest(400), itemNotFound(404)
Error response codes: badRequest(400), itemNotFound(404), conflict(409)
Request
-------
Request (microversion 1.1 - 1.18)
---------------------------------
.. rest_parameters:: parameters.yaml
- uuid: resource_provider_uuid_path
- aggregates: aggregates
Request example
---------------
Request example (microversion 1.1 - 1.18)
-----------------------------------------
.. literalinclude:: ./samples/aggregates/update-aggregates-request.json
:language: javascript
Response
--------
Request (microversion 1.19 - )
---------------------------------
.. rest_parameters:: parameters.yaml
- uuid: resource_provider_uuid_path
- aggregates: aggregates
- resource_provider_generation: resource_provider_generation_v1_19
Request example (microversion 1.19 - )
-----------------------------------------
.. literalinclude:: ./samples/aggregates/update-aggregates-request-1.19.json
:language: javascript
Response (microversion 1.19 - )
----------------------------------
.. rest_parameters:: parameters.yaml
- aggregates: aggregates
- resource_provider_generation: resource_provider_generation_v1_19
Response Example
----------------
Response Example (microversion 1.19 - )
------------------------------------------
.. literalinclude:: ./samples/aggregates/update-aggregates.json
.. literalinclude:: ./samples/aggregates/update-aggregates-1.19.json
:language: javascript

View File

@ -310,6 +310,9 @@ resource_provider_generation_optional:
concurrent resource provider updates. The value is ignored;
it is present to preserve symmetry between read and
write representations.
resource_provider_generation_v1_19:
<<: *resource_provider_generation
min_version: 1.19
resource_provider_links:
type: array
in: body

View File

@ -0,0 +1,7 @@
{
"aggregates": [
"42896e0d-205d-4fe3-bd1e-100924931787",
"5e08ea53-c4c6-448e-9334-ac4953de3cfa"
],
"resource_provider_generation": 8
}

View File

@ -0,0 +1,7 @@
{
"aggregates": [
"42896e0d-205d-4fe3-bd1e-100924931787",
"5e08ea53-c4c6-448e-9334-ac4953de3cfa"
],
"resource_provider_generation": 9
}

View File

@ -0,0 +1,7 @@
{
"aggregates": [
"42896e0d-205d-4fe3-bd1e-100924931787",
"5e08ea53-c4c6-448e-9334-ac4953de3cfa"
],
"resource_provider_generation": 9
}

View File

@ -0,0 +1,10 @@
---
features:
- |
Placement API microversion 1.19 enhances the payloads for the
`GET /resource_providers/{uuid}/aggregates` response and the
`PUT /resource_providers/{uuid}/aggregates` request and response to be
identical, and to include the ``resource_provider_generation``. As with
other generation-aware APIs, if the ``resource_provider_generation``
specified in the `PUT` request does not match the generation known by the
server, a 409 Conflict error is returned.