Microversion 1.38: API support for consumer types
Update allocations, reshaper and usage APIs to accept and present consumer_type in microversion 1.38. ensure_consumer in placement/handlers/util.py is updated to be consumer type aware. allocation, usage and reshaper schema and handlers are updated gabbits/consumer-types-1.38.yaml adds tests across the various URIs A TODO is left in placement/handlers/allocation.py where the database is being accessed in a way that is not ideal. This will be cleared up in a followup patch (to add use of an AttributeCache). Co-Authored-By: Surya Seetharaman <suryaseetharaman.9@gmail.com> Co-Authored-By: melanie witt <melwittt@gmail.com> Story: 2005473 Task: 36421 Change-Id: I24c2315093e07dbf25c4fb53152e6a4de7477a51
This commit is contained in:
@@ -44,6 +44,7 @@ Request
|
|||||||
|
|
||||||
- consumer_uuid: consumer_uuid_body
|
- consumer_uuid: consumer_uuid_body
|
||||||
- consumer_generation: consumer_generation_min
|
- consumer_generation: consumer_generation_min
|
||||||
|
- consumer_type: consumer_type
|
||||||
- project_id: project_id_body
|
- project_id: project_id_body
|
||||||
- user_id: user_id_body
|
- user_id: user_id_body
|
||||||
- allocations: allocations_dict_empty
|
- allocations: allocations_dict_empty
|
||||||
@@ -51,9 +52,15 @@ Request
|
|||||||
- resources: resources
|
- resources: resources
|
||||||
- mappings: mappings_in_allocations
|
- mappings: mappings_in_allocations
|
||||||
|
|
||||||
Request example (microversions 1.28 - )
|
Request example (microversions 1.38 - )
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
|
.. literalinclude:: ./samples/allocations/manage-allocations-request-1.38.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
Request example (microversions 1.28 - 1.36)
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
.. literalinclude:: ./samples/allocations/manage-allocations-request-1.28.json
|
.. literalinclude:: ./samples/allocations/manage-allocations-request-1.28.json
|
||||||
:language: javascript
|
:language: javascript
|
||||||
|
|
||||||
@@ -98,12 +105,19 @@ Response
|
|||||||
- generation: resource_provider_generation
|
- generation: resource_provider_generation
|
||||||
- resources: resources
|
- resources: resources
|
||||||
- consumer_generation: consumer_generation_get
|
- consumer_generation: consumer_generation_get
|
||||||
|
- consumer_type: consumer_type
|
||||||
- project_id: project_id_body_1_12
|
- project_id: project_id_body_1_12
|
||||||
- user_id: user_id_body_1_12
|
- user_id: user_id_body_1_12
|
||||||
|
|
||||||
Response Example (1.28 - )
|
Response Example (1.38 - )
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
|
.. literalinclude:: ./samples/allocations/get-allocations-1.38.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
Response Example (1.28 - 1.36)
|
||||||
|
------------------------------
|
||||||
|
|
||||||
.. literalinclude:: ./samples/allocations/get-allocations-1.28.json
|
.. literalinclude:: ./samples/allocations/get-allocations-1.28.json
|
||||||
:language: javascript
|
:language: javascript
|
||||||
|
|
||||||
@@ -146,14 +160,21 @@ Request (microversions 1.12 - )
|
|||||||
- allocations: allocations_dict
|
- allocations: allocations_dict
|
||||||
- resources: resources
|
- resources: resources
|
||||||
- consumer_generation: consumer_generation_min
|
- consumer_generation: consumer_generation_min
|
||||||
|
- consumer_type: consumer_type
|
||||||
- project_id: project_id_body
|
- project_id: project_id_body
|
||||||
- user_id: user_id_body
|
- user_id: user_id_body
|
||||||
- generation: resource_provider_generation_optional
|
- generation: resource_provider_generation_optional
|
||||||
- mappings: mappings_in_allocations
|
- mappings: mappings_in_allocations
|
||||||
|
|
||||||
Request example (microversions 1.28 - )
|
Request example (microversions 1.38 - )
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
|
.. literalinclude:: ./samples/allocations/update-allocations-request-1.38.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
Request example (microversions 1.28 - 1.36)
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
.. literalinclude:: ./samples/allocations/update-allocations-request-1.28.json
|
.. literalinclude:: ./samples/allocations/update-allocations-request-1.28.json
|
||||||
:language: javascript
|
:language: javascript
|
||||||
|
|
||||||
|
@@ -201,6 +201,19 @@ allocation_candidates_same_subtree:
|
|||||||
specified request group must be an ancestor of the rest.
|
specified request group must be an ancestor of the rest.
|
||||||
The ``same_subtree`` query parameter can be repeated and each repeat group
|
The ``same_subtree`` query parameter can be repeated and each repeat group
|
||||||
is treated independently.
|
is treated independently.
|
||||||
|
consumer_type_req:
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
min_version: 1.38
|
||||||
|
description: |
|
||||||
|
A string that consists of numbers, ``A-Z``, and ``_`` describing the
|
||||||
|
consumer type by which to filter usage results. For example, to retrieve
|
||||||
|
only usage information for 'INSTANCE' type consumers a parameter of
|
||||||
|
``consumer_type=INSTANCE`` should be provided.
|
||||||
|
The ``all`` query parameter may be specified to group all results under
|
||||||
|
one key, ``all``. The ``unknown`` query parameter may be specified to
|
||||||
|
group all results under one key, ``unknown``.
|
||||||
project_id: &project_id
|
project_id: &project_id
|
||||||
type: string
|
type: string
|
||||||
in: query
|
in: query
|
||||||
@@ -473,6 +486,13 @@ capacity:
|
|||||||
required: true
|
required: true
|
||||||
description: >
|
description: >
|
||||||
The amount of the resource that the provider can accommodate.
|
The amount of the resource that the provider can accommodate.
|
||||||
|
consumer_count:
|
||||||
|
type: integer
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
min_version: 1.38
|
||||||
|
description: >
|
||||||
|
The number of consumers of a particular ``consumer_type``.
|
||||||
consumer_generation: &consumer_generation
|
consumer_generation: &consumer_generation
|
||||||
type: integer
|
type: integer
|
||||||
in: body
|
in: body
|
||||||
@@ -489,6 +509,18 @@ consumer_generation_get:
|
|||||||
consumer_generation_min:
|
consumer_generation_min:
|
||||||
<<: *consumer_generation
|
<<: *consumer_generation
|
||||||
min_version: 1.28
|
min_version: 1.28
|
||||||
|
consumer_type:
|
||||||
|
type: string
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
min_version: 1.38
|
||||||
|
description: >
|
||||||
|
A string that consists of numbers, ``A-Z``, and ``_`` describing what kind
|
||||||
|
of consumer is creating, or has created, allocations using a quantity of
|
||||||
|
inventory. The string is determined by the client when writing allocations
|
||||||
|
and it is up to the client to ensure correct choices amongst collaborating
|
||||||
|
services. For example, the compute service may choose to type some
|
||||||
|
consumers 'INSTANCE' and others 'MIGRATION'.
|
||||||
consumer_uuid_body:
|
consumer_uuid_body:
|
||||||
<<: *consumer_uuid
|
<<: *consumer_uuid
|
||||||
in: body
|
in: body
|
||||||
@@ -749,6 +781,12 @@ resources:
|
|||||||
required: true
|
required: true
|
||||||
description: >
|
description: >
|
||||||
A dictionary of resource records keyed by resource class name.
|
A dictionary of resource records keyed by resource class name.
|
||||||
|
resources_single:
|
||||||
|
type: integer
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
description: >
|
||||||
|
An amount of resource class consumed in a usage report.
|
||||||
step_size: &step_size
|
step_size: &step_size
|
||||||
type: integer
|
type: integer
|
||||||
in: body
|
in: body
|
||||||
|
@@ -40,6 +40,7 @@ Request
|
|||||||
- allocations.{consumer_uuid}.user_id: user_id_body
|
- allocations.{consumer_uuid}.user_id: user_id_body
|
||||||
- allocations.{consumer_uuid}.mappings: mappings
|
- allocations.{consumer_uuid}.mappings: mappings
|
||||||
- allocations.{consumer_uuid}.consumer_generation: consumer_generation
|
- allocations.{consumer_uuid}.consumer_generation: consumer_generation
|
||||||
|
- allocations.{consumer_uuid}.consumer_type: consumer_type
|
||||||
|
|
||||||
Request Example
|
Request Example
|
||||||
---------------
|
---------------
|
||||||
|
22
api-ref/source/samples/allocations/get-allocations-1.38.json
Normal file
22
api-ref/source/samples/allocations/get-allocations-1.38.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"allocations": {
|
||||||
|
"92637880-2d79-43c6-afab-d860886c6391": {
|
||||||
|
"generation": 2,
|
||||||
|
"resources": {
|
||||||
|
"DISK_GB": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ba8e1ef8-7fa3-41a4-9bb4-d7cb2019899b": {
|
||||||
|
"generation": 8,
|
||||||
|
"resources": {
|
||||||
|
"MEMORY_MB": 512,
|
||||||
|
"VCPU": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"consumer_generation": 1,
|
||||||
|
"project_id": "7e67cbf7-7c38-4a32-b85b-0739c690991a",
|
||||||
|
"user_id": "067f691e-725a-451a-83e2-5c3d13e1dffc",
|
||||||
|
"consumer_type": "INSTANCE"
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"30328d13-e299-4a93-a102-61e4ccabe474": {
|
||||||
|
"consumer_generation": 1,
|
||||||
|
"project_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
|
||||||
|
"user_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
|
||||||
|
"allocations": {
|
||||||
|
"e10927c4-8bc9-465d-ac60-d2f79f7e4a00": {
|
||||||
|
"resources": {
|
||||||
|
"VCPU": 2,
|
||||||
|
"MEMORY_MB": 3
|
||||||
|
},
|
||||||
|
"generation": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"consumer_type": "INSTANCE"
|
||||||
|
},
|
||||||
|
"71921e4e-1629-4c5b-bf8d-338d915d2ef3": {
|
||||||
|
"consumer_generation": 1,
|
||||||
|
"project_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
|
||||||
|
"user_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
|
||||||
|
"allocations": {},
|
||||||
|
"consumer_type": "MIGRATION"
|
||||||
|
},
|
||||||
|
"48c1d40f-45d8-4947-8d46-52b4e1326df8": {
|
||||||
|
"consumer_generation": 1,
|
||||||
|
"project_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
|
||||||
|
"user_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
|
||||||
|
"allocations": {
|
||||||
|
"e10927c4-8bc9-465d-ac60-d2f79f7e4a00": {
|
||||||
|
"resources": {
|
||||||
|
"VCPU": 4,
|
||||||
|
"MEMORY_MB": 5
|
||||||
|
},
|
||||||
|
"generation": 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"consumer_type": "INSTANCE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"allocations": {
|
||||||
|
"4e061c03-611e-4caa-bf26-999dcff4284e": {
|
||||||
|
"resources": {
|
||||||
|
"DISK_GB": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"89873422-1373-46e5-b467-f0c5e6acf08f": {
|
||||||
|
"resources": {
|
||||||
|
"MEMORY_MB": 1024,
|
||||||
|
"VCPU": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"consumer_generation": 1,
|
||||||
|
"user_id": "66cb2f29-c86d-47c3-8af5-69ae7b778c70",
|
||||||
|
"project_id": "42a32c07-3eeb-4401-9373-68a8cdca6784",
|
||||||
|
"consumer_type": "INSTANCE"
|
||||||
|
}
|
||||||
|
|
71
api-ref/source/samples/reshaper/post-reshaper-1.38.json
Normal file
71
api-ref/source/samples/reshaper/post-reshaper-1.38.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"allocations": {
|
||||||
|
"9ae60315-80c2-48a0-a168-ca4f27c307e1": {
|
||||||
|
"allocations": {
|
||||||
|
"a7466641-cd72-499b-b6c9-c208eacecb3d": {
|
||||||
|
"resources": {
|
||||||
|
"DISK_GB": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"project_id": "2f0c4ffc-4c4d-407a-b334-56297b871b7f",
|
||||||
|
"user_id": "cc8a0fe0-2b7c-4392-ae51-747bc73cf473",
|
||||||
|
"consumer_generation": 1,
|
||||||
|
"consumer_type": "INSTANCE"
|
||||||
|
},
|
||||||
|
"4a6444e5-10d6-43f6-9a0b-8acce9309ac9": {
|
||||||
|
"allocations": {
|
||||||
|
"c4ddddbb-01ee-4814-85c9-f57a962c22ba": {
|
||||||
|
"resources": {
|
||||||
|
"VCPU": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"a7466641-cd72-499b-b6c9-c208eacecb3d": {
|
||||||
|
"resources": {
|
||||||
|
"DISK_GB": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"project_id": "2f0c4ffc-4c4d-407a-b334-56297b871b7f",
|
||||||
|
"user_id": "406e1095-71cb-47b9-9b3c-aedb7f663f5a",
|
||||||
|
"consumer_generation": 1,
|
||||||
|
"consumer_type": "INSTANCE"
|
||||||
|
},
|
||||||
|
"e10e7ca0-2ac5-4c98-bad9-51c95b1930ed": {
|
||||||
|
"allocations": {
|
||||||
|
"c4ddddbb-01ee-4814-85c9-f57a962c22ba": {
|
||||||
|
"resources": {
|
||||||
|
"VCPU": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"project_id": "2f0c4ffc-4c4d-407a-b334-56297b871b7f",
|
||||||
|
"user_id": "cc8a0fe0-2b7c-4392-ae51-747bc73cf473",
|
||||||
|
"consumer_generation": 1,
|
||||||
|
"consumer_type": "INSTANCE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inventories": {
|
||||||
|
"c4ddddbb-01ee-4814-85c9-f57a962c22ba": {
|
||||||
|
"inventories": {
|
||||||
|
"VCPU": {
|
||||||
|
"max_unit": 8,
|
||||||
|
"total": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resource_provider_generation": null
|
||||||
|
},
|
||||||
|
"a7466641-cd72-499b-b6c9-c208eacecb3d": {
|
||||||
|
"inventories": {
|
||||||
|
"DISK_GB": {
|
||||||
|
"min_unit": 10,
|
||||||
|
"total": 2048,
|
||||||
|
"max_unit": 1200,
|
||||||
|
"step_size": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resource_provider_generation": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
22
api-ref/source/samples/usages/get-usages-1.38.json
Normal file
22
api-ref/source/samples/usages/get-usages-1.38.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"usages" : {
|
||||||
|
"INSTANCE" : {
|
||||||
|
"consumer_count" : 5,
|
||||||
|
"MEMORY_MB" : 512,
|
||||||
|
"VCPU" : 2,
|
||||||
|
"DISK_GB" : 5
|
||||||
|
},
|
||||||
|
"MIGRATION" : {
|
||||||
|
"DISK_GB" : 5,
|
||||||
|
"VCPU" : 2,
|
||||||
|
"consumer_count" : 2,
|
||||||
|
"MEMORY_MB" : 512
|
||||||
|
},
|
||||||
|
"unknown" : {
|
||||||
|
"VCPU" : 2,
|
||||||
|
"DISK_GB" : 5,
|
||||||
|
"consumer_count" : 1,
|
||||||
|
"MEMORY_MB" : 512
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -28,16 +28,32 @@ Request
|
|||||||
|
|
||||||
- project_id: project_id
|
- project_id: project_id
|
||||||
- user_id: user_id
|
- user_id: user_id
|
||||||
|
- consumer_type: consumer_type_req
|
||||||
|
|
||||||
Response
|
Response (microversions 1.38 - )
|
||||||
--------
|
--------------------------------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- usages.consumer_type: consumer_type
|
||||||
|
- usages.consumer_type.consumer_count: consumer_count
|
||||||
|
- usages.consumer_type.RESOURCE_CLASS: resources_single
|
||||||
|
|
||||||
|
Response Example (microversions 1.38 - )
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. literalinclude:: ./samples/usages/get-usages-1.38.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
Response (microversions 1.9 - 1.36)
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
.. rest_parameters:: parameters.yaml
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
- usages: resources
|
- usages: resources
|
||||||
|
|
||||||
Response Example
|
Response Example (microversions 1.9 - 1.36)
|
||||||
----------------
|
-------------------------------------------
|
||||||
|
|
||||||
.. literalinclude:: ./samples/usages/get-usages.json
|
.. literalinclude:: ./samples/usages/get-usages.json
|
||||||
:language: javascript
|
:language: javascript
|
||||||
|
@@ -22,11 +22,13 @@ from oslo_utils import timeutils
|
|||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
|
from placement import db_api
|
||||||
from placement import errors
|
from placement import errors
|
||||||
from placement import exception
|
from placement import exception
|
||||||
from placement.handlers import util as data_util
|
from placement.handlers import util as data_util
|
||||||
from placement import microversion
|
from placement import microversion
|
||||||
from placement.objects import allocation as alloc_obj
|
from placement.objects import allocation as alloc_obj
|
||||||
|
from placement.objects import consumer_type
|
||||||
from placement.objects import resource_provider as rp_obj
|
from placement.objects import resource_provider as rp_obj
|
||||||
from placement.policies import allocation as policies
|
from placement.policies import allocation as policies
|
||||||
from placement.schemas import allocation as schema
|
from placement.schemas import allocation as schema
|
||||||
@@ -55,7 +57,7 @@ def _last_modified_from_allocations(allocations, want_version):
|
|||||||
return last_modified
|
return last_modified
|
||||||
|
|
||||||
|
|
||||||
def _serialize_allocations_for_consumer(allocations, want_version):
|
def _serialize_allocations_for_consumer(context, allocations, want_version):
|
||||||
"""Turn a list of allocations into a dict by resource provider uuid.
|
"""Turn a list of allocations into a dict by resource provider uuid.
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -80,6 +82,8 @@ def _serialize_allocations_for_consumer(allocations, want_version):
|
|||||||
'user_id': USER_ID,
|
'user_id': USER_ID,
|
||||||
# Generation for consumer >= 1.28
|
# Generation for consumer >= 1.28
|
||||||
'consumer_generation': 1
|
'consumer_generation': 1
|
||||||
|
# Consumer Type for consumer >= 1.38
|
||||||
|
'consumer_type': INSTANCE
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
allocation_data = collections.defaultdict(dict)
|
allocation_data = collections.defaultdict(dict)
|
||||||
@@ -105,6 +109,18 @@ def _serialize_allocations_for_consumer(allocations, want_version):
|
|||||||
show_consumer_gen = want_version.matches((1, 28))
|
show_consumer_gen = want_version.matches((1, 28))
|
||||||
if show_consumer_gen:
|
if show_consumer_gen:
|
||||||
result['consumer_generation'] = consumer.generation
|
result['consumer_generation'] = consumer.generation
|
||||||
|
show_consumer_type = want_version.matches((1, 38))
|
||||||
|
if show_consumer_type:
|
||||||
|
# TODO(cdent): This should either access a subclass of
|
||||||
|
# AttributeCache or the data returned from the persistence layer
|
||||||
|
# should already have a name. We want to avoid accessing the
|
||||||
|
# database from the handler layer repeated times.
|
||||||
|
if consumer.consumer_type_id:
|
||||||
|
con_type = consumer_type.ConsumerType.get_by_id(
|
||||||
|
context, consumer.consumer_type_id).name
|
||||||
|
else:
|
||||||
|
con_type = consumer_type.DEFAULT_CONSUMER_TYPE
|
||||||
|
result['consumer_type'] = con_type
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -222,10 +238,11 @@ def inspect_consumers(context, data, want_version):
|
|||||||
project_id = data[consumer_uuid]['project_id']
|
project_id = data[consumer_uuid]['project_id']
|
||||||
user_id = data[consumer_uuid]['user_id']
|
user_id = data[consumer_uuid]['user_id']
|
||||||
consumer_generation = data[consumer_uuid].get('consumer_generation')
|
consumer_generation = data[consumer_uuid].get('consumer_generation')
|
||||||
|
consumer_type = data[consumer_uuid].get('consumer_type')
|
||||||
try:
|
try:
|
||||||
consumer, new_consumer_created = data_util.ensure_consumer(
|
consumer, new_consumer_created = data_util.ensure_consumer(
|
||||||
context, consumer_uuid, project_id, user_id,
|
context, consumer_uuid, project_id, user_id,
|
||||||
consumer_generation, want_version)
|
consumer_generation, consumer_type, want_version)
|
||||||
if new_consumer_created:
|
if new_consumer_created:
|
||||||
new_consumers_created.append(consumer)
|
new_consumers_created.append(consumer)
|
||||||
consumers[consumer_uuid] = consumer
|
consumers[consumer_uuid] = consumer
|
||||||
@@ -252,7 +269,8 @@ def list_for_consumer(req):
|
|||||||
# consumer id.
|
# consumer id.
|
||||||
allocations = alloc_obj.get_all_by_consumer_id(context, consumer_id)
|
allocations = alloc_obj.get_all_by_consumer_id(context, consumer_id)
|
||||||
|
|
||||||
output = _serialize_allocations_for_consumer(allocations, want_version)
|
output = _serialize_allocations_for_consumer(
|
||||||
|
context, allocations, want_version)
|
||||||
last_modified = _last_modified_from_allocations(allocations, want_version)
|
last_modified = _last_modified_from_allocations(allocations, want_version)
|
||||||
allocations_json = jsonutils.dumps(output)
|
allocations_json = jsonutils.dumps(output)
|
||||||
|
|
||||||
@@ -392,6 +410,21 @@ def _set_allocations_for_consumer(req, schema):
|
|||||||
}
|
}
|
||||||
allocation_data = allocations_dict
|
allocation_data = allocations_dict
|
||||||
|
|
||||||
|
# NOTE(melwitt): Group all of the database updates in a single transaction
|
||||||
|
# so that updates get rolled back automatically in the event of a consumer
|
||||||
|
# generation conflict.
|
||||||
|
_set_allocations_for_consumer_same_transaction(
|
||||||
|
context, consumer_uuid, data, allocation_data, want_version)
|
||||||
|
|
||||||
|
req.response.status = 204
|
||||||
|
req.response.content_type = None
|
||||||
|
return req.response
|
||||||
|
|
||||||
|
|
||||||
|
@db_api.placement_context_manager.writer
|
||||||
|
def _set_allocations_for_consumer_same_transaction(context, consumer_uuid,
|
||||||
|
data, allocation_data,
|
||||||
|
want_version):
|
||||||
allocation_objects = []
|
allocation_objects = []
|
||||||
# Consumer object saved in case we need to delete the auto-created consumer
|
# Consumer object saved in case we need to delete the auto-created consumer
|
||||||
# record
|
# record
|
||||||
@@ -408,7 +441,8 @@ def _set_allocations_for_consumer(req, schema):
|
|||||||
# no consumer generation, so this is safe to do.
|
# no consumer generation, so this is safe to do.
|
||||||
data_util.ensure_consumer(
|
data_util.ensure_consumer(
|
||||||
context, consumer_uuid, data.get('project_id'),
|
context, consumer_uuid, data.get('project_id'),
|
||||||
data.get('user_id'), data.get('consumer_generation'), want_version)
|
data.get('user_id'), data.get('consumer_generation'),
|
||||||
|
data.get('consumer_type'), want_version)
|
||||||
allocations = alloc_obj.get_all_by_consumer_id(context, consumer_uuid)
|
allocations = alloc_obj.get_all_by_consumer_id(context, consumer_uuid)
|
||||||
for allocation in allocations:
|
for allocation in allocations:
|
||||||
allocation.used = 0
|
allocation.used = 0
|
||||||
@@ -420,7 +454,7 @@ def _set_allocations_for_consumer(req, schema):
|
|||||||
consumer, created_new_consumer = data_util.ensure_consumer(
|
consumer, created_new_consumer = data_util.ensure_consumer(
|
||||||
context, consumer_uuid, data.get('project_id'),
|
context, consumer_uuid, data.get('project_id'),
|
||||||
data.get('user_id'), data.get('consumer_generation'),
|
data.get('user_id'), data.get('consumer_generation'),
|
||||||
want_version)
|
data.get('consumer_type'), want_version)
|
||||||
for resource_provider_uuid, allocation in allocation_data.items():
|
for resource_provider_uuid, allocation in allocation_data.items():
|
||||||
resource_provider = rp_objs[resource_provider_uuid]
|
resource_provider = rp_objs[resource_provider_uuid]
|
||||||
new_allocations = _new_allocations(context,
|
new_allocations = _new_allocations(context,
|
||||||
@@ -456,10 +490,6 @@ def _set_allocations_for_consumer(req, schema):
|
|||||||
'allocate: %(error)s' % {'error': exc},
|
'allocate: %(error)s' % {'error': exc},
|
||||||
comment=errors.CONCURRENT_UPDATE)
|
comment=errors.CONCURRENT_UPDATE)
|
||||||
|
|
||||||
req.response.status = 204
|
|
||||||
req.response.content_type = None
|
|
||||||
return req.response
|
|
||||||
|
|
||||||
|
|
||||||
@wsgi_wrapper.PlacementWsgify
|
@wsgi_wrapper.PlacementWsgify
|
||||||
@microversion.version_handler('1.0', '1.7')
|
@microversion.version_handler('1.0', '1.7')
|
||||||
@@ -490,12 +520,19 @@ def set_allocations_for_consumer(req): # noqa
|
|||||||
|
|
||||||
|
|
||||||
@wsgi_wrapper.PlacementWsgify # noqa
|
@wsgi_wrapper.PlacementWsgify # noqa
|
||||||
@microversion.version_handler('1.34')
|
@microversion.version_handler('1.34', '1.37')
|
||||||
@util.require_content('application/json')
|
@util.require_content('application/json')
|
||||||
def set_allocations_for_consumer(req): # noqa
|
def set_allocations_for_consumer(req): # noqa
|
||||||
return _set_allocations_for_consumer(req, schema.ALLOCATION_SCHEMA_V1_34)
|
return _set_allocations_for_consumer(req, schema.ALLOCATION_SCHEMA_V1_34)
|
||||||
|
|
||||||
|
|
||||||
|
@wsgi_wrapper.PlacementWsgify # noqa
|
||||||
|
@microversion.version_handler('1.38')
|
||||||
|
@util.require_content('application/json')
|
||||||
|
def set_allocations_for_consumer(req): # noqa
|
||||||
|
return _set_allocations_for_consumer(req, schema.ALLOCATION_SCHEMA_V1_38)
|
||||||
|
|
||||||
|
|
||||||
@wsgi_wrapper.PlacementWsgify
|
@wsgi_wrapper.PlacementWsgify
|
||||||
@microversion.version_handler('1.13')
|
@microversion.version_handler('1.13')
|
||||||
@util.require_content('application/json')
|
@util.require_content('application/json')
|
||||||
@@ -508,6 +545,8 @@ def set_allocations(req):
|
|||||||
want_schema = schema.POST_ALLOCATIONS_V1_28
|
want_schema = schema.POST_ALLOCATIONS_V1_28
|
||||||
if want_version.matches((1, 34)):
|
if want_version.matches((1, 34)):
|
||||||
want_schema = schema.POST_ALLOCATIONS_V1_34
|
want_schema = schema.POST_ALLOCATIONS_V1_34
|
||||||
|
if want_version.matches((1, 38)):
|
||||||
|
want_schema = schema.POST_ALLOCATIONS_V1_38
|
||||||
data = util.extract_json(req.body, want_schema)
|
data = util.extract_json(req.body, want_schema)
|
||||||
|
|
||||||
consumers, new_consumers_created = inspect_consumers(
|
consumers, new_consumers_created = inspect_consumers(
|
||||||
|
@@ -46,7 +46,9 @@ def reshape(req):
|
|||||||
context.can(policies.RESHAPE)
|
context.can(policies.RESHAPE)
|
||||||
|
|
||||||
reshaper_schema = schema.POST_RESHAPER_SCHEMA
|
reshaper_schema = schema.POST_RESHAPER_SCHEMA
|
||||||
if want_version.matches((1, 34)):
|
if want_version.matches((1, 38)):
|
||||||
|
reshaper_schema = schema.POST_RESHAPER_SCHEMA_V1_38
|
||||||
|
elif want_version.matches((1, 34)):
|
||||||
reshaper_schema = schema.POST_RESHAPER_SCHEMA_V1_34
|
reshaper_schema = schema.POST_RESHAPER_SCHEMA_V1_34
|
||||||
data = util.extract_json(req.body, reshaper_schema)
|
data = util.extract_json(req.body, reshaper_schema)
|
||||||
inventories = data['inventories']
|
inventories = data['inventories']
|
||||||
|
@@ -11,6 +11,8 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
"""Placement API handlers for usage information."""
|
"""Placement API handlers for usage information."""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
from oslo_utils import encodeutils
|
from oslo_utils import encodeutils
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
@@ -90,6 +92,7 @@ def get_total_usages(req):
|
|||||||
"""
|
"""
|
||||||
project_id = req.GET.get('project_id')
|
project_id = req.GET.get('project_id')
|
||||||
user_id = req.GET.get('user_id')
|
user_id = req.GET.get('user_id')
|
||||||
|
consumer_type = req.GET.get('consumer_type')
|
||||||
|
|
||||||
context = req.environ['placement.context']
|
context = req.environ['placement.context']
|
||||||
context.can(
|
context.can(
|
||||||
@@ -97,14 +100,36 @@ def get_total_usages(req):
|
|||||||
target={'project_id': project_id})
|
target={'project_id': project_id})
|
||||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||||
|
|
||||||
util.validate_query_params(req, schema.GET_USAGES_SCHEMA_1_9)
|
want_schema = schema.GET_USAGES_SCHEMA_1_9
|
||||||
|
show_consumer_type = want_version.matches((1, 38))
|
||||||
|
if show_consumer_type:
|
||||||
|
want_schema = schema.GET_USAGES_SCHEMA_V1_38
|
||||||
|
util.validate_query_params(req, want_schema)
|
||||||
|
|
||||||
usages = usage_obj.get_all_by_project_user(context, project_id,
|
if show_consumer_type:
|
||||||
user_id=user_id)
|
usages = usage_obj.get_by_consumer_type(
|
||||||
|
context, project_id, user_id=user_id, consumer_type=consumer_type)
|
||||||
|
else:
|
||||||
|
usages = usage_obj.get_all_by_project_user(context, project_id,
|
||||||
|
user_id=user_id)
|
||||||
|
|
||||||
response = req.response
|
response = req.response
|
||||||
usages_dict = {'usages': {resource.resource_class: resource.usage
|
if show_consumer_type:
|
||||||
for resource in usages}}
|
usage = collections.defaultdict(dict)
|
||||||
|
for resource in usages:
|
||||||
|
ct = resource.consumer_type
|
||||||
|
rc = resource.resource_class
|
||||||
|
cc = resource.consumer_count
|
||||||
|
used = resource.usage
|
||||||
|
usage[ct][rc] = used
|
||||||
|
usage[ct]['consumer_count'] = cc
|
||||||
|
usages_dict = {
|
||||||
|
'usages': usage
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
usages_dict = {'usages': {resource.resource_class: resource.usage
|
||||||
|
for resource in usages}}
|
||||||
|
|
||||||
response.body = encodeutils.to_utf8(jsonutils.dumps(usages_dict))
|
response.body = encodeutils.to_utf8(jsonutils.dumps(usages_dict))
|
||||||
req.response.content_type = 'application/json'
|
req.response.content_type = 'application/json'
|
||||||
if want_version.matches((1, 15)):
|
if want_version.matches((1, 15)):
|
||||||
|
@@ -17,6 +17,7 @@ import webob
|
|||||||
from placement import errors
|
from placement import errors
|
||||||
from placement import exception
|
from placement import exception
|
||||||
from placement.objects import consumer as consumer_obj
|
from placement.objects import consumer as consumer_obj
|
||||||
|
from placement.objects import consumer_type as consumer_type_obj
|
||||||
from placement.objects import project as project_obj
|
from placement.objects import project as project_obj
|
||||||
from placement.objects import user as user_obj
|
from placement.objects import user as user_obj
|
||||||
|
|
||||||
@@ -24,8 +25,78 @@ from placement.objects import user as user_obj
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_consumer_type_id(ctx, name):
|
||||||
|
"""Tries to fetch the provided consumer_type and creates a new one if it
|
||||||
|
does not exist.
|
||||||
|
|
||||||
|
:param ctx: The request context.
|
||||||
|
:param name: The name of the consumer type.
|
||||||
|
:returns: The id of the ConsumerType object.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cons_type = consumer_type_obj.ConsumerType.get_by_name(ctx, name)
|
||||||
|
except exception.ConsumerTypeNotFound:
|
||||||
|
cons_type = consumer_type_obj.ConsumerType(ctx, name=name)
|
||||||
|
try:
|
||||||
|
cons_type.create()
|
||||||
|
except exception.ConsumerTypeExists:
|
||||||
|
# another thread created concurrently, so try again
|
||||||
|
return fetch_consumer_type_id(ctx, name)
|
||||||
|
return cons_type.id
|
||||||
|
|
||||||
|
|
||||||
|
def _get_or_create_project(ctx, project_id):
|
||||||
|
try:
|
||||||
|
proj = project_obj.Project.get_by_external_id(ctx, project_id)
|
||||||
|
except exception.NotFound:
|
||||||
|
# Auto-create the project if we found no record of it...
|
||||||
|
try:
|
||||||
|
proj = project_obj.Project(ctx, external_id=project_id)
|
||||||
|
proj.create()
|
||||||
|
except exception.ProjectExists:
|
||||||
|
# No worries, another thread created this project already
|
||||||
|
proj = project_obj.Project.get_by_external_id(ctx, project_id)
|
||||||
|
return proj
|
||||||
|
|
||||||
|
|
||||||
|
def _get_or_create_user(ctx, user_id):
|
||||||
|
try:
|
||||||
|
user = user_obj.User.get_by_external_id(ctx, user_id)
|
||||||
|
except exception.NotFound:
|
||||||
|
# Auto-create the user if we found no record of it...
|
||||||
|
try:
|
||||||
|
user = user_obj.User(ctx, external_id=user_id)
|
||||||
|
user.create()
|
||||||
|
except exception.UserExists:
|
||||||
|
# No worries, another thread created this user already
|
||||||
|
user = user_obj.User.get_by_external_id(ctx, user_id)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def _create_consumer(ctx, consumer_uuid, project, user, consumer_type_id):
|
||||||
|
created_new_consumer = False
|
||||||
|
try:
|
||||||
|
consumer = consumer_obj.Consumer(
|
||||||
|
ctx, uuid=consumer_uuid, project=project, user=user,
|
||||||
|
consumer_type_id=consumer_type_id)
|
||||||
|
consumer.create()
|
||||||
|
created_new_consumer = True
|
||||||
|
except exception.ConsumerExists:
|
||||||
|
# Another thread created this consumer already, verify whether
|
||||||
|
# the consumer type matches
|
||||||
|
consumer = consumer_obj.Consumer.get_by_uuid(ctx, consumer_uuid)
|
||||||
|
# If the types don't match, update the consumer record
|
||||||
|
if consumer_type_id != consumer.consumer_type_id:
|
||||||
|
LOG.debug("Supplied consumer type for consumer %s was "
|
||||||
|
"different than existing record. Updating "
|
||||||
|
"consumer record.", consumer_uuid)
|
||||||
|
consumer.consumer_type_id = consumer_type_id
|
||||||
|
consumer.update()
|
||||||
|
return consumer, created_new_consumer
|
||||||
|
|
||||||
|
|
||||||
def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
||||||
consumer_generation, want_version):
|
consumer_generation, consumer_type, want_version):
|
||||||
"""Ensures there are records in the consumers, projects and users table for
|
"""Ensures there are records in the consumers, projects and users table for
|
||||||
the supplied external identifiers.
|
the supplied external identifiers.
|
||||||
|
|
||||||
@@ -44,35 +115,19 @@ def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
|||||||
:param user_id: The external ID of the user consuming the resources.
|
:param user_id: The external ID of the user consuming the resources.
|
||||||
:param consumer_generation: The generation provided by the user for this
|
:param consumer_generation: The generation provided by the user for this
|
||||||
consumer.
|
consumer.
|
||||||
|
:param consumer_type: The type of consumer provided by the user.
|
||||||
:param want_version: the microversion matcher.
|
:param want_version: the microversion matcher.
|
||||||
:raises webob.exc.HTTPConflict if consumer generation is required and there
|
:raises webob.exc.HTTPConflict if consumer generation is required and there
|
||||||
was a mismatch
|
was a mismatch
|
||||||
"""
|
"""
|
||||||
created_new_consumer = False
|
created_new_consumer = False
|
||||||
requires_consumer_generation = want_version.matches((1, 28))
|
requires_consumer_generation = want_version.matches((1, 28))
|
||||||
|
requires_consumer_type = want_version.matches((1, 38))
|
||||||
if project_id is None:
|
if project_id is None:
|
||||||
project_id = ctx.config.placement.incomplete_consumer_project_id
|
project_id = ctx.config.placement.incomplete_consumer_project_id
|
||||||
user_id = ctx.config.placement.incomplete_consumer_user_id
|
user_id = ctx.config.placement.incomplete_consumer_user_id
|
||||||
try:
|
proj = _get_or_create_project(ctx, project_id)
|
||||||
proj = project_obj.Project.get_by_external_id(ctx, project_id)
|
user = _get_or_create_user(ctx, user_id)
|
||||||
except exception.NotFound:
|
|
||||||
# Auto-create the project if we found no record of it...
|
|
||||||
try:
|
|
||||||
proj = project_obj.Project(ctx, external_id=project_id)
|
|
||||||
proj.create()
|
|
||||||
except exception.ProjectExists:
|
|
||||||
# No worries, another thread created this project already
|
|
||||||
proj = project_obj.Project.get_by_external_id(ctx, project_id)
|
|
||||||
try:
|
|
||||||
user = user_obj.User.get_by_external_id(ctx, user_id)
|
|
||||||
except exception.NotFound:
|
|
||||||
# Auto-create the user if we found no record of it...
|
|
||||||
try:
|
|
||||||
user = user_obj.User(ctx, external_id=user_id)
|
|
||||||
user.create()
|
|
||||||
except exception.UserExists:
|
|
||||||
# No worries, another thread created this user already
|
|
||||||
user = user_obj.User.get_by_external_id(ctx, user_id)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
consumer = consumer_obj.Consumer.get_by_uuid(ctx, consumer_uuid)
|
consumer = consumer_obj.Consumer.get_by_uuid(ctx, consumer_uuid)
|
||||||
@@ -101,14 +156,14 @@ def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
|||||||
# existing consumer's record. If the eventual call to
|
# existing consumer's record. If the eventual call to
|
||||||
# AllocationList.replace_all() fails for whatever reason (say, a
|
# AllocationList.replace_all() fails for whatever reason (say, a
|
||||||
# resource provider generation conflict or out of resources failure),
|
# resource provider generation conflict or out of resources failure),
|
||||||
# we will end up deleting the auto-created consumer but we MAY not undo
|
# we will end up deleting the auto-created consumer and we will undo
|
||||||
# the changes to the second consumer's project and user ID. I say MAY
|
# the changes to the second consumer's project and user ID.
|
||||||
# and not WILL NOT because I'm not sure that the exception that gets
|
# NOTE(melwitt): The aforementioned rollback of changes is predicated
|
||||||
# raised from AllocationList.replace_all() will cause the context
|
# on the fact that the same transaction context is used for both
|
||||||
# manager's transaction to rollback automatically. I believe that the
|
# util.ensure_consumer() and AllocationList.replace_all() within the
|
||||||
# same transaction context is used for both util.ensure_consumer() and
|
# same HTTP request. The @db_api.placement_context_manager.writer
|
||||||
# AllocationList.replace_all() within the same HTTP request, but need
|
# decorator on the outermost method will nest to methods called within
|
||||||
# to test this to be 100% certain...
|
# the outermost method.
|
||||||
if (project_id != consumer.project.external_id or
|
if (project_id != consumer.project.external_id or
|
||||||
user_id != consumer.user.external_id):
|
user_id != consumer.user.external_id):
|
||||||
LOG.debug("Supplied project or user ID for consumer %s was "
|
LOG.debug("Supplied project or user ID for consumer %s was "
|
||||||
@@ -117,6 +172,15 @@ def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
|||||||
consumer.project = proj
|
consumer.project = proj
|
||||||
consumer.user = user
|
consumer.user = user
|
||||||
consumer.update()
|
consumer.update()
|
||||||
|
# Update the consumer type if it's different than the existing one.
|
||||||
|
if requires_consumer_type:
|
||||||
|
cons_type_id = fetch_consumer_type_id(ctx, consumer_type)
|
||||||
|
if cons_type_id != consumer.consumer_type_id:
|
||||||
|
LOG.debug("Supplied consumer type for consumer %s was "
|
||||||
|
"different than existing record. Updating "
|
||||||
|
"consumer record.", consumer_uuid)
|
||||||
|
consumer.consumer_type_id = cons_type_id
|
||||||
|
consumer.update()
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
# If we are attempting to modify or create allocations after 1.26, we
|
# If we are attempting to modify or create allocations after 1.26, we
|
||||||
# need a consumer generation specified. The user must have specified
|
# need a consumer generation specified. The user must have specified
|
||||||
@@ -129,14 +193,11 @@ def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
|||||||
'consumer generation conflict - '
|
'consumer generation conflict - '
|
||||||
'expected null but got %s' % consumer_generation,
|
'expected null but got %s' % consumer_generation,
|
||||||
comment=errors.CONCURRENT_UPDATE)
|
comment=errors.CONCURRENT_UPDATE)
|
||||||
|
cons_type_id = (fetch_consumer_type_id(ctx, consumer_type)
|
||||||
|
if requires_consumer_type else None)
|
||||||
# No such consumer. This is common for new allocations. Create the
|
# No such consumer. This is common for new allocations. Create the
|
||||||
# consumer record
|
# consumer record
|
||||||
try:
|
consumer, created_new_consumer = _create_consumer(
|
||||||
consumer = consumer_obj.Consumer(
|
ctx, consumer_uuid, proj, user, cons_type_id)
|
||||||
ctx, uuid=consumer_uuid, project=proj, user=user)
|
|
||||||
consumer.create()
|
|
||||||
created_new_consumer = True
|
|
||||||
except exception.ConsumerExists:
|
|
||||||
# No worries, another thread created this user already
|
|
||||||
consumer = consumer_obj.Consumer.get_by_uuid(ctx, consumer_uuid)
|
|
||||||
return consumer, created_new_consumer
|
return consumer, created_new_consumer
|
||||||
|
@@ -89,6 +89,16 @@ VERSIONS = [
|
|||||||
'1.36', # Add a `same_subtree` parameter on GET /allocation_candidates
|
'1.36', # Add a `same_subtree` parameter on GET /allocation_candidates
|
||||||
# and allow resourceless requests for groups in `same_subtree`.
|
# and allow resourceless requests for groups in `same_subtree`.
|
||||||
'1.37', # Allow re-parenting and un-parenting resource providers
|
'1.37', # Allow re-parenting and un-parenting resource providers
|
||||||
|
'1.38', # Adds ``consumer_type`` (required) key in the request body of
|
||||||
|
# ``POST /allocations``, ``PUT /allocations/{consumer_uuid}``
|
||||||
|
# and in the response of ``GET /allocations/{consumer_uuid}``.
|
||||||
|
# ``GET /usages`` request will also gain ``consumer_type`` key as
|
||||||
|
# an optional queryparam to filter usages based on consumer_types.
|
||||||
|
# ``GET /usages`` response will group results based on the
|
||||||
|
# consumer type and will include a new ``consumer_count`` key per
|
||||||
|
# type irrespective of whether the ``consumer_type`` was specified
|
||||||
|
# in the request. The corresponding changes to ``/reshaper`` are
|
||||||
|
# included.
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@@ -10,6 +10,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import distinct
|
from sqlalchemy import distinct
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy import sql
|
from sqlalchemy import sql
|
||||||
@@ -87,12 +88,15 @@ def _get_all_by_project_user(context, project_id, user_id=None,
|
|||||||
query = query.filter(models.User.external_id == user_id)
|
query = query.filter(models.User.external_id == user_id)
|
||||||
query = query.group_by(models.Allocation.resource_class_id)
|
query = query.group_by(models.Allocation.resource_class_id)
|
||||||
|
|
||||||
if consumer_type:
|
# Handle 'all' or 'unknown' consumer type. The consumer_type is True if
|
||||||
# NOTE(melwitt): We have to count separately in order to get a count of
|
# 'all' is requested, and None if 'unknown' is requested.
|
||||||
# unique consumers. If we count after grouping by resource class, we
|
if consumer_type is None or consumer_type:
|
||||||
# will count duplicate consumers for any unique consumer that consumes
|
# NOTE(melwitt): We have to count the number of consumers in a separate
|
||||||
# more than one resource class simultaneously (example: an instance
|
# query in order to get a count of unique consumers. If we count in the
|
||||||
# consuming both VCPU and MEMORY_MB).
|
# same query after grouping by resource class, we will count duplicate
|
||||||
|
# consumers for any unique consumer that consumes more than one
|
||||||
|
# resource class simultaneously (example: an instance consuming both
|
||||||
|
# VCPU and MEMORY_MB).
|
||||||
count_query = (context.session.query(
|
count_query = (context.session.query(
|
||||||
func.count(distinct(models.Allocation.consumer_id)))
|
func.count(distinct(models.Allocation.consumer_id)))
|
||||||
.join(models.Consumer,
|
.join(models.Consumer,
|
||||||
@@ -105,12 +109,19 @@ def _get_all_by_project_user(context, project_id, user_id=None,
|
|||||||
models.User, models.Consumer.user_id == models.User.id)
|
models.User, models.Consumer.user_id == models.User.id)
|
||||||
count_query = count_query.filter(
|
count_query = count_query.filter(
|
||||||
models.User.external_id == user_id)
|
models.User.external_id == user_id)
|
||||||
unique_consumer_count = count_query.scalar()
|
if consumer_type is None:
|
||||||
|
count_query = count_query.filter(
|
||||||
|
models.Consumer.consumer_type_id == sa.null())
|
||||||
|
number_of_unique_consumers = count_query.scalar()
|
||||||
|
|
||||||
|
# Filter for unknown consumer type if specified.
|
||||||
|
if consumer_type is None:
|
||||||
|
query = query.filter(models.Consumer.consumer_type_id == sa.null())
|
||||||
|
|
||||||
result = [dict(resource_class=context.rc_cache.string_from_id(item[0]),
|
result = [dict(resource_class=context.rc_cache.string_from_id(item[0]),
|
||||||
usage=item[1],
|
usage=item[1],
|
||||||
consumer_type="all",
|
consumer_type='all' if consumer_type else 'unknown',
|
||||||
consumer_count=unique_consumer_count)
|
consumer_count=number_of_unique_consumers)
|
||||||
for item in query.all()]
|
for item in query.all()]
|
||||||
else:
|
else:
|
||||||
result = [dict(resource_class=context.rc_cache.string_from_id(item[0]),
|
result = [dict(resource_class=context.rc_cache.string_from_id(item[0]),
|
||||||
@@ -123,9 +134,11 @@ def _get_all_by_project_user(context, project_id, user_id=None,
|
|||||||
@db_api.placement_context_manager.reader
|
@db_api.placement_context_manager.reader
|
||||||
def _get_by_consumer_type(context, project_id, user_id=None,
|
def _get_by_consumer_type(context, project_id, user_id=None,
|
||||||
consumer_type=None):
|
consumer_type=None):
|
||||||
if consumer_type == 'all':
|
if consumer_type in ('all', 'unknown'):
|
||||||
|
cons_type = True if consumer_type == 'all' else None
|
||||||
return _get_all_by_project_user(context, project_id, user_id,
|
return _get_all_by_project_user(context, project_id, user_id,
|
||||||
consumer_type=True)
|
consumer_type=cons_type)
|
||||||
|
|
||||||
query = (context.session.query(
|
query = (context.session.query(
|
||||||
models.Allocation.resource_class_id,
|
models.Allocation.resource_class_id,
|
||||||
func.coalesce(func.sum(models.Allocation.used), 0),
|
func.coalesce(func.sum(models.Allocation.used), 0),
|
||||||
|
@@ -677,4 +677,24 @@ Add support for re-parenting and un-parenting a resource provider via ``PUT
|
|||||||
/resource_providers/{uuid}`` API by allowing changing the
|
/resource_providers/{uuid}`` API by allowing changing the
|
||||||
``parent_provider_uuid`` to any existing provider, except providers in same
|
``parent_provider_uuid`` to any existing provider, except providers in same
|
||||||
subtree. Un-parenting can be achieved by setting the ``parent_provider_uuid``
|
subtree. Un-parenting can be achieved by setting the ``parent_provider_uuid``
|
||||||
to ``null``. This means that the provider becomes a new root provider.
|
to ``null``. This means that the provider becomes a new root provider.
|
||||||
|
|
||||||
|
1.38 - Support consumer_type in allocations, usage and reshaper
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. versionadded:: Xena
|
||||||
|
|
||||||
|
Adds support for a ``consumer_type`` (required) key in the request body of
|
||||||
|
``POST /allocations``, ``PUT /allocations/{consumer_uuid}`` and in the response
|
||||||
|
of ``GET /allocations/{consumer_uuid}``. ``GET /usages`` requests gain a
|
||||||
|
``consumer_type`` key as an optional query parameter to filter usages based on
|
||||||
|
consumer_types. ``GET /usages`` response will group results based on the
|
||||||
|
consumer type and will include a new ``consumer_count`` key per type
|
||||||
|
irrespective of whether the ``consumer_type`` was specified in the request. If
|
||||||
|
an ``all`` ``consumer_type`` key is provided, all results are grouped under one
|
||||||
|
key, ``all``. Older allocations which were not created with a consumer type are
|
||||||
|
considered to have an ``unknown`` ``consumer_type``. If an ``unknown``
|
||||||
|
``consumer_type`` key is provided, all results are grouped under one key,
|
||||||
|
``unknown``.
|
||||||
|
|
||||||
|
The corresponding changes to ``POST /reshaper`` are included.
|
||||||
|
@@ -190,3 +190,18 @@ POST_ALLOCATIONS_V1_34 = copy.deepcopy(POST_ALLOCATIONS_V1_28)
|
|||||||
POST_ALLOCATIONS_V1_34["patternProperties"] = {
|
POST_ALLOCATIONS_V1_34["patternProperties"] = {
|
||||||
common.UUID_PATTERN: ALLOCATION_SCHEMA_V1_34
|
common.UUID_PATTERN: ALLOCATION_SCHEMA_V1_34
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A required consumer type was added to the allocations dicts in this
|
||||||
|
# version of PUT /allocations/{consumer_uuid} and POST /allocations.
|
||||||
|
ALLOCATION_SCHEMA_V1_38 = copy.deepcopy(ALLOCATION_SCHEMA_V1_34)
|
||||||
|
ALLOCATION_SCHEMA_V1_38['properties']['consumer_type'] = {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": common.CONSUMER_TYPE_PATTERN,
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 255,
|
||||||
|
}
|
||||||
|
ALLOCATION_SCHEMA_V1_38['required'].append("consumer_type")
|
||||||
|
POST_ALLOCATIONS_V1_38 = copy.deepcopy(POST_ALLOCATIONS_V1_34)
|
||||||
|
POST_ALLOCATIONS_V1_38["patternProperties"] = {
|
||||||
|
common.UUID_PATTERN: ALLOCATION_SCHEMA_V1_38
|
||||||
|
}
|
||||||
|
@@ -20,6 +20,8 @@ RC_PATTERN = _RC_TRAIT_PATTERN
|
|||||||
_CUSTOM_RC_TRAIT_PATTERN = "^CUSTOM_%s+$" % _RC_TRAIT_CHAR
|
_CUSTOM_RC_TRAIT_PATTERN = "^CUSTOM_%s+$" % _RC_TRAIT_CHAR
|
||||||
CUSTOM_RC_PATTERN = _CUSTOM_RC_TRAIT_PATTERN
|
CUSTOM_RC_PATTERN = _CUSTOM_RC_TRAIT_PATTERN
|
||||||
CUSTOM_TRAIT_PATTERN = _CUSTOM_RC_TRAIT_PATTERN
|
CUSTOM_TRAIT_PATTERN = _CUSTOM_RC_TRAIT_PATTERN
|
||||||
|
CONSUMER_TYPE_PATTERN = _RC_TRAIT_PATTERN
|
||||||
|
CONSUMER_TYPE_GET_PATTERN = "%s|^all|^unknown$" % CONSUMER_TYPE_PATTERN
|
||||||
|
|
||||||
# The suffix used with request groups. Prior to 1.33, the group were numbered.
|
# The suffix used with request groups. Prior to 1.33, the group were numbered.
|
||||||
# With 1.33 they become alphanumeric, '_', and '-' with a length limit of 64.
|
# With 1.33 they become alphanumeric, '_', and '-' with a length limit of 64.
|
||||||
|
@@ -50,3 +50,8 @@ POST_RESHAPER_SCHEMA_V1_34 = copy.deepcopy(POST_RESHAPER_SCHEMA)
|
|||||||
ALLOCATIONS_V1_34 = copy.deepcopy(allocation.POST_ALLOCATIONS_V1_34)
|
ALLOCATIONS_V1_34 = copy.deepcopy(allocation.POST_ALLOCATIONS_V1_34)
|
||||||
ALLOCATIONS_V1_34['minProperties'] = 0
|
ALLOCATIONS_V1_34['minProperties'] = 0
|
||||||
POST_RESHAPER_SCHEMA_V1_34['properties']['allocations'] = ALLOCATIONS_V1_34
|
POST_RESHAPER_SCHEMA_V1_34['properties']['allocations'] = ALLOCATIONS_V1_34
|
||||||
|
|
||||||
|
POST_RESHAPER_SCHEMA_V1_38 = copy.deepcopy(POST_RESHAPER_SCHEMA_V1_34)
|
||||||
|
ALLOCATIONS_V1_38 = copy.deepcopy(allocation.POST_ALLOCATIONS_V1_38)
|
||||||
|
ALLOCATIONS_V1_38['minProperties'] = 0
|
||||||
|
POST_RESHAPER_SCHEMA_V1_38['properties']['allocations'] = ALLOCATIONS_V1_38
|
||||||
|
@@ -11,6 +11,11 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
"""Placement API schemas for usage information."""
|
"""Placement API schemas for usage information."""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from placement.schemas import common
|
||||||
|
|
||||||
|
|
||||||
# Represents the allowed query string parameters to GET /usages
|
# Represents the allowed query string parameters to GET /usages
|
||||||
GET_USAGES_SCHEMA_1_9 = {
|
GET_USAGES_SCHEMA_1_9 = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -31,3 +36,15 @@ GET_USAGES_SCHEMA_1_9 = {
|
|||||||
],
|
],
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# An optional consumer type was added to the usage dicts in this
|
||||||
|
# version of GET /usages.
|
||||||
|
|
||||||
|
GET_USAGES_SCHEMA_V1_38 = copy.deepcopy(GET_USAGES_SCHEMA_1_9)
|
||||||
|
GET_USAGES_SCHEMA_V1_38['properties']['consumer_type'] = {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": common.CONSUMER_TYPE_GET_PATTERN,
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 255,
|
||||||
|
}
|
||||||
|
@@ -11,14 +11,18 @@ vars:
|
|||||||
x-roles: admin,member,reader
|
x-roles: admin,member,reader
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.36 here because 1.37 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.36
|
||||||
openstack-system-scope: all
|
openstack-system-scope: all
|
||||||
- &system_reader_headers
|
- &system_reader_headers
|
||||||
x-auth-token: user
|
x-auth-token: user
|
||||||
x-roles: reader
|
x-roles: reader
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.36 here because 1.37 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.36
|
||||||
openstack-system-scope: all
|
openstack-system-scope: all
|
||||||
- &project_admin_headers
|
- &project_admin_headers
|
||||||
x-auth-token: user
|
x-auth-token: user
|
||||||
@@ -26,21 +30,27 @@ vars:
|
|||||||
x-project-id: *project_id
|
x-project-id: *project_id
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.36 here because 1.37 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.36
|
||||||
- &project_member_headers
|
- &project_member_headers
|
||||||
x-auth-token: user
|
x-auth-token: user
|
||||||
x-roles: member,reader
|
x-roles: member,reader
|
||||||
x-project-id: *project_id
|
x-project-id: *project_id
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.36 here because 1.37 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.36
|
||||||
- &project_reader_headers
|
- &project_reader_headers
|
||||||
x-auth-token: user
|
x-auth-token: user
|
||||||
x-roles: reader
|
x-roles: reader
|
||||||
x-project-id: *project_id
|
x-project-id: *project_id
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.36 here because 1.37 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.36
|
||||||
- &agg_1 f918801a-5e54-4bee-9095-09a9d0c786b8
|
- &agg_1 f918801a-5e54-4bee-9095-09a9d0c786b8
|
||||||
- &agg_2 a893eb5c-e2a0-4251-ab26-f71d3b0cfc0b
|
- &agg_2 a893eb5c-e2a0-4251-ab26-f71d3b0cfc0b
|
||||||
|
|
||||||
|
@@ -10,7 +10,9 @@ defaults:
|
|||||||
x-auth-token: user
|
x-auth-token: user
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.37 here because 1.38 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.37
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
|
|
||||||
|
@@ -11,14 +11,18 @@ vars:
|
|||||||
x-roles: admin,member,reader
|
x-roles: admin,member,reader
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.37 here because 1.38 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.37
|
||||||
openstack-system-scope: all
|
openstack-system-scope: all
|
||||||
- &system_reader_headers
|
- &system_reader_headers
|
||||||
x-auth-token: user
|
x-auth-token: user
|
||||||
x-roles: reader
|
x-roles: reader
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.37 here because 1.38 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.37
|
||||||
openstack-system-scope: all
|
openstack-system-scope: all
|
||||||
- &project_admin_headers
|
- &project_admin_headers
|
||||||
x-auth-token: user
|
x-auth-token: user
|
||||||
@@ -26,21 +30,27 @@ vars:
|
|||||||
x-project-id: *project_id
|
x-project-id: *project_id
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.37 here because 1.38 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.37
|
||||||
- &project_member_headers
|
- &project_member_headers
|
||||||
x-auth-token: user
|
x-auth-token: user
|
||||||
x-roles: member,reader
|
x-roles: member,reader
|
||||||
x-project-id: *project_id
|
x-project-id: *project_id
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.37 here because 1.38 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.37
|
||||||
- &project_reader_headers
|
- &project_reader_headers
|
||||||
x-auth-token: user
|
x-auth-token: user
|
||||||
x-roles: reader
|
x-roles: reader
|
||||||
x-project-id: *project_id
|
x-project-id: *project_id
|
||||||
accept: application/json
|
accept: application/json
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
openstack-api-version: placement latest
|
# We need 1.37 here because 1.38 required consumer_type which these
|
||||||
|
# allocations do not have.
|
||||||
|
openstack-api-version: placement 1.37
|
||||||
- &agg_1 f918801a-5e54-4bee-9095-09a9d0c786b8
|
- &agg_1 f918801a-5e54-4bee-9095-09a9d0c786b8
|
||||||
- &agg_2 a893eb5c-e2a0-4251-ab26-f71d3b0cfc0b
|
- &agg_2 a893eb5c-e2a0-4251-ab26-f71d3b0cfc0b
|
||||||
|
|
||||||
|
265
placement/tests/functional/gabbits/consumer-types-1.38.yaml
Normal file
265
placement/tests/functional/gabbits/consumer-types-1.38.yaml
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# Test consumer types work as designed.
|
||||||
|
fixtures:
|
||||||
|
- AllocationFixture
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
request_headers:
|
||||||
|
x-auth-token: admin
|
||||||
|
accept: application/json
|
||||||
|
content-type: application/json
|
||||||
|
openstack-api-version: placement 1.38
|
||||||
|
|
||||||
|
tests:
|
||||||
|
|
||||||
|
- name: 400 on no consumer type post
|
||||||
|
POST: /allocations
|
||||||
|
data:
|
||||||
|
f5a91a0a-e111-4a9c-8a33-7b320ae1e52a:
|
||||||
|
consumer_generation: null
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- "'consumer_type' is a required property"
|
||||||
|
|
||||||
|
- name: 400 on no consumer type put
|
||||||
|
PUT: /allocations/f5a91a0a-e111-4a9c-8a33-7b320ae1e52a
|
||||||
|
data:
|
||||||
|
consumer_generation: null
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- "'consumer_type' is a required property"
|
||||||
|
|
||||||
|
- name: consumer type post
|
||||||
|
POST: /allocations
|
||||||
|
data:
|
||||||
|
f5a91a0a-e111-4a9c-8a33-7b320ae1e52a:
|
||||||
|
consumer_type: INSTANCE
|
||||||
|
consumer_generation: null
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
status: 204
|
||||||
|
|
||||||
|
- name: consumer type put
|
||||||
|
PUT: /allocations/f5a91a0a-e111-4a9c-8a33-7b320ae1e52a
|
||||||
|
data:
|
||||||
|
consumer_generation: 1
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
consumer_type: PONY
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
status: 204
|
||||||
|
|
||||||
|
- name: consumer put without type
|
||||||
|
PUT: /allocations/4fa4553e-e739-4f0b-a758-2fa79fda2ee0
|
||||||
|
request_headers:
|
||||||
|
openstack-api-version: placement 1.36
|
||||||
|
data:
|
||||||
|
consumer_generation: null
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
status: 204
|
||||||
|
|
||||||
|
- name: reset to new type
|
||||||
|
PUT: /allocations/4fa4553e-e739-4f0b-a758-2fa79fda2ee0
|
||||||
|
data:
|
||||||
|
consumer_generation: 1
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
consumer_type: INSTANCE
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
status: 204
|
||||||
|
|
||||||
|
- name: malformed consumer type put
|
||||||
|
PUT: /allocations/4fa4553e-e739-4f0b-a758-2fa79fda2ee0
|
||||||
|
data:
|
||||||
|
consumer_generation: 1
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
consumer_type: instance
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- "'instance' does not match '^[A-Z0-9_]+$'"
|
||||||
|
|
||||||
|
- name: malformed consumer type post
|
||||||
|
POST: /allocations
|
||||||
|
data:
|
||||||
|
4fa4553e-e739-4f0b-a758-2fa79fda2ee0:
|
||||||
|
consumer_generation: 1
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
consumer_type: instance
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- "'instance' does not match '^[A-Z0-9_]+$'"
|
||||||
|
|
||||||
|
# check usages, some allocations are pre-provided by the fixture
|
||||||
|
- name: usages include consumer_type
|
||||||
|
GET: /usages?project_id=$ENVIRON['PROJECT_ID']
|
||||||
|
response_json_paths:
|
||||||
|
$.usages.PONY:
|
||||||
|
consumer_count: 1
|
||||||
|
DISK_GB: 10
|
||||||
|
$.usages.INSTANCE:
|
||||||
|
consumer_count: 1
|
||||||
|
DISK_GB: 10
|
||||||
|
$.usages.unknown:
|
||||||
|
consumer_count: 3
|
||||||
|
DISK_GB: 1020
|
||||||
|
VCPU: 7
|
||||||
|
|
||||||
|
- name: limit usages by consumer_type
|
||||||
|
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&consumer_type=PONY
|
||||||
|
response_json_paths:
|
||||||
|
$.usages.`len`: 1
|
||||||
|
$.usages.PONY:
|
||||||
|
consumer_count: 1
|
||||||
|
DISK_GB: 10
|
||||||
|
|
||||||
|
- name: limit usages bad consumer_type
|
||||||
|
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&consumer_type=COW
|
||||||
|
response_json_paths:
|
||||||
|
$.usages.`len`: 0
|
||||||
|
|
||||||
|
- name: limit usages by all
|
||||||
|
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&consumer_type=all
|
||||||
|
response_json_paths:
|
||||||
|
$.usages.`len`: 1
|
||||||
|
$.usages.all:
|
||||||
|
consumer_count: 5
|
||||||
|
DISK_GB: 1040
|
||||||
|
VCPU: 7
|
||||||
|
|
||||||
|
- name: ALL is not all
|
||||||
|
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&consumer_type=ALL
|
||||||
|
response_json_paths:
|
||||||
|
$.usages.`len`: 0
|
||||||
|
|
||||||
|
- name: limit usages by unknown
|
||||||
|
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&consumer_type=unknown
|
||||||
|
response_json_paths:
|
||||||
|
$.usages.`len`: 1
|
||||||
|
$.usages.unknown:
|
||||||
|
consumer_count: 3
|
||||||
|
DISK_GB: 1020
|
||||||
|
VCPU: 7
|
||||||
|
|
||||||
|
- name: UNKNOWN is not unknown
|
||||||
|
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&consumer_type=UNKNOWN
|
||||||
|
response_json_paths:
|
||||||
|
$.usages.`len`: 0
|
||||||
|
|
||||||
|
- name: reshaper accepts consumer type
|
||||||
|
POST: /reshaper
|
||||||
|
data:
|
||||||
|
inventories:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
# It's 9 because of the previous work
|
||||||
|
resource_provider_generation: 9
|
||||||
|
inventories:
|
||||||
|
DISK_GB:
|
||||||
|
total: 2048
|
||||||
|
VCPU:
|
||||||
|
total: 97
|
||||||
|
allocations:
|
||||||
|
4b01cd5a-9e12-46d7-9b2a-5bc0f6040a40:
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
consumer_generation: null
|
||||||
|
consumer_type: RESHAPED
|
||||||
|
status: 204
|
||||||
|
|
||||||
|
- name: confirm reshaped allocations
|
||||||
|
GET: /allocations/4b01cd5a-9e12-46d7-9b2a-5bc0f6040a40
|
||||||
|
response_json_paths:
|
||||||
|
$.consumer_type: RESHAPED
|
||||||
|
|
||||||
|
- name: reshaper requires consumer type
|
||||||
|
POST: /reshaper
|
||||||
|
data:
|
||||||
|
inventories:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
# It's 9 because of the previous work
|
||||||
|
resource_provider_generation: 9
|
||||||
|
inventories:
|
||||||
|
DISK_GB:
|
||||||
|
total: 2048
|
||||||
|
VCPU:
|
||||||
|
total: 97
|
||||||
|
allocations:
|
||||||
|
4b01cd5a-9e12-46d7-9b2a-5bc0f6040a40:
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
consumer_generation: 1
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- "'consumer_type' is a required"
|
||||||
|
|
||||||
|
- name: reshaper refuses consumer type earlier microversion
|
||||||
|
request_headers:
|
||||||
|
openstack-api-version: placement 1.36
|
||||||
|
POST: /reshaper
|
||||||
|
data:
|
||||||
|
inventories:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
# It's 9 because of the previous work
|
||||||
|
resource_provider_generation: 9
|
||||||
|
inventories:
|
||||||
|
DISK_GB:
|
||||||
|
total: 2048
|
||||||
|
VCPU:
|
||||||
|
total: 97
|
||||||
|
allocations:
|
||||||
|
4b01cd5a-9e12-46d7-9b2a-5bc0f6040a40:
|
||||||
|
allocations:
|
||||||
|
$ENVIRON['RP_UUID']:
|
||||||
|
resources:
|
||||||
|
DISK_GB: 10
|
||||||
|
project_id: $ENVIRON['PROJECT_ID']
|
||||||
|
user_id: $ENVIRON['USER_ID']
|
||||||
|
consumer_generation: 1
|
||||||
|
consumer_type: RESHAPED
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- "JSON does not validate: Additional properties are not allowed"
|
||||||
|
- "'consumer_type' was unexpected"
|
@@ -41,13 +41,13 @@ tests:
|
|||||||
response_json_paths:
|
response_json_paths:
|
||||||
$.errors[0].title: Not Acceptable
|
$.errors[0].title: Not Acceptable
|
||||||
|
|
||||||
- name: latest microversion is 1.37
|
- name: latest microversion is 1.38
|
||||||
GET: /
|
GET: /
|
||||||
request_headers:
|
request_headers:
|
||||||
openstack-api-version: placement latest
|
openstack-api-version: placement latest
|
||||||
response_headers:
|
response_headers:
|
||||||
vary: /openstack-api-version/
|
vary: /openstack-api-version/
|
||||||
openstack-api-version: placement 1.37
|
openstack-api-version: placement 1.38
|
||||||
|
|
||||||
- name: other accept header bad version
|
- name: other accept header bad version
|
||||||
GET: /
|
GET: /
|
||||||
|
@@ -25,6 +25,7 @@ from placement import exception
|
|||||||
from placement.handlers import util
|
from placement.handlers import util
|
||||||
from placement import microversion
|
from placement import microversion
|
||||||
from placement.objects import consumer as consumer_obj
|
from placement.objects import consumer as consumer_obj
|
||||||
|
from placement.objects import consumer_type as consumer_type_obj
|
||||||
from placement.objects import project as project_obj
|
from placement.objects import project as project_obj
|
||||||
from placement.objects import user as user_obj
|
from placement.objects import user as user_obj
|
||||||
from placement.tests.unit import base
|
from placement.tests.unit import base
|
||||||
@@ -54,6 +55,12 @@ class TestEnsureConsumer(base.ContextTestCase):
|
|||||||
self.mock_consumer_create = self.useFixture(fixtures.MockPatch(
|
self.mock_consumer_create = self.useFixture(fixtures.MockPatch(
|
||||||
'placement.objects.consumer.'
|
'placement.objects.consumer.'
|
||||||
'Consumer.create')).mock
|
'Consumer.create')).mock
|
||||||
|
self.mock_consumer_update = self.useFixture(fixtures.MockPatch(
|
||||||
|
'placement.objects.consumer.'
|
||||||
|
'Consumer.update')).mock
|
||||||
|
self.mock_consumer_type_get = self.useFixture(fixtures.MockPatch(
|
||||||
|
'placement.objects.consumer_type.'
|
||||||
|
'ConsumerType.get_by_name')).mock
|
||||||
self.ctx = context.RequestContext(user_id='fake', project_id='fake')
|
self.ctx = context.RequestContext(user_id='fake', project_id='fake')
|
||||||
self.ctx.config = self.conf
|
self.ctx.config = self.conf
|
||||||
self.consumer_id = uuidsentinel.consumer
|
self.consumer_id = uuidsentinel.consumer
|
||||||
@@ -71,6 +78,12 @@ class TestEnsureConsumer(base.ContextTestCase):
|
|||||||
mv_parsed.min_version = microversion_parse.parse_version_string(
|
mv_parsed.min_version = microversion_parse.parse_version_string(
|
||||||
microversion.min_version_string())
|
microversion.min_version_string())
|
||||||
self.after_version = mv_parsed
|
self.after_version = mv_parsed
|
||||||
|
mv_parsed = microversion_parse.Version(1, 38)
|
||||||
|
mv_parsed.max_version = microversion_parse.parse_version_string(
|
||||||
|
microversion.max_version_string())
|
||||||
|
mv_parsed.min_version = microversion_parse.parse_version_string(
|
||||||
|
microversion.min_version_string())
|
||||||
|
self.cons_type_req_version = mv_parsed
|
||||||
|
|
||||||
def test_no_existing_project_user_consumer_before_gen_success(self):
|
def test_no_existing_project_user_consumer_before_gen_success(self):
|
||||||
"""Tests that we don't require a consumer_generation=None before the
|
"""Tests that we don't require a consumer_generation=None before the
|
||||||
@@ -83,7 +96,7 @@ class TestEnsureConsumer(base.ContextTestCase):
|
|||||||
consumer_gen = 1 # should be ignored
|
consumer_gen = 1 # should be ignored
|
||||||
util.ensure_consumer(
|
util.ensure_consumer(
|
||||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||||
consumer_gen, self.before_version)
|
consumer_gen, 'TYPE', self.before_version)
|
||||||
|
|
||||||
self.mock_project_get.assert_called_once_with(
|
self.mock_project_get.assert_called_once_with(
|
||||||
self.ctx, self.project_id)
|
self.ctx, self.project_id)
|
||||||
@@ -106,7 +119,7 @@ class TestEnsureConsumer(base.ContextTestCase):
|
|||||||
consumer_gen = None # should NOT be ignored (and None is expected)
|
consumer_gen = None # should NOT be ignored (and None is expected)
|
||||||
util.ensure_consumer(
|
util.ensure_consumer(
|
||||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||||
consumer_gen, self.after_version)
|
consumer_gen, 'TYPE', self.after_version)
|
||||||
|
|
||||||
self.mock_project_get.assert_called_once_with(
|
self.mock_project_get.assert_called_once_with(
|
||||||
self.ctx, self.project_id)
|
self.ctx, self.project_id)
|
||||||
@@ -131,7 +144,7 @@ class TestEnsureConsumer(base.ContextTestCase):
|
|||||||
webob.exc.HTTPConflict,
|
webob.exc.HTTPConflict,
|
||||||
util.ensure_consumer,
|
util.ensure_consumer,
|
||||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||||
consumer_gen, self.after_version)
|
consumer_gen, 'TYPE', self.after_version)
|
||||||
|
|
||||||
def test_no_existing_project_user_consumer_use_incomplete(self):
|
def test_no_existing_project_user_consumer_use_incomplete(self):
|
||||||
"""Verify that if the project_id arg is None, that we fall back to the
|
"""Verify that if the project_id arg is None, that we fall back to the
|
||||||
@@ -144,7 +157,7 @@ class TestEnsureConsumer(base.ContextTestCase):
|
|||||||
consumer_gen = None # should NOT be ignored (and None is expected)
|
consumer_gen = None # should NOT be ignored (and None is expected)
|
||||||
util.ensure_consumer(
|
util.ensure_consumer(
|
||||||
self.ctx, self.consumer_id, None, None,
|
self.ctx, self.consumer_id, None, None,
|
||||||
consumer_gen, self.before_version)
|
consumer_gen, 'TYPE', self.before_version)
|
||||||
|
|
||||||
self.mock_project_get.assert_called_once_with(
|
self.mock_project_get.assert_called_once_with(
|
||||||
self.ctx, self.conf.placement.incomplete_consumer_project_id)
|
self.ctx, self.conf.placement.incomplete_consumer_project_id)
|
||||||
@@ -170,7 +183,7 @@ class TestEnsureConsumer(base.ContextTestCase):
|
|||||||
consumer_gen = None # should be ignored
|
consumer_gen = None # should be ignored
|
||||||
util.ensure_consumer(
|
util.ensure_consumer(
|
||||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||||
consumer_gen, self.before_version)
|
consumer_gen, 'TYPE', self.before_version)
|
||||||
|
|
||||||
self.mock_project_create.assert_not_called()
|
self.mock_project_create.assert_not_called()
|
||||||
self.mock_user_create.assert_not_called()
|
self.mock_user_create.assert_not_called()
|
||||||
@@ -192,7 +205,7 @@ class TestEnsureConsumer(base.ContextTestCase):
|
|||||||
consumer_gen = 2 # should NOT be ignored (and 2 is expected)
|
consumer_gen = 2 # should NOT be ignored (and 2 is expected)
|
||||||
util.ensure_consumer(
|
util.ensure_consumer(
|
||||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||||
consumer_gen, self.after_version)
|
consumer_gen, 'TYPE', self.after_version)
|
||||||
|
|
||||||
self.mock_project_create.assert_not_called()
|
self.mock_project_create.assert_not_called()
|
||||||
self.mock_user_create.assert_not_called()
|
self.mock_user_create.assert_not_called()
|
||||||
@@ -217,4 +230,62 @@ class TestEnsureConsumer(base.ContextTestCase):
|
|||||||
webob.exc.HTTPConflict,
|
webob.exc.HTTPConflict,
|
||||||
util.ensure_consumer,
|
util.ensure_consumer,
|
||||||
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||||
consumer_gen, self.after_version)
|
consumer_gen, 'TYPE', self.after_version)
|
||||||
|
|
||||||
|
def test_existing_consumer_different_consumer_type_supplied(self):
|
||||||
|
"""Tests that we update a consumer's type ID if the one supplied by the
|
||||||
|
user is different than the one in the existing record.
|
||||||
|
"""
|
||||||
|
proj = project_obj.Project(self.ctx, id=1, external_id=self.project_id)
|
||||||
|
self.mock_project_get.return_value = proj
|
||||||
|
user = user_obj.User(self.ctx, id=1, external_id=self.user_id)
|
||||||
|
self.mock_user_get.return_value = user
|
||||||
|
# Consumer currently has type ID = 1
|
||||||
|
consumer = consumer_obj.Consumer(
|
||||||
|
self.ctx, id=1, project=proj, user=user, generation=1,
|
||||||
|
consumer_type_id=1)
|
||||||
|
self.mock_consumer_get.return_value = consumer
|
||||||
|
# Supplied consumer type ID = 2
|
||||||
|
consumer_type = consumer_type_obj.ConsumerType(
|
||||||
|
self.ctx, id=2, name='TYPE')
|
||||||
|
self.mock_consumer_type_get.return_value = consumer_type
|
||||||
|
|
||||||
|
consumer_gen = 1
|
||||||
|
util.ensure_consumer(
|
||||||
|
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||||
|
consumer_gen, 'TYPE', self.cons_type_req_version)
|
||||||
|
# Expect 1 call to update() to update to the supplied consumer type ID
|
||||||
|
self.mock_consumer_update.assert_called_once_with()
|
||||||
|
# Consumer should have the new consumer type ID = 2
|
||||||
|
self.assertEqual(2, consumer.consumer_type_id)
|
||||||
|
|
||||||
|
def test_consumer_create_exists_different_consumer_type_supplied(self):
|
||||||
|
"""Tests that we update a consumer's type ID if the one supplied by a
|
||||||
|
racing request is different than the one in the existing (recently
|
||||||
|
created) record.
|
||||||
|
"""
|
||||||
|
proj = project_obj.Project(self.ctx, id=1, external_id=self.project_id)
|
||||||
|
self.mock_project_get.return_value = proj
|
||||||
|
user = user_obj.User(self.ctx, id=1, external_id=self.user_id)
|
||||||
|
self.mock_user_get.return_value = user
|
||||||
|
# Request A recently created consumer has type ID = 1
|
||||||
|
consumer = consumer_obj.Consumer(
|
||||||
|
self.ctx, id=1, project=proj, user=user, generation=1,
|
||||||
|
consumer_type_id=1, uuid=uuidsentinel.consumer)
|
||||||
|
self.mock_consumer_get.return_value = consumer
|
||||||
|
# Request B supplied consumer type ID = 2
|
||||||
|
consumer_type = consumer_type_obj.ConsumerType(
|
||||||
|
self.ctx, id=2, name='TYPE')
|
||||||
|
self.mock_consumer_type_get.return_value = consumer_type
|
||||||
|
# Request B will encounter ConsumerExists as Request A just created it
|
||||||
|
self.mock_consumer_create.side_effect = (
|
||||||
|
exception.ConsumerExists(uuid=uuidsentinel.consumer))
|
||||||
|
|
||||||
|
consumer_gen = 1
|
||||||
|
util.ensure_consumer(
|
||||||
|
self.ctx, self.consumer_id, self.project_id, self.user_id,
|
||||||
|
consumer_gen, 'TYPE', self.cons_type_req_version)
|
||||||
|
# Expect 1 call to update() to update to the supplied consumer type ID
|
||||||
|
self.mock_consumer_update.assert_called_once_with()
|
||||||
|
# Consumer should have the new consumer type ID = 2
|
||||||
|
self.assertEqual(2, consumer.consumer_type_id)
|
||||||
|
18
releasenotes/notes/consumer_type-857b812aef10381e.yaml
Normal file
18
releasenotes/notes/consumer_type-857b812aef10381e.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Microversion 1.38 adds support for a ``consumer_type`` (required) key in
|
||||||
|
the request body of ``POST /allocations``, ``PUT
|
||||||
|
/allocations/{consumer_uuid}`` and in the response of ``GET
|
||||||
|
/allocations/{consumer_uuid}``. ``GET /usages`` requests gain a
|
||||||
|
``consumer_type`` key as an optional query parameter to filter usages based
|
||||||
|
on consumer_types. ``GET /usages`` response will group results based on the
|
||||||
|
consumer type and will include a new ``consumer_count`` key per type
|
||||||
|
irrespective of whether the ``consumer_type`` was specified in the request.
|
||||||
|
If an ``all`` ``consumer_type`` key is provided, all results are grouped
|
||||||
|
under one key, ``all``. Older allocations which were not created with a
|
||||||
|
consumer type are considered to have an ``unknown`` ``consumer_type``. If
|
||||||
|
an ``unknown`` ``consumer_type`` key is provided, all results are grouped
|
||||||
|
under one key, ``unknown``.
|
||||||
|
|
||||||
|
The corresponding changes to ``POST /reshaper`` are included.
|
Reference in New Issue
Block a user