Merge "placement: Add GET /usages to placement API"

This commit is contained in:
Jenkins 2017-06-24 01:30:23 +00:00 committed by Gerrit Code Review
commit 8e41216a8d
10 changed files with 196 additions and 23 deletions

View File

@ -117,6 +117,9 @@ ROUTE_DECLARATIONS = {
'PUT': trait.update_traits_for_resource_provider, 'PUT': trait.update_traits_for_resource_provider,
'DELETE': trait.delete_traits_for_resource_provider 'DELETE': trait.delete_traits_for_resource_provider
}, },
'/usages': {
'GET': usage.get_total_usages,
},
} }

View File

@ -13,7 +13,6 @@
import copy import copy
import jsonschema
from oslo_db import exception as db_exc from oslo_db import exception as db_exc
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
@ -208,13 +207,8 @@ def list_resource_providers(req):
schema = GET_RPS_SCHEMA_1_3 schema = GET_RPS_SCHEMA_1_3
if want_version >= (1, 4): if want_version >= (1, 4):
schema = GET_RPS_SCHEMA_1_4 schema = GET_RPS_SCHEMA_1_4
try:
jsonschema.validate(dict(req.GET), schema, util.validate_query_params(req, schema)
format_checker=jsonschema.FormatChecker())
except jsonschema.ValidationError as exc:
raise webob.exc.HTTPBadRequest(
_('Invalid query string parameters: %(exc)s') %
{'exc': exc})
filters = {} filters = {}
for attr in ['uuid', 'name', 'member_of']: for attr in ['uuid', 'name', 'member_of']:

View File

@ -15,6 +15,7 @@ from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
import webob import webob
from nova.api.openstack.placement import microversion
from nova.api.openstack.placement import util from nova.api.openstack.placement import util
from nova.api.openstack.placement import wsgi_wrapper from nova.api.openstack.placement import wsgi_wrapper
from nova import exception from nova import exception
@ -22,6 +23,28 @@ from nova.i18n import _
from nova import objects from nova import objects
# Represents the allowed query string parameters to GET /usages
GET_USAGES_SCHEMA_1_9 = {
"type": "object",
"properties": {
"project_id": {
"type": "string",
"minLength": 1,
"maxLength": 255,
},
"user_id": {
"type": "string",
"minLength": 1,
"maxLength": 255,
},
},
"required": [
"project_id"
],
"additionalProperties": False,
}
def _serialize_usages(resource_provider, usage): def _serialize_usages(resource_provider, usage):
usage_dict = {resource.resource_class: resource.usage usage_dict = {resource.resource_class: resource.usage
for resource in usage} for resource in usage}
@ -63,3 +86,33 @@ def list_usages(req):
_serialize_usages(resource_provider, usage))) _serialize_usages(resource_provider, usage)))
req.response.content_type = 'application/json' req.response.content_type = 'application/json'
return req.response return req.response
@wsgi_wrapper.PlacementWsgify
@microversion.version_handler('1.9')
@util.check_accept('application/json')
def get_total_usages(req):
"""GET the sum of usages for a project or a project/user.
On success return a 200 and an application/json body representing the
sum/total of usages.
Return 404 Not Found if the wanted microversion does not match.
"""
context = req.environ['placement.context']
schema = GET_USAGES_SCHEMA_1_9
util.validate_query_params(req, schema)
project_id = req.GET.get('project_id')
user_id = req.GET.get('user_id')
usages = objects.UsageList.get_all_by_project_user(context, project_id,
user_id=user_id)
response = req.response
usages_dict = {'usages': {resource.resource_class: resource.usage
for resource in usages}}
response.body = encodeutils.to_utf8(jsonutils.dumps(usages_dict))
req.response.content_type = 'application/json'
return req.response

View File

@ -46,6 +46,7 @@ VERSIONS = [
'1.7', # PUT /resource_classes/{name} is bodiless create or update '1.7', # PUT /resource_classes/{name} is bodiless create or update
'1.8', # Adds 'project_id' and 'user_id' required request parameters to '1.8', # Adds 'project_id' and 'user_id' required request parameters to
# PUT /allocations # PUT /allocations
'1.9', # Adds GET /usages
] ]

View File

