Merge "Strict two level limit model"
This commit is contained in:
commit
9541dae228
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue