summaryrefslogtreecommitdiff
path: root/nova
diff options
context:
space:
mode:
authormelanie witt <melwittt@gmail.com>2017-05-31 18:53:34 +0000
committermelanie witt <melwittt@gmail.com>2017-06-15 18:23:37 +0000
commit77224c1feb350245b9fbba1d8b31d6e5904714b6 (patch)
treee81b2a84a203bd09da3820fde8045f74f3a33a3c /nova
parenta909673682cdd8f02ef0ae5e8c6f061640e320ff (diff)
downloadnova-77224c1feb350245b9fbba1d8b31d6e5904714b6.tar.gz
placement: Add GET /usages to placement API
This adds GET /usages as part of a new microversion 1.9 of the placement API. Usages can be queried by project or project/user: GET /usages?project_id=<project id> GET /usages?project_id=<project id>&user_id=<user id> and will be returned as a sum of usages, for example: 200 OK Content-Type: application/json { "usages": { "VCPU": 2, "MEMORY_MB": 1024, "DISK_GB": 50, ... } } A new method UsageList.get_all_by_project_user() has been added for usage queries. Part of blueprint placement-project-user Change-Id: I8b948a4dfe6a50bea053b5dcae8f039229e2e364
Diffstat (limited to 'nova')
-rw-r--r--nova/api/openstack/placement/handler.py3
-rw-r--r--nova/api/openstack/placement/handlers/resource_provider.py10
-rw-r--r--nova/api/openstack/placement/handlers/usage.py53
-rw-r--r--nova/api/openstack/placement/microversion.py1
-rw-r--r--nova/api/openstack/placement/util.py10
-rw-r--r--nova/objects/resource_provider.py28
-rw-r--r--nova/tests/functional/api/openstack/placement/fixtures.py59
-rw-r--r--nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml4
-rw-r--r--nova/tests/functional/api/openstack/placement/gabbits/usage.yaml44
-rw-r--r--nova/tests/functional/api/openstack/placement/gabbits/with-allocations.yaml33
-rw-r--r--nova/tests/functional/db/test_resource_provider.py27
-rw-r--r--nova/tests/unit/api/openstack/placement/test_microversion.py2
-rw-r--r--nova/tests/unit/objects/test_objects.py2
13 files changed, 251 insertions, 25 deletions
diff --git a/nova/api/openstack/placement/handler.py b/nova/api/openstack/placement/handler.py
index fa9ebfa5d5..b13517481d 100644
--- a/nova/api/openstack/placement/handler.py
+++ b/nova/api/openstack/placement/handler.py
@@ -117,6 +117,9 @@ ROUTE_DECLARATIONS = {
'PUT': trait.update_traits_for_resource_provider,
'DELETE': trait.delete_traits_for_resource_provider
},
+ '/usages': {
+ 'GET': usage.get_total_usages,
+ },
}
diff --git a/nova/api/openstack/placement/handlers/resource_provider.py b/nova/api/openstack/placement/handlers/resource_provider.py
index 1eef16f4a6..199d53b8fb 100644
--- a/nova/api/openstack/placement/handlers/resource_provider.py
+++ b/nova/api/openstack/placement/handlers/resource_provider.py
@@ -13,7 +13,6 @@
import copy
-import jsonschema
from oslo_db import exception as db_exc
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
@@ -267,13 +266,8 @@ def list_resource_providers(req):
schema = GET_RPS_SCHEMA_1_3
if want_version >= (1, 4):
schema = GET_RPS_SCHEMA_1_4
- 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})
+
+ util.validate_query_params(req, schema)
filters = {}
for attr in ['uuid', 'name', 'member_of']:
diff --git a/nova/api/openstack/placement/handlers/usage.py b/nova/api/openstack/placement/handlers/usage.py
index d0fea2d358..68c3eb3989 100644
--- a/nova/api/openstack/placement/handlers/usage.py
+++ b/nova/api/openstack/placement/handlers/usage.py
@@ -15,6 +15,7 @@ from oslo_serialization import jsonutils
from oslo_utils import encodeutils
import webob
+from nova.api.openstack.placement import microversion
from nova.api.openstack.placement import util
from nova.api.openstack.placement import wsgi_wrapper
from nova import exception
@@ -22,6 +23,28 @@ from nova.i18n import _
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):
usage_dict = {resource.resource_class: resource.usage
for resource in usage}
@@ -63,3 +86,33 @@ def list_usages(req):
_serialize_usages(resource_provider, usage)))
req.response.content_type = 'application/json'
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
diff --git a/nova/api/openstack/placement/microversion.py b/nova/api/openstack/placement/microversion.py
index 2f7ccaa2c8..8d80e3b02e 100644
--- a/nova/api/openstack/placement/microversion.py
+++ b/nova/api/openstack/placement/microversion.py
@@ -46,6 +46,7 @@ VERSIONS = [
'1.7', # PUT /resource_classes/{name} is bodiless create or update
'1.8', # Adds 'project_id' and 'user_id' required request parameters to
# PUT /allocations
+ '1.9', # Adds GET /usages
]
diff --git a/nova/api/openstack/placement/util.py b/nova/api/openstack/placement/util.py
index d716723094..91df481650 100644
--- a/nova/api/openstack/placement/util.py
+++ b/nova/api/openstack/placement/util.py
@@ -170,6 +170,16 @@ def trait_url(environ, trait):
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):
"""Extract the value of a named field in a URL.
diff --git a/nova/objects/resource_provider.py b/nova/objects/resource_provider.py
index d33d027eae..0317fa2b24 100644
--- a/nova/objects/resource_provider.py
+++ b/nova/objects/resource_provider.py
@@ -1894,7 +1894,8 @@ class Usage(base.NovaObject):
class UsageList(base.ObjectListBase, base.NovaObject):
# Version 1.0: Initial version
# Version 1.1: Turn off remotable
- VERSION = '1.1'
+ # Version 1.2: Add get_all_by_project_user()
+ VERSION = '1.2'
fields = {
'objects': fields.ListOfObjectsField('Usage'),
@@ -1919,11 +1920,36 @@ class UsageList(base.ObjectListBase, base.NovaObject):
for item in query.all()]
return result
+ @staticmethod
+ @db_api.api_context_manager.reader
+ def _get_all_by_project_user(context, project_id, user_id=None):
+ query = (context.session.query(models.Allocation.resource_class_id,
+ func.coalesce(func.sum(models.Allocation.used), 0))
+ .join(models.Consumer,
+ models.Allocation.consumer_id == models.Consumer.uuid)
+ .join(models.Project,
+ models.Consumer.project_id == models.Project.id)
+ .filter(models.Project.external_id == project_id))
+ if user_id:
+ query = query.join(models.User,
+ models.Consumer.user_id == models.User.id)
+ query = query.filter(models.User.external_id == user_id)
+ query = query.group_by(models.Allocation.resource_class_id)
+ result = [dict(resource_class_id=item[0], usage=item[1])
+ for item in query.all()]
+ return result
+
@classmethod
def get_all_by_resource_provider_uuid(cls, context, rp_uuid):
usage_list = cls._get_all_by_resource_provider_uuid(context, rp_uuid)
return base.obj_make_list(context, cls(context), Usage, usage_list)
+ @classmethod
+ def get_all_by_project_user(cls, context, project_id, user_id=None):
+ usage_list = cls._get_all_by_project_user(context, project_id,
+ user_id=user_id)
+ return base.obj_make_list(context, cls(context), Usage, usage_list)
+
def __repr__(self):
strings = [repr(x) for x in self.objects]
return "UsageList[" + ", ".join(strings) + "]"
diff --git a/nova/tests/functional/api/openstack/placement/fixtures.py b/nova/tests/functional/api/openstack/placement/fixtures.py
index 3c31f21b47..3b72723e32 100644
--- a/nova/tests/functional/api/openstack/placement/fixtures.py
+++ b/nova/tests/functional/api/openstack/placement/fixtures.py
@@ -95,6 +95,13 @@ class AllocationFixture(APIFixture):
def start_fixture(self):
super(AllocationFixture, self).start_fixture()
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
rp_name = os.environ['RP_NAME']
rp_uuid = os.environ['RP_UUID']
@@ -103,6 +110,9 @@ class AllocationFixture(APIFixture):
rp.create()
# 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(
self.context, resource_provider=rp,
resource_class='DISK_GB', total=2048,
@@ -112,36 +122,67 @@ class AllocationFixture(APIFixture):
alloc1 = objects.Allocation(
self.context, resource_provider=rp,
resource_class='DISK_GB',
- consumer_id=uuidutils.generate_uuid(),
+ consumer_id=consumer_id,
used=500)
alloc2 = objects.Allocation(
self.context, resource_provider=rp,
resource_class='DISK_GB',
- consumer_id=uuidutils.generate_uuid(),
+ consumer_id=consumer_id,
used=500)
- alloc_list = objects.AllocationList(self.context,
- objects=[alloc1, alloc2])
+ alloc_list = objects.AllocationList(
+ self.context,
+ objects=[alloc1, alloc2],
+ project_id=project_id,
+ user_id=user_id,
+ )
alloc_list.create_all()
# 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(
self.context, resource_provider=rp,
- resource_class='VCPU', total=8,
+ resource_class='VCPU', total=10,
max_unit=4)
inventory.obj_set_defaults()
rp.add_inventory(inventory)
alloc1 = objects.Allocation(
self.context, resource_provider=rp,
resource_class='VCPU',
- consumer_id=uuidutils.generate_uuid(),
+ consumer_id=consumer_id,
used=2)
alloc2 = objects.Allocation(
self.context, resource_provider=rp,
resource_class='VCPU',
- consumer_id=uuidutils.generate_uuid(),
+ consumer_id=consumer_id,
used=4)
- alloc_list = objects.AllocationList(self.context,
- objects=[alloc1, alloc2])
+ alloc_list = objects.AllocationList(
+ 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()
# The ALT_RP_XXX variables are for a resource provider that has
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml
index 33cdd3338d..d721c9be67 100644
--- a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml
+++ b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml
@@ -39,13 +39,13 @@ tests:
response_json_paths:
$.errors[0].title: Not Acceptable
-- name: latest microversion is 1.8
+- name: latest microversion is 1.9
GET: /
request_headers:
openstack-api-version: placement latest
response_headers:
vary: /OpenStack-API-Version/
- openstack-api-version: placement 1.8
+ openstack-api-version: placement 1.9
- name: other accept header bad version
GET: /
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/usage.yaml b/nova/tests/functional/api/openstack/placement/gabbits/usage.yaml
index dbe3a767e3..b34c025572 100644
--- a/nova/tests/functional/api/openstack/placement/gabbits/usage.yaml
+++ b/nova/tests/functional/api/openstack/placement/gabbits/usage.yaml
@@ -37,3 +37,47 @@ tests:
content-type: application/json
response_json_paths:
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'"
diff --git a/nova/tests/functional/api/openstack/placement/gabbits/with-allocations.yaml b/nova/tests/functional/api/openstack/placement/gabbits/with-allocations.yaml
index a2ffc01c2f..13f112bd7b 100644
--- a/nova/tests/functional/api/openstack/placement/gabbits/with-allocations.yaml
+++ b/nova/tests/functional/api/openstack/placement/gabbits/with-allocations.yaml
@@ -21,9 +21,9 @@ tests:
# required but superfluous, is present
content-type: /application/json/
response_json_paths:
- $.resource_provider_generation: 4
- $.usages.DISK_GB: 1000
- $.usages.VCPU: 6
+ $.resource_provider_generation: 5
+ $.usages.DISK_GB: 1020
+ $.usages.VCPU: 7
- name: fail to delete resource provider
DELETE: /resource_providers/$ENVIRON['RP_UUID']
@@ -41,3 +41,30 @@ tests:
content-type: /application/json/
response_strings:
- 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
diff --git a/nova/tests/functional/db/test_resource_provider.py b/nova/tests/functional/db/test_resource_provider.py
index 9fd9bd2995..44e416feb0 100644
--- a/nova/tests/functional/db/test_resource_provider.py
+++ b/nova/tests/functional/db/test_resource_provider.py
@@ -1258,6 +1258,33 @@ class TestAllocationListCreateDelete(ResourceProviderBaseCase):
res = conn.execute(sel).fetchall()
self.assertEqual(1, len(res), "consumer lookup not created.")
+ # Create allocation for a different user in the project
+ other_consumer_uuid = uuidsentinel.other_consumer
+ allocation3 = objects.Allocation(resource_provider=rp,
+ consumer_id=other_consumer_uuid,
+ resource_class=rp_class,
+ used=200)
+ allocation_list = objects.AllocationList(
+ self.context,
+ objects=[allocation3],
+ project_id=self.context.project_id,
+ user_id=uuidsentinel.other_user,
+ )
+ allocation_list.create_all()
+
+ # Get usages back by project
+ usage_list = objects.UsageList.get_all_by_project_user(
+ self.context, self.context.project_id)
+ self.assertEqual(1, len(usage_list))
+ self.assertEqual(500, usage_list[0].usage)
+
+ # Get usages back by project and user
+ usage_list = objects.UsageList.get_all_by_project_user(
+ self.context, self.context.project_id,
+ user_id=uuidsentinel.other_user)
+ self.assertEqual(1, len(usage_list))
+ self.assertEqual(200, usage_list[0].usage)
+
class UsageListTestCase(ResourceProviderBaseCase):
diff --git a/nova/tests/unit/api/openstack/placement/test_microversion.py b/nova/tests/unit/api/openstack/placement/test_microversion.py
index 2a98fa9049..11f1d49cf5 100644
--- a/nova/tests/unit/api/openstack/placement/test_microversion.py
+++ b/nova/tests/unit/api/openstack/placement/test_microversion.py
@@ -74,7 +74,7 @@ class TestMicroversionIntersection(test.NoDBTestCase):
# if you add two different versions of method 'foobar' the
# number only goes up by one if no other version foobar yet
# exists. This operates as a simple sanity check.
- TOTAL_VERSIONED_METHODS = 13
+ TOTAL_VERSIONED_METHODS = 14
def test_methods_versioned(self):
methods_data = microversion.VERSIONED_METHODS
diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py
index e64cd73be5..0911e40dc8 100644
--- a/nova/tests/unit/objects/test_objects.py
+++ b/nova/tests/unit/objects/test_objects.py
@@ -1171,7 +1171,7 @@ object_data = {
'Trait': '1.0-2b58dd7c5037153cb4bfc94c0ae5dd3a',
'TraitList': '1.0-ff48fc1575f20800796b48266114c608',
'Usage': '1.1-b738dbebeb20e3199fc0ebca6e292a47',
- 'UsageList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
+ 'UsageList': '1.2-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'USBDeviceBus': '1.0-e4c7dd6032e46cd74b027df5eb2d4750',
'VirtCPUFeature': '1.0-ea2464bdd09084bd388e5f61d5d4fc86',
'VirtCPUModel': '1.0-5e1864af9227f698326203d7249796b5',