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:
parent
899e691c9f
commit
4b4835a01c
|
@ -50,7 +50,7 @@ deployment.
|
||||||
enforcement_model = cfg.StrOpt(
|
enforcement_model = cfg.StrOpt(
|
||||||
'enforcement_model',
|
'enforcement_model',
|
||||||
default='flat',
|
default='flat',
|
||||||
choices=['flat'],
|
choices=['flat', 'strict_two_level'],
|
||||||
help=utils.fmt("""
|
help=utils.fmt("""
|
||||||
The enforcement model to use when validating limits associated to projects.
|
The enforcement model to use when validating limits associated to projects.
|
||||||
Enforcement models will behave differently depending on the existing limits,
|
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.")
|
message_format = _("Invalid domain specific configuration: %(reason)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidLimit(Forbidden):
|
||||||
|
message_format = _("Invalid resource limit: %(reason)s.")
|
||||||
|
|
||||||
|
|
||||||
class NotFound(Error):
|
class NotFound(Error):
|
||||||
message_format = _("Could not find: %(target)s.")
|
message_format = _("Could not find: %(target)s.")
|
||||||
code = int(http_client.NOT_FOUND)
|
code = int(http_client.NOT_FOUND)
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import copy
|
||||||
|
|
||||||
from keystone.common import cache
|
from keystone.common import cache
|
||||||
from keystone.common import driver_hints
|
from keystone.common import driver_hints
|
||||||
|
@ -18,8 +19,7 @@ from keystone.common import manager
|
||||||
from keystone.common import provider_api
|
from keystone.common import provider_api
|
||||||
import keystone.conf
|
import keystone.conf
|
||||||
from keystone import exception
|
from keystone import exception
|
||||||
from keystone.limit import models
|
from keystone.limit.models import base
|
||||||
|
|
||||||
|
|
||||||
CONF = keystone.conf.CONF
|
CONF = keystone.conf.CONF
|
||||||
PROVIDERS = provider_api.ProviderAPIs
|
PROVIDERS = provider_api.ProviderAPIs
|
||||||
|
@ -36,9 +36,8 @@ class Manager(manager.Manager):
|
||||||
unified_limit_driver = CONF.unified_limit.driver
|
unified_limit_driver = CONF.unified_limit.driver
|
||||||
super(Manager, self).__init__(unified_limit_driver)
|
super(Manager, self).__init__(unified_limit_driver)
|
||||||
|
|
||||||
self.enforcement_model = models.get_enforcement_model_from_config(
|
self.enforcement_model = base.load_driver(
|
||||||
CONF.unified_limit.enforcement_model
|
CONF.unified_limit.enforcement_model)
|
||||||
)
|
|
||||||
|
|
||||||
def _assert_resource_exist(self, unified_limit, target):
|
def _assert_resource_exist(self, unified_limit, target):
|
||||||
try:
|
try:
|
||||||
|
@ -64,8 +63,8 @@ class Manager(manager.Manager):
|
||||||
def get_model(self):
|
def get_model(self):
|
||||||
"""Return information of the configured enforcement model."""
|
"""Return information of the configured enforcement model."""
|
||||||
return {
|
return {
|
||||||
'name': self.enforcement_model.name,
|
'name': self.enforcement_model.NAME,
|
||||||
'description': self.enforcement_model.description
|
'description': self.enforcement_model.DESCRIPTION
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_registered_limits(self, registered_limits):
|
def create_registered_limits(self, registered_limits):
|
||||||
|
@ -97,10 +96,14 @@ class Manager(manager.Manager):
|
||||||
def create_limits(self, limits):
|
def create_limits(self, limits):
|
||||||
for limit in limits:
|
for limit in limits:
|
||||||
self._assert_resource_exist(limit, 'limit')
|
self._assert_resource_exist(limit, 'limit')
|
||||||
|
self.enforcement_model.check_limit(copy.deepcopy(limits))
|
||||||
return self.driver.create_limits(limits)
|
return self.driver.create_limits(limits)
|
||||||
|
|
||||||
def update_limit(self, limit_id, limit):
|
def update_limit(self, limit_id, limit):
|
||||||
self._assert_resource_exist(limit, '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)
|
updated_limit = self.driver.update_limit(limit_id, limit)
|
||||||
self.get_limit.invalidate(self, updated_limit['id'])
|
self.get_limit.invalidate(self, updated_limit['id'])
|
||||||
return updated_limit
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# 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'
|
class FlatModel(base.ModelBase):
|
||||||
description = (
|
|
||||||
|
NAME = 'flat'
|
||||||
|
DESCRIPTION = (
|
||||||
'Limit enforcement and validation does not take project hierarchy '
|
'Limit enforcement and validation does not take project hierarchy '
|
||||||
'into consideration.'
|
'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)
|
expected_status=http_client.OK)
|
||||||
limits = r.result['limits']
|
limits = r.result['limits']
|
||||||
self.assertEqual(len(limits), 1)
|
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 =
|
keystone.application_credential =
|
||||||
sql = keystone.application_credential.backends.sql:ApplicationCredential
|
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 =
|
oslo.config.opts =
|
||||||
keystone = keystone.conf.opts:list_opts
|
keystone = keystone.conf.opts:list_opts
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue