diff --git a/keystone/conf/unified_limit.py b/keystone/conf/unified_limit.py index 1f9559a828..06939d5aef 100644 --- a/keystone/conf/unified_limit.py +++ b/keystone/conf/unified_limit.py @@ -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, diff --git a/keystone/exception.py b/keystone/exception.py index cac8ba7ffb..5c77ed1fe5 100644 --- a/keystone/exception.py +++ b/keystone/exception.py @@ -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) diff --git a/keystone/limit/core.py b/keystone/limit/core.py index 61021c81cd..3c2109d55e 100644 --- a/keystone/limit/core.py +++ b/keystone/limit/core.py @@ -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 diff --git a/keystone/limit/models/__init__.py b/keystone/limit/models/__init__.py index 5fc936fd43..e69de29bb2 100644 --- a/keystone/limit/models/__init__.py +++ b/keystone/limit/models/__init__.py @@ -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() diff --git a/keystone/limit/models/base.py b/keystone/limit/models/base.py new file mode 100644 index 0000000000..1b3aaaf872 --- /dev/null +++ b/keystone/limit/models/base.py @@ -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() diff --git a/keystone/limit/models/flat.py b/keystone/limit/models/flat.py index 68a864236a..c20516059a 100644 --- a/keystone/limit/models/flat.py +++ b/keystone/limit/models/flat.py @@ -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 diff --git a/keystone/limit/models/strict_two_level.py b/keystone/limit/models/strict_two_level.py new file mode 100644 index 0000000000..95085bed99 --- /dev/null +++ b/keystone/limit/models/strict_two_level.py @@ -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) diff --git a/keystone/tests/unit/test_limits.py b/keystone/tests/unit/test_limits.py index 29ac0611ed..ff6d2f1a8c 100644 --- a/keystone/tests/unit/test_limits.py +++ b/keystone/tests/unit/test_limits.py @@ -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) diff --git a/releasenotes/notes/bp-strict-two-level-model.yaml b/releasenotes/notes/bp-strict-two-level-model.yaml new file mode 100644 index 0000000000..964fa4ad81 --- /dev/null +++ b/releasenotes/notes/bp-strict-two-level-model.yaml @@ -0,0 +1,15 @@ +--- +features: + - > + [`blueprint 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 `_]: + + 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. diff --git a/setup.cfg b/setup.cfg index 2137e7ad52..662d6f2d1c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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