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
This commit is contained in:
John Garbutt 2020-03-10 17:27:53 +00:00 committed by melanie witt
parent 4207493829
commit 94f9e443f2
7 changed files with 206 additions and 9 deletions

View File

@ -20,6 +20,7 @@ from nova.api.openstack.compute.schemas import quota_classes
from nova.api.openstack import wsgi
from nova.api import validation
from nova import exception
from nova.limit import utils as limit_utils
from nova import objects
from nova.policies import quota_class_sets as qcs_policies
from nova import quota
@ -129,11 +130,20 @@ class QuotaClassSetsController(wsgi.Controller):
quota_class = id
for key, value in body['quota_class_set'].items():
try:
objects.Quotas.update_class(context, quota_class, key, value)
except exception.QuotaClassNotFound:
objects.Quotas.create_class(context, quota_class, key, value)
quota_updates = body['quota_class_set'].items()
# TODO(johngarbutt) eventually cores, ram and instances changes will
# get sent to keystone when using unified limits, but only when the
# quota_class == "default".
if not limit_utils.use_unified_limits():
# When not unified limits, keep updating the database, even though
# the noop driver doesn't read these values
for key, value in quota_updates:
try:
objects.Quotas.update_class(
context, quota_class, key, value)
except exception.QuotaClassNotFound:
objects.Quotas.create_class(
context, quota_class, key, value)
values = QUOTAS.get_class_quotas(context, quota_class)
return self._format_quota_set(None, values, filtered_quotas,

View File

@ -22,6 +22,7 @@ from oslo_log import log as logging
import nova.conf
from nova import context as nova_context
from nova import exception
from nova.limit import utils as nova_limit_utils
from nova import objects
LOG = logging.getLogger(__name__)
@ -119,7 +120,7 @@ def enforce_api_limit(entity_type: str, count: int) -> None:
This is generally used for limiting the size of certain API requests
that eventually get stored in the database.
"""
if CONF.quota.driver != UNIFIED_LIMITS_DRIVER:
if not nova_limit_utils.use_unified_limits():
return
if entity_type not in API_LIMITS:
@ -163,7 +164,7 @@ def enforce_db_limit(
* server_groups scope is context.project_id
* server_group_members scope is server_group_uuid
"""
if CONF.quota.driver != UNIFIED_LIMITS_DRIVER:
if not nova_limit_utils.use_unified_limits():
return
if entity_type not in DB_COUNT_FUNCTION.keys():

22
nova/limit/utils.py Normal file
View File

@ -0,0 +1,22 @@
# Copyright 2022 StackHPC
#
# 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 nova.conf
CONF = nova.conf.CONF
UNIFIED_LIMITS_DRIVER = "nova.quota.UnifiedLimitsDriver"
def use_unified_limits():
return CONF.quota.driver == UNIFIED_LIMITS_DRIVER

View File

@ -28,6 +28,7 @@ from nova.db.api import api as api_db_api
from nova.db.api import models as api_models
from nova.db.main import api as main_db_api
from nova import exception
from nova.limit import local as local_limit
from nova import objects
from nova.scheduler.client import report
from nova import utils
@ -792,6 +793,30 @@ class UnifiedLimitsDriver(NoopQuotaDriver):
# To make unified limits APIs the same as the DB driver, return 0
return 0
def get_class_quotas(self, context, resources, quota_class):
"""Given a list of resources, retrieve the quotas for the given
quota class.
:param context: The request context, for access checks.
:param resources: A dictionary of the registered resources.
:param quota_class: Placeholder, we always assume default quota class.
"""
local_limits = local_limit.get_legacy_default_limits()
# TODO(melwitt): This is temporary when we are in a state where cores,
# ram, and instances quota limits are not known/enforced with unified
# limits yet. This will occur in later patches and when it does, we
# will change the default to 0 to signal to operators that they need to
# register a limit for a resource before that resource will be
# allocated.
# Default to unlimited, as per no-op for everything that isn't
# a local limit
quotas = {}
for resource in resources.values():
quotas[resource.name] = local_limits.get(resource.name, -1)
return quotas
class BaseResource(object):
"""Describe a single resource for quota checking."""

View File

@ -14,11 +14,13 @@
# under the License.
import copy
import mock
from oslo_limit import fixture as limit_fixture
import webob
from nova.api.openstack.compute import quota_classes \
as quota_classes_v21
from nova import exception
from nova.limit import local as local_limit
from nova import objects
from nova import test
from nova.tests.unit.api.openstack import fakes
@ -262,3 +264,108 @@ class NoopQuotaClassesTest(test.NoDBTestCase):
class UnifiedLimitsQuotaClassesTest(NoopQuotaClassesTest):
quota_driver = "nova.quota.UnifiedLimitsDriver"
def setUp(self):
super(UnifiedLimitsQuotaClassesTest, self).setUp()
# Set server_groups so all config options get a different value
# but we also test as much as possible with the default config
self.flags(driver="nova.quota.UnifiedLimitsDriver", group='quota')
reglimits = {local_limit.SERVER_METADATA_ITEMS: 128,
local_limit.INJECTED_FILES: 5,
local_limit.INJECTED_FILES_CONTENT: 10 * 1024,
local_limit.INJECTED_FILES_PATH: 255,
local_limit.KEY_PAIRS: 100,
local_limit.SERVER_GROUPS: 12,
local_limit.SERVER_GROUP_MEMBERS: 10}
self.useFixture(limit_fixture.LimitFixture(reglimits, {}))
def test_show_v21(self):
req = fakes.HTTPRequest.blank("")
response = self.controller.show(req, "test_class")
expected_response = {
'quota_class_set': {
'id': 'test_class',
'cores': -1,
'fixed_ips': -1,
'floating_ips': -1,
'ram': -1,
'injected_file_content_bytes': 10240,
'injected_file_path_bytes': 255,
'injected_files': 5,
'instances': -1,
'key_pairs': 100,
'metadata_items': 128,
'security_group_rules': -1,
'security_groups': -1,
}
}
self.assertEqual(expected_response, response)
def test_show_v257(self):
req = fakes.HTTPRequest.blank("", version='2.57')
response = self.controller.show(req, "default")
expected_response = {
'quota_class_set': {
'id': 'default',
'cores': -1,
'instances': -1,
'ram': -1,
'key_pairs': 100,
'metadata_items': 128,
'server_group_members': 10,
'server_groups': 12,
}
}
self.assertEqual(expected_response, response)
def test_update_still_rejects_badrequests(self):
req = fakes.HTTPRequest.blank("")
body = {'quota_class_set': {'instances': 50, 'cores': 50,
'ram': 51200, 'unsupported': 12}}
self.assertRaises(exception.ValidationError, self.controller.update,
req, 'test_class', body=body)
@mock.patch.object(objects.Quotas, "update_class")
def test_update_v21(self, mock_update):
req = fakes.HTTPRequest.blank("")
body = {'quota_class_set': {'ram': 51200}}
response = self.controller.update(req, 'default', body=body)
expected_response = {
'quota_class_set': {
'cores': -1,
'fixed_ips': -1,
'floating_ips': -1,
'injected_file_content_bytes': 10240,
'injected_file_path_bytes': 255,
'injected_files': 5,
'instances': -1,
'key_pairs': 100,
'metadata_items': 128,
'ram': -1,
'security_group_rules': -1,
'security_groups': -1
}
}
self.assertEqual(expected_response, response)
# TODO(johngarbutt) we should be proxying to keystone
self.assertEqual(0, mock_update.call_count)
@mock.patch.object(objects.Quotas, "update_class")
def test_update_v257(self, mock_update):
req = fakes.HTTPRequest.blank("", version='2.57')
body = {'quota_class_set': {'ram': 51200}}
response = self.controller.update(req, 'default', body=body)
expected_response = {
'quota_class_set': {
'cores': -1,
'instances': -1,
'ram': -1,
'key_pairs': 100,
'metadata_items': 128,
'server_group_members': 10,
'server_groups': 12,
}
}
self.assertEqual(expected_response, response)
# TODO(johngarbutt) we should be proxying to keystone
self.assertEqual(0, mock_update.call_count)

View File

@ -24,6 +24,7 @@ from oslo_utils.fixture import uuidsentinel as uuids
from nova import context
from nova import exception
from nova.limit import local as local_limit
from nova.limit import utils as limit_utils
from nova import objects
from nova import test
@ -33,7 +34,7 @@ CONF = cfg.CONF
class TestLocalLimits(test.NoDBTestCase):
def setUp(self):
super(TestLocalLimits, self).setUp()
self.flags(driver=local_limit.UNIFIED_LIMITS_DRIVER, group="quota")
self.flags(driver=limit_utils.UNIFIED_LIMITS_DRIVER, group="quota")
self.context = context.RequestContext()
def test_enforce_api_limit_metadata(self):
@ -240,7 +241,7 @@ class GetLegacyLimitsTest(test.NoDBTestCase):
"server_group_members": 7}
self.resources = list(local_limit.API_LIMITS | local_limit.DB_LIMITS)
self.resources.sort()
self.flags(driver=local_limit.UNIFIED_LIMITS_DRIVER, group="quota")
self.flags(driver=limit_utils.UNIFIED_LIMITS_DRIVER, group="quota")
def test_convert_keys_to_legacy_name(self):
limits = local_limit._convert_keys_to_legacy_name(self.new)