@ -170,6 +170,16 @@ def trait_url(environ, trait):
return '%s/traits/%s' % (prefix, trait.name) return '%s/traits/%s' % (prefix, trait.name)
def validate_query_params(req, schema):
try:
jsonschema.validate(dict(req.GET), schema,
format_checker=jsonschema.FormatChecker())
except jsonschema.ValidationError as exc:
raise webob.exc.HTTPBadRequest(
_('Invalid query string parameters: %(exc)s') %
{'exc': exc})
def wsgi_path_item(environ, name): def wsgi_path_item(environ, name):
"""Extract the value of a named field in a URL. """Extract the value of a named field in a URL.

View File

@ -95,6 +95,13 @@ class AllocationFixture(APIFixture):
def start_fixture(self): def start_fixture(self):
super(AllocationFixture, self).start_fixture() super(AllocationFixture, self).start_fixture()
self.context = context.get_admin_context() self.context = context.get_admin_context()
# For use creating and querying allocations/usages
os.environ['ALT_USER_ID'] = uuidutils.generate_uuid()
project_id = os.environ['PROJECT_ID']
user_id = os.environ['USER_ID']
alt_user_id = os.environ['ALT_USER_ID']
# Stealing from the super # Stealing from the super
rp_name = os.environ['RP_NAME'] rp_name = os.environ['RP_NAME']
rp_uuid = os.environ['RP_UUID'] rp_uuid = os.environ['RP_UUID']
@ -103,6 +110,9 @@ class AllocationFixture(APIFixture):
rp.create() rp.create()
# Create some DISK_GB inventory and allocations. # Create some DISK_GB inventory and allocations.
# Each set of allocations must have the same consumer_id because only
# the first allocation is used for the project/user association.
consumer_id = uuidutils.generate_uuid()
inventory = objects.Inventory( inventory = objects.Inventory(
self.context, resource_provider=rp, self.context, resource_provider=rp,
resource_class='DISK_GB', total=2048, resource_class='DISK_GB', total=2048,
@ -112,36 +122,67 @@ class AllocationFixture(APIFixture):
alloc1 = objects.Allocation( alloc1 = objects.Allocation(
self.context, resource_provider=rp, self.context, resource_provider=rp,
resource_class='DISK_GB', resource_class='DISK_GB',
consumer_id=uuidutils.generate_uuid(), consumer_id=consumer_id,
used=500) used=500)
alloc2 = objects.Allocation( alloc2 = objects.Allocation(
self.context, resource_provider=rp, self.context, resource_provider=rp,
resource_class='DISK_GB', resource_class='DISK_GB',
consumer_id=uuidutils.generate_uuid(), consumer_id=consumer_id,
used=500) used=500)
alloc_list = objects.AllocationList(self.context, alloc_list = objects.AllocationList(
objects=[alloc1, alloc2]) self.context,
objects=[alloc1, alloc2],
project_id=project_id,
user_id=user_id,
)
alloc_list.create_all() alloc_list.create_all()
# Create some VCPU inventory and allocations. # Create some VCPU inventory and allocations.
# Each set of allocations must have the same consumer_id because only
# the first allocation is used for the project/user association.
consumer_id = uuidutils.generate_uuid()
inventory = objects.Inventory( inventory = objects.Inventory(
self.context, resource_provider=rp, self.context, resource_provider=rp,
resource_class='VCPU', total=8, resource_class='VCPU', total=10,
max_unit=4) max_unit=4)
inventory.obj_set_defaults() inventory.obj_set_defaults()
rp.add_inventory(inventory) rp.add_inventory(inventory)
alloc1 = objects.Allocation( alloc1 = objects.Allocation(
self.context, resource_provider=rp, self.context, resource_provider=rp,
resource_class='VCPU', resource_class='VCPU',
consumer_id=uuidutils.generate_uuid(), consumer_id=consumer_id,
used=2) used=2)
alloc2 = objects.Allocation( alloc2 = objects.Allocation(
self.context, resource_provider=rp, self.context, resource_provider=rp,
resource_class='VCPU', resource_class='VCPU',
consumer_id=uuidutils.generate_uuid(), consumer_id=consumer_id,
used=4) used=4)
alloc_list = objects.AllocationList(self.context, alloc_list = objects.AllocationList(
objects=[alloc1, alloc2]) self.context,
objects=[alloc1, alloc2],
project_id=project_id,
user_id=user_id)
alloc_list.create_all()
# Create a couple of allocations for a different user.
# Each set of allocations must have the same consumer_id because only
# the first allocation is used for the project/user association.
consumer_id = uuidutils.generate_uuid()
alloc1 = objects.Allocation(
self.context, resource_provider=rp,
resource_class='DISK_GB',
consumer_id=consumer_id,
used=20)
alloc2 = objects.Allocation(
self.context, resource_provider=rp,
resource_class='VCPU',
consumer_id=consumer_id,
used=1)
alloc_list = objects.AllocationList(
self.context,
objects=[alloc1, alloc2],
project_id=project_id,
user_id=alt_user_id)
alloc_list.create_all() alloc_list.create_all()
# The ALT_RP_XXX variables are for a resource provider that has # The ALT_RP_XXX variables are for a resource provider that has

View File

@ -39,13 +39,13 @@ tests:
response_json_paths: response_json_paths:
$.errors[0].title: Not Acceptable $.errors[0].title: Not Acceptable
- name: latest microversion is 1.8 - name: latest microversion is 1.9
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.8 openstack-api-version: placement 1.9
- name: other accept header bad version - name: other accept header bad version
GET: / GET: /

View File

@ -37,3 +37,47 @@ tests:
content-type: application/json content-type: application/json
response_json_paths: response_json_paths:
usages: {} usages: {}
- name: get total usages earlier version
GET: /usages?project_id=$ENVIRON['PROJECT_ID']
request_headers:
openstack-api-version: placement 1.8
status: 404
- name: get total usages no project or user
GET: /usages
request_headers:
openstack-api-version: placement 1.9
status: 400
- name: get total usages project_id less than min length
GET: /usages?project_id=
request_headers:
openstack-api-version: placement 1.9
status: 400
response_strings:
- "Failed validating 'minLength'"
- name: get total usages user_id less than min length
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&user_id=
request_headers:
openstack-api-version: placement 1.9
status: 400
response_strings:
- "Failed validating 'minLength'"
- name: get total usages project_id exceeds max length
GET: /usages?project_id=78725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b1
request_headers:
openstack-api-version: placement 1.9
status: 400
response_strings:
- "Failed validating 'maxLength'"
- name: get total usages user_id exceeds max length
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&user_id=78725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b178725f09-5c01-4c9e-97a5-98d75e1e32b1
request_headers:
openstack-api-version: placement 1.9
status: 400
response_strings:
- "Failed validating 'maxLength'"

View File

@ -21,9 +21,9 @@ tests:
# required but superfluous, is present # required but superfluous, is present
content-type: /application/json/ content-type: /application/json/
response_json_paths: response_json_paths:
$.resource_provider_generation: 4 $.resource_provider_generation: 5
$.usages.DISK_GB: 1000 $.usages.DISK_GB: 1020
$.usages.VCPU: 6 $.usages.VCPU: 7
- name: fail to delete resource provider - name: fail to delete resource provider
DELETE: /resource_providers/$ENVIRON['RP_UUID'] DELETE: /resource_providers/$ENVIRON['RP_UUID']
@ -41,3 +41,30 @@ tests:
content-type: /application/json/ content-type: /application/json/
response_strings: response_strings:
- Unable to delete inventory for resource provider $ENVIRON['RP_UUID'] because the inventory is in use. - Unable to delete inventory for resource provider $ENVIRON['RP_UUID'] because the inventory is in use.
- name: get total usages by project
GET: /usages?project_id=$ENVIRON['PROJECT_ID']
request_headers:
openstack-api-version: placement 1.9
status: 200
response_json_paths:
$.usages.DISK_GB: 1020
$.usages.VCPU: 7
- name: get total usages by project and user
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&user_id=$ENVIRON['USER_ID']
request_headers:
openstack-api-version: placement 1.9
status: 200
response_json_paths:
$.usages.DISK_GB: 1000
$.usages.VCPU: 6
- name: get total usages by project and alt user
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&user_id=$ENVIRON['ALT_USER_ID']
request_headers:
openstack-api-version: placement 1.9
status: 200
response_json_paths:
$.usages.DISK_GB: 20
$.usages.VCPU: 1

View File

@ -74,7 +74,7 @@ class TestMicroversionIntersection(test.NoDBTestCase):
# if you add two different versions of method 'foobar' the # if you add two different versions of method 'foobar' the
# number only goes up by one if no other version foobar yet # number only goes up by one if no other version foobar yet
# exists. This operates as a simple sanity check. # exists. This operates as a simple sanity check.
TOTAL_VERSIONED_METHODS = 13 TOTAL_VERSIONED_METHODS = 14
def test_methods_versioned(self): def test_methods_versioned(self):
methods_data = microversion.VERSIONED_METHODS methods_data = microversion.VERSIONED_METHODS