Strict two level limit model

This patch introduced the hierarchical limit structure
into Keystone.

The strict two level enforcement model is added as well.

Change-Id: Ic80e435a14ad7d6d4eccd4cd6365fb2d99fd26c1
bp: strict-two-level-model
This commit is contained in:
wangxiyuan 2018-06-08 16:45:56 +08:00
parent 899e691c9f
commit 4b4835a01c
10 changed files with 719 additions and 42 deletions

View File

@ -50,7 +50,7 @@ deployment.
enforcement_model = cfg.StrOpt(
'enforcement_model',
default='flat',
choices=['flat'],
choices=['flat', 'strict_two_level'],
help=utils.fmt("""
The enforcement model to use when validating limits associated to projects.
Enforcement models will behave differently depending on the existing limits,

View File

@ -346,6 +346,10 @@ class InvalidDomainConfig(Forbidden):
message_format = _("Invalid domain specific configuration: %(reason)s.")
class InvalidLimit(Forbidden):
message_format = _("Invalid resource limit: %(reason)s.")
class NotFound(Error):
message_format = _("Could not find: %(target)s.")
code = int(http_client.NOT_FOUND)

View File

@ -11,6 +11,7 @@
# 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 copy
from keystone.common import cache
from keystone.common import driver_hints
@ -18,8 +19,7 @@ from keystone.common import manager
from keystone.common import provider_api
import keystone.conf
from keystone import exception
from keystone.limit import models
from keystone.limit.models import base
CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
@ -36,9 +36,8 @@ class Manager(manager.Manager):
unified_limit_driver = CONF.unified_limit.driver
super(Manager, self).__init__(unified_limit_driver)
self.enforcement_model = models.get_enforcement_model_from_config(
CONF.unified_limit.enforcement_model
)
self.enforcement_model = base.load_driver(
CONF.unified_limit.enforcement_model)
def _assert_resource_exist(self, unified_limit, target):
try:
@ -64,8 +63,8 @@ class Manager(manager.Manager):
def get_model(self):
"""Return information of the configured enforcement model."""
return {
'name': self.enforcement_model.name,
'description': self.enforcement_model.description
'name': self.enforcement_model.NAME,
'description': self.enforcement_model.DESCRIPTION
}
def create_registered_limits(self, registered_limits):
@ -97,10 +96,14 @@ class Manager(manager.Manager):
def create_limits(self, limits):
for limit in limits:
self._assert_resource_exist(limit, 'limit')
self.enforcement_model.check_limit(copy.deepcopy(limits))
return self.driver.create_limits(limits)
def update_limit(self, limit_id, limit):
self._assert_resource_exist(limit, 'limit')
limit_ref = self.get_limit(limit_id)
limit_ref.update(limit)
self.enforcement_model.check_limit(copy.deepcopy([limit_ref]))
updated_limit = self.driver.update_limit(limit_id, limit)
self.get_limit.invalidate(self, updated_limit['id'])
return updated_limit

View File

@ -1,29 +0,0 @@
# 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.
from keystone.limit.models import flat
def get_enforcement_model_from_config(enforcement_model):
"""Factory that returns an enforcement model object based on configuration.
:param enforcement_model str: A string, usually from a configuration
option, representing the name of the
enforcement model
:returns: an `Model` object
"""
# NOTE(lbragstad): The configuration option set is strictly checked by the
# ``oslo.config`` object. If someone passes in a garbage value, it will
# fail before it gets to this point.
if enforcement_model == 'flat':
return flat.Model()

View File

@ -0,0 +1,56 @@
# 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 abc
import six
import stevedore
import keystone.conf
from keystone.i18n import _
CONF = keystone.conf.CONF
def load_driver(driver_name, *args):
namespace = 'keystone.unified_limit.model'
try:
driver_manager = stevedore.DriverManager(namespace,
driver_name,
invoke_on_load=True,
invoke_args=args)
return driver_manager.driver
except stevedore.exception.NoMatches:
msg = (_('Unable to find %(name)r driver in %(namespace)r.'))
raise ImportError(msg % {'name': driver_name, 'namespace': namespace})
@six.add_metaclass(abc.ABCMeta)
class ModelBase(object):
"""Interface for a limit model driver."""
NAME = None
DESCRIPTION = None
MAX_PROJECT_TREE_DEPTH = None
def check_limit(self, limits):
"""Check the new creating or updating limits if satisfy the model.
:param limits: A list of the limit objects need to be check.
:type limits: A list of the limits. Each limit is a dict that contains
"resource_limit", "resource_name", "project_id", "service_id" and
optional "region_id", "description".
:raises keystone.exception.InvalidLimit: If any of the input limits
doesn't satisfy the limit model.
"""
raise NotImplementedError()

View File

@ -10,13 +10,19 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystone.limit.models import base
# TODO(lbragstad): This should inherit from an abstract interface so that we
# ensure all models implement the same things.
class Model(object):
name = 'flat'
description = (
class FlatModel(base.ModelBase):
NAME = 'flat'
DESCRIPTION = (
'Limit enforcement and validation does not take project hierarchy '
'into consideration.'
)
MAX_PROJECT_TREE_DEPTH = None
def check_limit(self, limits):
# Flat limit model is not hierarchical, so don't need to check the
# value.
return

View File

@ -0,0 +1,128 @@
# Copyright 2018 Huawei
# 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.
from oslo_log import log
from keystone.common import driver_hints
from keystone.common import provider_api
from keystone import exception
from keystone.limit.models import base
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
class StrictTwoLevelModel(base.ModelBase):
NAME = 'strict_two_level'
DESCRIPTION = (
'This model requires project hierarchy never exceeds a depth of two'
)
MAX_PROJECT_TREE_DEPTH = 2
def _get_specified_limit_value(self, project_id, resource_name, service_id,
region_id, is_parent=True):
"""Get the specified limit value.
Try to give the resource limit first. If the specified limit is a
parent in a project tree and the resource limit value is None, get the
related registered limit value instead.
"""
hints = driver_hints.Hints()
hints.add_filter('project_id', project_id)
hints.add_filter('service_id', service_id)
hints.add_filter('resource_name', resource_name)
hints.add_filter('region_id', region_id)
limits = PROVIDERS.unified_limit_api.list_limits(hints)
limit_value = limits[0]['resource_limit'] if limits else None
if not limits and is_parent:
hints = driver_hints.Hints()
hints.add_filter('service_id', service_id)
hints.add_filter('resource_name', resource_name)
hints.add_filter('region_id', region_id)
limits = PROVIDERS.unified_limit_api.list_registered_limits(hints)
limit_value = limits[0]['default_limit'] if limits else None
return limit_value
def _check_limit(self, project_id, resource_name, resource_limit,
service_id, region_id, parent_id):
"""Check the specified limit value satisfies the related project tree.
1. Ensure the limit is smaller than its parent.
2. Ensure the limit is bigger than its children.
"""
if parent_id:
parent_limit_value = self._get_specified_limit_value(
parent_id, resource_name, service_id, region_id)
if parent_limit_value and resource_limit > parent_limit_value:
raise exception.InvalidLimit(
reason="Limit is bigger than parent.")
sub_projects = PROVIDERS.resource_api.list_projects_in_subtree(
project_id)
for sub_project in sub_projects:
sub_limit_value = self._get_specified_limit_value(
sub_project['id'], resource_name, service_id, region_id,
is_parent=False)
if sub_limit_value and resource_limit < sub_limit_value:
raise exception.InvalidLimit(
reason="Limit is smaller than child.")
def check_limit(self, limits):
"""Check the input limits satisfy the related project tree or not.
1. Ensure the input is legal.
2. Ensure the input will not break the exist limit tree.
"""
for limit in limits:
project_id = limit['project_id']
resource_name = limit['resource_name']
resource_limit = limit['resource_limit']
service_id = limit['service_id']
region_id = limit.get('region_id')
try:
parent_project = PROVIDERS.resource_api.list_project_parents(
project_id)[0]
if not parent_project['is_domain']:
parent_id = parent_project['id']
parent_limit = list(filter(
lambda x: x['project_id'] == parent_id, limits))
if parent_limit:
if resource_limit > parent_limit[0]['resource_limit']:
raise exception.InvalidLimit(
reason="The input hierarchy tree is invalid.")
# The limit's parent is in request body, no need to
# check the backend any more.
continue
else:
parent_id = None
self._check_limit(project_id, resource_name, resource_limit,
service_id, region_id, parent_id)
except exception.InvalidLimit:
error = ("The resource limit (project_id: %(project_id)s, "
"resource_name: %(resource_name)s, "
"resource_limit: %(resource_limit)s, "
"service_id: %(service_id)s, "
"region_id: %(region_id)s) doesn't satisfy "
"current hierarchy model.") % {
'project_id': project_id,
'resource_name': resource_name,
'resource_limit': resource_limit,
'service_id': service_id,
'region_id': region_id
}
LOG.error(error)
raise exception.InvalidLimit(reason=error)

View File

@ -770,3 +770,493 @@ class LimitsTestCase(test_v3.RestfulTestCase):
expected_status=http_client.OK)
limits = r.result['limits']
self.assertEqual(len(limits), 1)
class StrictTwoLevelLimitsTestCase(LimitsTestCase):
def setUp(self):
super(StrictTwoLevelLimitsTestCase, self).setUp()
# create two hierarchical projects trees for test.
# A D
# / \ / \
# B C E F
project_ref = {'project': {'name': 'A', 'enabled': True}}
response = self.post('/projects', body=project_ref)
self.project_A = response.json_body['project']
project_ref = {'project': {'name': 'B', 'enabled': True,
'parent_id': self.project_A['id']}}
response = self.post('/projects', body=project_ref)
self.project_B = response.json_body['project']
project_ref = {'project': {'name': 'C', 'enabled': True,
'parent_id': self.project_A['id']}}
response = self.post('/projects', body=project_ref)
self.project_C = response.json_body['project']
project_ref = {'project': {'name': 'D', 'enabled': True}}
response = self.post('/projects', body=project_ref)
self.project_D = response.json_body['project']
project_ref = {'project': {'name': 'E', 'enabled': True,
'parent_id': self.project_D['id']}}
response = self.post('/projects', body=project_ref)
self.project_E = response.json_body['project']
project_ref = {'project': {'name': 'F', 'enabled': True,
'parent_id': self.project_D['id']}}
response = self.post('/projects', body=project_ref)
self.project_F = response.json_body['project']
def config_overrides(self):
super(StrictTwoLevelLimitsTestCase, self).config_overrides()
self.config_fixture.config(group='unified_limit',
enforcement_model='strict_two_level')
def test_create_child_limit(self):
# when A is 20, success to create B to 15, C to 18.
# A,20 A,20
# / \ --> / \
# B C B,15 C,18
ref = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=20)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.CREATED)
ref = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=15)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.CREATED)
ref = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=18)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.CREATED)
def test_create_child_limit_break_hierarchical_tree(self):
# when A is 20, success to create B to 15, but fail to create C to 21.
# A,20 A,20
# / \ --> / \
# B C B,15 C
#
# A,20 A,20
# / \ -/-> / \
# B,15 C B,15 C,21
ref = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=20)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.CREATED)
ref = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=15)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.CREATED)
ref = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=21)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.FORBIDDEN)
def test_create_child_with_default_parent(self):
# If A is not set, the default value is 10 (from registered limit).
# success to create B to 5, but fail to create C to 11.
# A(10) A(10)
# / \ --> / \
# B C B,5 C
#
# A(10) A(10)
# / \ -/-> / \
# B,5 C B,5 C,11
ref = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=5)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.CREATED)
ref = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=11)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.FORBIDDEN)
def test_create_parent_limit(self):
# When B is 9 , success to set A to 12
# A A,12
# / \ --> / \
# B,9 C B,9 C
ref = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=9)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.CREATED)
ref = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=12)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.CREATED)
def test_create_parent_limit_break_hierarchical_tree(self):
# When B is 9 , fail to set A to 8
# A A,8
# / \ -/-> / \
# B,9 C B,9 C
ref = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=9)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.CREATED)
ref = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=8)
self.post(
'/limits',
body={'limits': [ref]},
expected_status=http_client.FORBIDDEN)
def test_create_multi_limits(self):
# success to create a tree in one request like:
# A,12 D,9
# / \ / \
# B,9 C,5 E,5 F,4
ref_A = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=12)
ref_B = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=9)
ref_C = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=5)
ref_D = unit.new_limit_ref(project_id=self.project_D['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=9)
ref_E = unit.new_limit_ref(project_id=self.project_E['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=5)
ref_F = unit.new_limit_ref(project_id=self.project_F['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=4)
self.post(
'/limits',
body={'limits': [ref_A, ref_B, ref_C, ref_D, ref_E, ref_F]},
expected_status=http_client.CREATED)
def test_create_multi_limits_invalid_input(self):
# fail to create a tree in one request like:
# A,12 D,9
# / \ / \
# B,9 C,5 E,5 F,10
# because F will break the second limit tree.
ref_A = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=12)
ref_B = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=9)
ref_C = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=5)
ref_D = unit.new_limit_ref(project_id=self.project_D['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=9)
ref_E = unit.new_limit_ref(project_id=self.project_E['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=5)
ref_F = unit.new_limit_ref(project_id=self.project_F['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=10)
self.post(
'/limits',
body={'limits': [ref_A, ref_B, ref_C, ref_D, ref_E, ref_F]},
expected_status=http_client.FORBIDDEN)
def test_create_multi_limits_break_hierarchical_tree(self):
# when there is some hierarchical_trees already like:
# A,12 D
# / \ / \
# B,9 C E,5 F
# fail to set C to 5 and D to 4 in one request like:
# A,12 D,4
# / \ / \
# B,9 C,5 E,5 F
# because D will break the second limit tree.
ref_A = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=12)
ref_B = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=9)
ref_E = unit.new_limit_ref(project_id=self.project_E['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=5)
self.post(
'/limits',
body={'limits': [ref_A, ref_B, ref_E]},
expected_status=http_client.CREATED)
ref_C = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=5)
ref_D = unit.new_limit_ref(project_id=self.project_D['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=4)
self.post(
'/limits',
body={'limits': [ref_C, ref_D]},
expected_status=http_client.FORBIDDEN)
def test_update_child_limit(self):
# Success to update C to 9
# A,10 A,10
# / \ --> / \
# B,6 C,7 B,6 C,9
ref_A = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=10)
ref_B = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=6)
ref_C = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=7)
self.post(
'/limits',
body={'limits': [ref_A, ref_B]},
expected_status=http_client.CREATED)
r = self.post(
'/limits',
body={'limits': [ref_C]},
expected_status=http_client.CREATED)
update_dict = {'resource_limit': 9}
self.patch(
'/limits/%s' % r.result['limits'][0]['id'],
body={'limit': update_dict},
expected_status=http_client.OK)
def test_update_child_limit_break_hierarchical_tree(self):
# Fail to update C to 11
# A,10 A,10
# / \ -/-> / \
# B,6 C,7 B,6 C,11
ref_A = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=10)
ref_B = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=6)
ref_C = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=7)
self.post(
'/limits',
body={'limits': [ref_A, ref_B]},
expected_status=http_client.CREATED)
r = self.post(
'/limits',
body={'limits': [ref_C]},
expected_status=http_client.CREATED)
update_dict = {'resource_limit': 11}
self.patch(
'/limits/%s' % r.result['limits'][0]['id'],
body={'limit': update_dict},
expected_status=http_client.FORBIDDEN)
def test_update_child_limit_with_default_parent(self):
# If A is not set, the default value is 10 (from registered limit).
# Success to update C to 9 but fail to update C to 11
# A,(10) A,(10)
# / \ --> / \
# B, C,7 B C,9
#
# A,(10) A,(10)
# / \ -/-> / \
# B, C,7 B C,11
ref_C = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=7)
r = self.post(
'/limits',
body={'limits': [ref_C]},
expected_status=http_client.CREATED)
update_dict = {'resource_limit': 9}
self.patch(
'/limits/%s' % r.result['limits'][0]['id'],
body={'limit': update_dict},
expected_status=http_client.OK)
update_dict = {'resource_limit': 11}
self.patch(
'/limits/%s' % r.result['limits'][0]['id'],
body={'limit': update_dict},
expected_status=http_client.FORBIDDEN)
def test_update_parent_limit(self):
# Success to update A to 8
# A,10 A,8
# / \ --> / \
# B,6 C,7 B,6 C,7
ref_A = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=10)
ref_B = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=6)
ref_C = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=7)
r = self.post(
'/limits',
body={'limits': [ref_A]},
expected_status=http_client.CREATED)
self.post(
'/limits',
body={'limits': [ref_B, ref_C]},
expected_status=http_client.CREATED)
update_dict = {'resource_limit': 8}
self.patch(
'/limits/%s' % r.result['limits'][0]['id'],
body={'limit': update_dict},
expected_status=http_client.OK)
def test_update_parent_limit_break_hierarchical_tree(self):
# Fail to update A to 6
# A,10 A,6
# / \ -/-> / \
# B,6 C,7 B,6 C,7
ref_A = unit.new_limit_ref(project_id=self.project_A['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=10)
ref_B = unit.new_limit_ref(project_id=self.project_B['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=6)
ref_C = unit.new_limit_ref(project_id=self.project_C['id'],
service_id=self.service_id,
region_id=self.region_id,
resource_name='volume',
resource_limit=7)
r = self.post(
'/limits',
body={'limits': [ref_A]},
expected_status=http_client.CREATED)
self.post(
'/limits',
body={'limits': [ref_B, ref_C]},
expected_status=http_client.CREATED)
update_dict = {'resource_limit': 6}
self.patch(
'/limits/%s' % r.result['limits'][0]['id'],
body={'limit': update_dict},
expected_status=http_client.FORBIDDEN)

View File

@ -0,0 +1,15 @@
---
features:
- >
[`blueprint strict-two-level-model <https://blueprints.launchpad.net/keystone/+spec/strict-two-level-model>`_]
A new limit enforcement model called `strict_two_level` is added. Change the
value of the option `[unified_limit]/enforcement_model` to
`strict_two_level` to enable it.
In this [`model <http://specs.openstack.org/openstack/keystone-specs/specs/keystone/rocky/strict-two-level-enforcement-model.html>`_]:
1. The project depth is force limited to 2 level.
2. Any child project's limit can not exceed his parent's.
Please ensure that the previous project and limit structure deployment in
your Keystone won't break this model before starting to use it.

View File

@ -169,6 +169,10 @@ keystone.revoke =
keystone.application_credential =
sql = keystone.application_credential.backends.sql:ApplicationCredential
keystone.unified_limit.model =
flat = keystone.limit.models.flat:FlatModel
strict_two_level = keystone.limit.models.strict_two_level:StrictTwoLevelModel
oslo.config.opts =
keystone = keystone.conf.opts:list_opts