View File

@ -1956,6 +1956,37 @@ class UnifiedLimitsDriverTestCase(NoopQuotaDriverTestCase):
def setUp(self):
super(UnifiedLimitsDriverTestCase, self).setUp()
self.driver = quota.UnifiedLimitsDriver()
# Set this so all limits get a different value but we also test as much
# as possible with the default config
reglimits = {local_limit.SERVER_METADATA_ITEMS: 128,
local_limit.INJECTED_FILES: 5,
local_limit.INJECTED_FILES_CONTENT: 10 * 1024,
local_limit.INJECTED_FILES_PATH: 255,
local_limit.KEY_PAIRS: 100,
local_limit.SERVER_GROUPS: 12,
local_limit.SERVER_GROUP_MEMBERS: 10}
self.useFixture(limit_fixture.LimitFixture(reglimits, {}))
def test_get_class_quotas(self):
result = self.driver.get_class_quotas(
None, quota.QUOTAS._resources, 'default')
expected_limits = {
'cores': -1,
'fixed_ips': -1,
'floating_ips': -1,
'injected_file_content_bytes': 10240,
'injected_file_path_bytes': 255,
'injected_files': 5,
'instances': -1,
'key_pairs': 100,
'metadata_items': 128,
'ram': -1,
'security_group_rules': -1,
'security_groups': -1,
'server_group_members': 10,
'server_groups': 12,
}
self.assertEqual(expected_limits, result)
@ddt.ddt