summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Smith <dansmith@redhat.com>2021-06-04 09:20:46 -0700
committerDan Smith <dansmith@redhat.com>2022-02-03 09:55:50 -0800
commitf865b8cac7fb008c870e433d22fb4216ed95e285 (patch)
treef88f7837b07c7dc5ce578d439603a976c73461f1
parentd7368446e484dc07b613ff5b225b1c3e47700389 (diff)
downloadglance-f865b8cac7fb008c870e433d22fb4216ed95e285.tar.gz
[APIImpact] Quota usage API
This adds a /v2/info/usage API endpoint which exposes to the user their current limits and usage. The discovery API does not (appear to) have existing tests, so this adds a module for that, although only usage tests are added currently. Implements: blueprint quota-api Change-Id: I50c98bac50f815bdb9baae024e77afd388f74554
-rw-r--r--api-ref/source/v2/discovery.inc23
-rw-r--r--api-ref/source/v2/samples/usage-response.json20
-rw-r--r--glance/api/middleware/version_negotiation.py1
-rw-r--r--glance/api/v2/discovery.py51
-rw-r--r--glance/api/v2/router.py4
-rw-r--r--glance/api/versions.py5
-rw-r--r--glance/quota/keystone.py26
-rw-r--r--glance/tests/functional/v2/test_discovery.py98
-rw-r--r--glance/tests/functional/v2/test_images.py8
-rw-r--r--glance/tests/unit/test_versions.py23
-rw-r--r--releasenotes/notes/added-quota-usage-api-f1914054132f2021.yaml9
-rw-r--r--requirements.txt2
12 files changed, 259 insertions, 11 deletions
diff --git a/api-ref/source/v2/discovery.inc b/api-ref/source/v2/discovery.inc
index dff7a7f40..134428024 100644
--- a/api-ref/source/v2/discovery.inc
+++ b/api-ref/source/v2/discovery.inc
@@ -103,3 +103,26 @@ Response Example
.. literalinclude:: samples/stores-list-response.json
:language: json
+
+Quota usage
+~~~~~~~~~~~
+
+.. rest_method:: GET /v2/info/usage
+
+The user's quota and current usage are displayed, if enabled by
+server-side configuration.
+
+Normal response codes: 200
+
+Request
+-------
+
+There are no request parameters.
+
+This call does not allow a request body.
+
+Response Example
+----------------
+
+.. literalinclude:: samples/usage-response.json
+ :language: json
diff --git a/api-ref/source/v2/samples/usage-response.json b/api-ref/source/v2/samples/usage-response.json
new file mode 100644
index 000000000..961d8153c
--- /dev/null
+++ b/api-ref/source/v2/samples/usage-response.json
@@ -0,0 +1,20 @@
+{
+ "usage": {
+ "image_size_total": {
+ "limit": 1024,
+ "usage": 256
+ },
+ "image_count_total": {
+ "limit": 10,
+ "usage": 2
+ },
+ "image_stage_total": {
+ "limit": 512,
+ "usage": 0
+ },
+ "image_count_uploading": {
+ "limit": 2,
+ "usage": 0
+ }
+ }
+}
diff --git a/glance/api/middleware/version_negotiation.py b/glance/api/middleware/version_negotiation.py
index d645b0ca3..ffa30347c 100644
--- a/glance/api/middleware/version_negotiation.py
+++ b/glance/api/middleware/version_negotiation.py
@@ -82,6 +82,7 @@ class VersionNegotiationFilter(wsgi.Middleware):
allowed_versions['v2.6'] = 2
allowed_versions['v2.7'] = 2
allowed_versions['v2.9'] = 2
+ allowed_versions['v2.13'] = 2
if CONF.enabled_backends:
allowed_versions['v2.8'] = 2
allowed_versions['v2.10'] = 2
diff --git a/glance/api/v2/discovery.py b/glance/api/v2/discovery.py
index 1002c8a5d..e8c2caf75 100644
--- a/glance/api/v2/discovery.py
+++ b/glance/api/v2/discovery.py
@@ -13,12 +13,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import copy
+
from oslo_config import cfg
+import oslo_serialization.jsonutils as json
import webob.exc
from glance.common import wsgi
+import glance.db
from glance.i18n import _
-
+from glance.quota import keystone as ks_quota
CONF = cfg.CONF
@@ -63,6 +67,49 @@ class InfoController(object):
return {'stores': backends}
+ def get_usage(self, req):
+ project_usage = ks_quota.get_usage(req.context)
+ return {'usage':
+ {name: {'usage': usage.usage,
+ 'limit': usage.limit}
+ for name, usage in project_usage.items()}}
+
+
+class ResponseSerializer(wsgi.JSONResponseSerializer):
+ def __init__(self, usage_schema=None):
+ super(ResponseSerializer, self).__init__()
+ self.schema = usage_schema or get_usage_schema()
+
+ def get_usage(self, response, usage):
+ body = json.dumps(self.schema.filter(usage), ensure_ascii=False)
+ response.unicode_body = str(body)
+ response.content_type = 'application/json'
+
+
+_USAGE_SCHEMA = {
+ 'usage': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'additionalProperties': True,
+ 'validation_data': {
+ 'type': 'object',
+ 'additonalProperties': False,
+ 'properties': {
+ 'usage': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ },
+ },
+ },
+ },
+}
+
+
+def get_usage_schema():
+ return glance.schema.Schema('usage', copy.deepcopy(_USAGE_SCHEMA))
+
def create_resource():
- return wsgi.Resource(InfoController())
+ usage_schema = get_usage_schema()
+ serializer = ResponseSerializer(usage_schema)
+ return wsgi.Resource(InfoController(), None, serializer)
diff --git a/glance/api/v2/router.py b/glance/api/v2/router.py
index 2055d5cbd..d3f115466 100644
--- a/glance/api/v2/router.py
+++ b/glance/api/v2/router.py
@@ -588,5 +588,9 @@ class API(wsgi.Router):
controller=reject_method_resource,
action='reject',
allowed_methods='GET')
+ mapper.connect('/info/usage',
+ controller=info_resource,
+ action='get_usage',
+ conditions={'method': ['GET']})
super(API, self).__init__(mapper)
diff --git a/glance/api/versions.py b/glance/api/versions.py
index 211cbc7bf..43df1ad71 100644
--- a/glance/api/versions.py
+++ b/glance/api/versions.py
@@ -78,7 +78,7 @@ class Controller(object):
version_objs = []
if CONF.enabled_backends:
version_objs.extend([
- build_version_object(2.12, 'v2', 'CURRENT'),
+ build_version_object(2.12, 'v2', 'SUPPORTED'),
build_version_object(2.11, 'v2', 'SUPPORTED'),
build_version_object('2.10', 'v2', 'SUPPORTED'),
build_version_object(2.9, 'v2', 'SUPPORTED'),
@@ -86,9 +86,10 @@ class Controller(object):
])
else:
version_objs.extend([
- build_version_object(2.9, 'v2', 'CURRENT'),
+ build_version_object(2.9, 'v2', 'SUPPORTED'),
])
version_objs.extend([
+ build_version_object(2.13, 'v2', 'CURRENT'),
build_version_object(2.7, 'v2', 'SUPPORTED'),
build_version_object(2.6, 'v2', 'SUPPORTED'),
build_version_object(2.5, 'v2', 'SUPPORTED'),
diff --git a/glance/quota/keystone.py b/glance/quota/keystone.py
index fa8e8c3e1..67b48d328 100644
--- a/glance/quota/keystone.py
+++ b/glance/quota/keystone.py
@@ -142,3 +142,29 @@ def enforce_image_count_uploading(context, project_id):
context, project_id, QUOTA_IMAGE_COUNT_UPLOADING,
lambda: db.user_get_uploading_count(context, project_id),
delta=0)
+
+
+def get_usage(context, project_id=None):
+ if not CONF.use_keystone_limits:
+ return {}
+
+ if not project_id:
+ project_id = context.project_id
+
+ usages = {
+ QUOTA_IMAGE_SIZE_TOTAL: lambda: db.user_get_storage_usage(
+ context, project_id) // units.Mi,
+ QUOTA_IMAGE_STAGING_TOTAL: lambda: db.user_get_staging_usage(
+ context, project_id) // units.Mi,
+ QUOTA_IMAGE_COUNT_TOTAL: lambda: db.user_get_image_count(
+ context, project_id),
+ QUOTA_IMAGE_COUNT_UPLOADING: lambda: db.user_get_uploading_count(
+ context, project_id),
+ }
+
+ def callback(project_id, resource_names):
+ return {name: usages[name]()
+ for name in resource_names}
+
+ enforcer = limit.Enforcer(callback)
+ return enforcer.calculate_usage(project_id, list(usages.keys()))
diff --git a/glance/tests/functional/v2/test_discovery.py b/glance/tests/functional/v2/test_discovery.py
new file mode 100644
index 000000000..537f19558
--- /dev/null
+++ b/glance/tests/functional/v2/test_discovery.py
@@ -0,0 +1,98 @@
+# Copyright 2021 Red Hat, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import fixtures
+
+from oslo_utils import units
+
+from glance.quota import keystone as ks_quota
+from glance.tests import functional
+from glance.tests.functional.v2.test_images import get_enforcer_class
+from glance.tests import utils as test_utils
+
+
+class TestDiscovery(functional.SynchronousAPIBase):
+ def setUp(self):
+ super(TestDiscovery, self).setUp()
+ self.config(use_keystone_limits=True)
+
+ self.enforcer_mock = self.useFixture(
+ fixtures.MockPatchObject(ks_quota, 'limit')).mock
+
+ def set_limit(self, limits):
+ self.enforcer_mock.Enforcer = get_enforcer_class(limits)
+
+ def _assert_usage(self, expected):
+ usage = self.api_get('/v2/info/usage')
+ usage = usage.json['usage']
+ for item in ('count', 'size', 'stage'):
+ key = 'image_%s_total' % item
+ self.assertEqual(expected[key], usage[key],
+ 'Mismatch in %s' % key)
+ self.assertEqual(expected['image_count_uploading'],
+ usage['image_count_uploading'])
+
+ def test_quota_with_usage(self):
+ self.set_limit({'image_size_total': 5,
+ 'image_count_total': 10,
+ 'image_stage_total': 15,
+ 'image_count_uploading': 20})
+
+ self.start_server()
+
+ # Initially we expect no usage, but our limits in place.
+ expected = {
+ 'image_size_total': {'limit': 5, 'usage': 0},
+ 'image_count_total': {'limit': 10, 'usage': 0},
+ 'image_stage_total': {'limit': 15, 'usage': 0},
+ 'image_count_uploading': {'limit': 20, 'usage': 0},
+ }
+ self._assert_usage(expected)
+
+ # Stage 1MiB and see our total count, uploading count, and
+ # staging area usage increase.
+ data = test_utils.FakeData(1 * units.Mi)
+ image_id = self._create_and_stage(data_iter=data)
+ expected['image_count_uploading']['usage'] = 1
+ expected['image_count_total']['usage'] = 1
+ expected['image_stage_total']['usage'] = 1
+ self._assert_usage(expected)
+
+ # Doing the import does not change anything (since we are
+ # synchronous and the task will not have run yet).
+ self._import_direct(image_id, ['store1'])
+ self._assert_usage(expected)
+
+ # After the import is complete, our usage of the staging area
+ # drops to zero, and our consumption of actual store space
+ # reflects the new active image.
+ self._wait_for_import(image_id)
+ expected['image_count_uploading']['usage'] = 0
+ expected['image_stage_total']['usage'] = 0
+ expected['image_size_total']['usage'] = 1
+ self._assert_usage(expected)
+
+ # Upload also yields a new active image and store usage.
+ data = test_utils.FakeData(1 * units.Mi)
+ image_id = self._create_and_upload(data_iter=data)
+ expected['image_count_total']['usage'] = 2
+ expected['image_size_total']['usage'] = 2
+ self._assert_usage(expected)
+
+ # Deleting an image drops the usage down.
+ self.api_delete('/v2/images/%s' % image_id)
+ expected['image_count_total']['usage'] = 1
+ expected['image_size_total']['usage'] = 1
+ self._assert_usage(expected)
diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py
index 124a0e92c..0325ac7d0 100644
--- a/glance/tests/functional/v2/test_images.py
+++ b/glance/tests/functional/v2/test_images.py
@@ -22,6 +22,7 @@ import uuid
import fixtures
from oslo_limit import exception as ol_exc
+from oslo_limit import limit
from oslo_serialization import jsonutils
from oslo_utils.secretutils import md5
from oslo_utils import units
@@ -7037,6 +7038,13 @@ def get_enforcer_class(limits):
over_limit_info_list=[ol_exc.OverLimitInfo(
name, limits.get(name), current.get(name), delta)])
+ def calculate_usage(self, project_id, names):
+ return {
+ name: limit.ProjectUsage(
+ limits.get(name, 0),
+ self._callback(project_id, [name])[name])
+ for name in names}
+
return FakeEnforcer
diff --git a/glance/tests/unit/test_versions.py b/glance/tests/unit/test_versions.py
index 1dd115fea..7ab8c3b93 100644
--- a/glance/tests/unit/test_versions.py
+++ b/glance/tests/unit/test_versions.py
@@ -30,6 +30,12 @@ from glance.tests.unit import base
def get_versions_list(url, enabled_backends=False):
image_versions = [
{
+ 'id': 'v2.13',
+ 'status': 'CURRENT',
+ 'links': [{'rel': 'self',
+ 'href': '%s/v2/' % url}],
+ },
+ {
'id': 'v2.7',
'status': 'SUPPORTED',
'links': [{'rel': 'self',
@@ -82,7 +88,7 @@ def get_versions_list(url, enabled_backends=False):
image_versions = [
{
'id': 'v2.12',
- 'status': 'CURRENT',
+ 'status': 'SUPPORTED',
'links': [{'rel': 'self',
'href': '%s/v2/' % url}],
},
@@ -114,7 +120,7 @@ def get_versions_list(url, enabled_backends=False):
else:
image_versions.insert(0, {
'id': 'v2.9',
- 'status': 'CURRENT',
+ 'status': 'SUPPORTED',
'links': [{'rel': 'self',
'href': '%s/v2/' % url}],
})
@@ -321,15 +327,20 @@ class VersionNegotiationTest(base.IsolatedUnitTest):
self.middleware.process_request(request)
self.assertEqual('/v2/images', request.path_info)
- # version 2.13 does not exist
- def test_request_url_v2_13_default_unsupported(self):
+ def test_request_url_v2_13_enabled_supported(self):
request = webob.Request.blank('/v2.13/images')
+ self.middleware.process_request(request)
+ self.assertEqual('/v2/images', request.path_info)
+
+ # version 2.14 does not exist
+ def test_request_url_v2_14_default_unsupported(self):
+ request = webob.Request.blank('/v2.14/images')
resp = self.middleware.process_request(request)
self.assertIsInstance(resp, versions.Controller)
- def test_request_url_v2_13_enabled_unsupported(self):
+ def test_request_url_v2_14_enabled_unsupported(self):
self.config(enabled_backends='slow:one,fast:two')
- request = webob.Request.blank('/v2.13/images')
+ request = webob.Request.blank('/v2.14/images')
resp = self.middleware.process_request(request)
self.assertIsInstance(resp, versions.Controller)
diff --git a/releasenotes/notes/added-quota-usage-api-f1914054132f2021.yaml b/releasenotes/notes/added-quota-usage-api-f1914054132f2021.yaml
new file mode 100644
index 000000000..35da85ee8
--- /dev/null
+++ b/releasenotes/notes/added-quota-usage-api-f1914054132f2021.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - |
+ This release brings additional functionality to the unified quota
+ work done in the previous release. A usage API is now available,
+ which provides a way for users to see their current quota limits
+ and their active resource usage towards them. For more
+ information, see the discovery section in the `api-ref
+ <https://developer.openstack.org/api-ref/image/v2/index.html#image-service-info-discovery>`_.
diff --git a/requirements.txt b/requirements.txt
index 798746a22..177be6533 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -38,7 +38,7 @@ six>=1.11.0 # MIT
oslo.db>=5.0.0 # Apache-2.0
oslo.i18n>=5.0.0 # Apache-2.0
-oslo.limit>=1.0.0 # Apache-2.0
+oslo.limit>=1.4.0 # Apache-2.0
oslo.log>=4.5.0 # Apache-2.0
oslo.messaging>=5.29.0,!=9.0.0 # Apache-2.0
oslo.middleware>=3.31.0 # Apache-2.0