Skip to content

Commit 94f9e44

Browse files
JohnGarbuttmelwitt
authored andcommitted
Update quota_class APIs for db and api limits
Implement a unified limits specific version of get_class_quotas as used by the quota_class API. This simply returns the limits defined in keystone that are now enforced when you enable unified limits. Note: this will need to be updated again once we add limits to things that use things like resource_class, etc. blueprint unified-limits-nova Change-Id: If9901662d30d15da13303a3da051e1b9fded72c0
1 parent 4207493 commit 94f9e44

File tree

7 files changed

+206
-9
lines changed

7 files changed

+206
-9
lines changed

nova/api/openstack/compute/quota_classes.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from nova.api.openstack import wsgi
2121
from nova.api import validation
2222
from nova import exception
23+
from nova.limit import utils as limit_utils
2324
from nova import objects
2425
from nova.policies import quota_class_sets as qcs_policies
2526
from nova import quota
@@ -129,11 +130,20 @@ def _update(self, req, id, body, filtered_quotas=None,
129130

130131
quota_class = id
131132

132-
for key, value in body['quota_class_set'].items():
133-
try:
134-
objects.Quotas.update_class(context, quota_class, key, value)
135-
except exception.QuotaClassNotFound:
136-
objects.Quotas.create_class(context, quota_class, key, value)
133+
quota_updates = body['quota_class_set'].items()
134+
# TODO(johngarbutt) eventually cores, ram and instances changes will
135+
# get sent to keystone when using unified limits, but only when the
136+
# quota_class == "default".
137+
if not limit_utils.use_unified_limits():
138+
# When not unified limits, keep updating the database, even though
139+
# the noop driver doesn't read these values
140+
for key, value in quota_updates:
141+
try:
142+
objects.Quotas.update_class(
143+
context, quota_class, key, value)
144+
except exception.QuotaClassNotFound:
145+
objects.Quotas.create_class(
146+
context, quota_class, key, value)
137147

138148
values = QUOTAS.get_class_quotas(context, quota_class)
139149
return self._format_quota_set(None, values, filtered_quotas,

nova/limit/local.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import nova.conf
2323
from nova import context as nova_context
2424
from nova import exception
25+
from nova.limit import utils as nova_limit_utils
2526
from nova import objects
2627

2728
LOG = logging.getLogger(__name__)
@@ -119,7 +120,7 @@ def enforce_api_limit(entity_type: str, count: int) -> None:
119120
This is generally used for limiting the size of certain API requests
120121
that eventually get stored in the database.
121122
"""
122-
if CONF.quota.driver != UNIFIED_LIMITS_DRIVER:
123+
if not nova_limit_utils.use_unified_limits():
123124
return
124125

125126
if entity_type not in API_LIMITS:
@@ -163,7 +164,7 @@ def enforce_db_limit(
163164
* server_groups scope is context.project_id
164165
* server_group_members scope is server_group_uuid
165166
"""
166-
if CONF.quota.driver != UNIFIED_LIMITS_DRIVER:
167+
if not nova_limit_utils.use_unified_limits():
167168
return
168169

169170
if entity_type not in DB_COUNT_FUNCTION.keys():

nova/limit/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2022 StackHPC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
import nova.conf
16+
CONF = nova.conf.CONF
17+
18+
UNIFIED_LIMITS_DRIVER = "nova.quota.UnifiedLimitsDriver"
19+
20+
21+
def use_unified_limits():
22+
return CONF.quota.driver == UNIFIED_LIMITS_DRIVER

nova/quota.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from nova.db.api import models as api_models
2929
from nova.db.main import api as main_db_api
3030
from nova import exception
31+
from nova.limit import local as local_limit
3132
from nova import objects
3233
from nova.scheduler.client import report
3334
from nova import utils
@@ -792,6 +793,30 @@ def get_reserved(self):
792793
# To make unified limits APIs the same as the DB driver, return 0
793794
return 0
794795

796+
def get_class_quotas(self, context, resources, quota_class):
797+
"""Given a list of resources, retrieve the quotas for the given
798+
quota class.
799+
800+
:param context: The request context, for access checks.
801+
:param resources: A dictionary of the registered resources.
802+
:param quota_class: Placeholder, we always assume default quota class.
803+
"""
804+
805+
local_limits = local_limit.get_legacy_default_limits()
806+
# TODO(melwitt): This is temporary when we are in a state where cores,
807+
# ram, and instances quota limits are not known/enforced with unified
808+
# limits yet. This will occur in later patches and when it does, we
809+
# will change the default to 0 to signal to operators that they need to
810+
# register a limit for a resource before that resource will be
811+
# allocated.
812+
# Default to unlimited, as per no-op for everything that isn't
813+
# a local limit
814+
quotas = {}
815+
for resource in resources.values():
816+
quotas[resource.name] = local_limits.get(resource.name, -1)
817+
818+
return quotas
819+
795820

796821
class BaseResource(object):
797822
"""Describe a single resource for quota checking."""

nova/tests/unit/api/openstack/compute/test_quota_classes.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
# under the License.
1515
import copy
1616
import mock
17+
from oslo_limit import fixture as limit_fixture
1718
import webob
1819

1920
from nova.api.openstack.compute import quota_classes \
2021
as quota_classes_v21
2122
from nova import exception
23+
from nova.limit import local as local_limit
2224
from nova import objects
2325
from nova import test
2426
from nova.tests.unit.api.openstack import fakes
@@ -262,3 +264,108 @@ def test_update_v257(self, mock_update):
262264

263265
class UnifiedLimitsQuotaClassesTest(NoopQuotaClassesTest):
264266
quota_driver = "nova.quota.UnifiedLimitsDriver"
267+
268+
def setUp(self):
269+
super(UnifiedLimitsQuotaClassesTest, self).setUp()
270+
# Set server_groups so all config options get a different value
271+
# but we also test as much as possible with the default config
272+
self.flags(driver="nova.quota.UnifiedLimitsDriver", group='quota')
273+
reglimits = {local_limit.SERVER_METADATA_ITEMS: 128,
274+
local_limit.INJECTED_FILES: 5,
275+
local_limit.INJECTED_FILES_CONTENT: 10 * 1024,
276+
local_limit.INJECTED_FILES_PATH: 255,
277+
local_limit.KEY_PAIRS: 100,
278+
local_limit.SERVER_GROUPS: 12,
279+
local_limit.SERVER_GROUP_MEMBERS: 10}
280+
self.useFixture(limit_fixture.LimitFixture(reglimits, {}))
281+
282+
def test_show_v21(self):
283+
req = fakes.HTTPRequest.blank("")
284+
response = self.controller.show(req, "test_class")
285+
expected_response = {
286+
'quota_class_set': {
287+
'id': 'test_class',
288+
'cores': -1,
289+
'fixed_ips': -1,
290+
'floating_ips': -1,
291+
'ram': -1,
292+
'injected_file_content_bytes': 10240,
293+
'injected_file_path_bytes': 255,
294+
'injected_files': 5,
295+
'instances': -1,
296+
'key_pairs': 100,
297+
'metadata_items': 128,
298+
'security_group_rules': -1,
299+
'security_groups': -1,
300+
}
301+
}
302+
self.assertEqual(expected_response, response)
303+
304+
def test_show_v257(self):
305+
req = fakes.HTTPRequest.blank("", version='2.57')
306+
response = self.controller.show(req, "default")
307+
expected_response = {
308+
'quota_class_set': {
309+
'id': 'default',
310+
'cores': -1,
311+
'instances': -1,
312+
'ram': -1,
313+
'key_pairs': 100,
314+
'metadata_items': 128,
315+
'server_group_members': 10,
316+
'server_groups': 12,
317+
}
318+
}
319+
self.assertEqual(expected_response, response)
320+
321+
def test_update_still_rejects_badrequests(self):
322+
req = fakes.HTTPRequest.blank("")
323+
body = {'quota_class_set': {'instances': 50, 'cores': 50,
324+
'ram': 51200, 'unsupported': 12}}
325+
self.assertRaises(exception.ValidationError, self.controller.update,
326+
req, 'test_class', body=body)
327+
328+
@mock.patch.object(objects.Quotas, "update_class")
329+
def test_update_v21(self, mock_update):
330+
req = fakes.HTTPRequest.blank("")
331+
body = {'quota_class_set': {'ram': 51200}}
332+
response = self.controller.update(req, 'default', body=body)
333+
expected_response = {
334+
'quota_class_set': {
335+
'cores': -1,
336+
'fixed_ips': -1,
337+
'floating_ips': -1,
338+
'injected_file_content_bytes': 10240,
339+
'injected_file_path_bytes': 255,
340+
'injected_files': 5,
341+
'instances': -1,
342+
'key_pairs': 100,
343+
'metadata_items': 128,
344+
'ram': -1,
345+
'security_group_rules': -1,
346+
'security_groups': -1
347+
}
348+
}
349+
self.assertEqual(expected_response, response)
350+
# TODO(johngarbutt) we should be proxying to keystone
351+
self.assertEqual(0, mock_update.call_count)
352+
353+
@mock.patch.object(objects.Quotas, "update_class")
354+
def test_update_v257(self, mock_update):
355+
req = fakes.HTTPRequest.blank("", version='2.57')
356+
body = {'quota_class_set': {'ram': 51200}}
357+
response = self.controller.update(req, 'default', body=body)
358+
expected_response = {
359+
'quota_class_set': {
360+
'cores': -1,
361+
'instances': -1,
362+
'ram': -1,
363+
'key_pairs': 100,
364+
'metadata_items': 128,
365+
'server_group_members': 10,
366+
'server_groups': 12,
367+
}
368+
}
369+
self.assertEqual(expected_response, response)
370+
# TODO(johngarbutt) we should be proxying to keystone
371+
self.assertEqual(0, mock_update.call_count)

nova/tests/unit/limit/test_local.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from nova import context
2525
from nova import exception
2626
from nova.limit import local as local_limit
27+
from nova.limit import utils as limit_utils
2728
from nova import objects
2829
from nova import test
2930

@@ -33,7 +34,7 @@
3334
class TestLocalLimits(test.NoDBTestCase):
3435
def setUp(self):
3536
super(TestLocalLimits, self).setUp()
36-
self.flags(driver=local_limit.UNIFIED_LIMITS_DRIVER, group="quota")
37+
self.flags(driver=limit_utils.UNIFIED_LIMITS_DRIVER, group="quota")
3738
self.context = context.RequestContext()
3839

3940
def test_enforce_api_limit_metadata(self):
@@ -240,7 +241,7 @@ def setUp(self):
240241
"server_group_members": 7}
241242
self.resources = list(local_limit.API_LIMITS | local_limit.DB_LIMITS)
242243
self.resources.sort()
243-
self.flags(driver=local_limit.UNIFIED_LIMITS_DRIVER, group="quota")
244+
self.flags(driver=limit_utils.UNIFIED_LIMITS_DRIVER, group="quota")
244245

245246
def test_convert_keys_to_legacy_name(self):
246247
limits = local_limit._convert_keys_to_legacy_name(self.new)

nova/tests/unit/test_quota.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1956,6 +1956,37 @@ class UnifiedLimitsDriverTestCase(NoopQuotaDriverTestCase):
19561956
def setUp(self):
19571957
super(UnifiedLimitsDriverTestCase, self).setUp()
19581958
self.driver = quota.UnifiedLimitsDriver()
1959+
# Set this so all limits get a different value but we also test as much
1960+
# as possible with the default config
1961+
reglimits = {local_limit.SERVER_METADATA_ITEMS: 128,
1962+
local_limit.INJECTED_FILES: 5,
1963+
local_limit.INJECTED_FILES_CONTENT: 10 * 1024,
1964+
local_limit.INJECTED_FILES_PATH: 255,
1965+
local_limit.KEY_PAIRS: 100,
1966+
local_limit.SERVER_GROUPS: 12,
1967+
local_limit.SERVER_GROUP_MEMBERS: 10}
1968+
self.useFixture(limit_fixture.LimitFixture(reglimits, {}))
1969+
1970+
def test_get_class_quotas(self):
1971+
result = self.driver.get_class_quotas(
1972+
None, quota.QUOTAS._resources, 'default')
1973+
expected_limits = {
1974+
'cores': -1,
1975+
'fixed_ips': -1,
1976+
'floating_ips': -1,
1977+
'injected_file_content_bytes': 10240,
1978+
'injected_file_path_bytes': 255,
1979+
'injected_files': 5,
1980+
'instances': -1,
1981+
'key_pairs': 100,
1982+
'metadata_items': 128,
1983+
'ram': -1,
1984+
'security_group_rules': -1,
1985+
'security_groups': -1,
1986+
'server_group_members': 10,
1987+
'server_groups': 12,
1988+
}
1989+
self.assertEqual(expected_limits, result)
19591990

19601991

19611992
@ddt.ddt

0 commit comments

Comments
 (0)