Provide config option to limit resources per stack

This provides an upper bounds on the number of resources a root level
stack can contain. The limitation is only applied to the engine creation
point so that existing stacks that are over the limit in the database
will not cause problems. Nested stacks will be addressed in a follow-up
patch.

Partial-Bug: #1215100
Change-Id: I1adcb22cf9bd5750b4ae3f219dd3264d1d02c1fc
This commit is contained in:
Clint Byrum 2013-09-05 15:07:47 -07:00
parent f00272b3b8
commit ecf3954d23
5 changed files with 78 additions and 2 deletions

View File

@ -31,6 +31,10 @@
# Subset of trustor roles to be delegated to heat (list value) # Subset of trustor roles to be delegated to heat (list value)
#trusts_delegated_roles=heat_stack_owner #trusts_delegated_roles=heat_stack_owner
# Maximum resources allowed per top-level stack. (integer
# value)
#max_resources_per_stack=1000
# Name of the engine node. This can be an opaque identifier.It # Name of the engine node. This can be an opaque identifier.It
# is not necessarily a hostname, FQDN, or IP address. (string # is not necessarily a hostname, FQDN, or IP address. (string
# value) # value)

View File

@ -97,8 +97,10 @@ engine_opts = [
'stored password or trusts')), 'stored password or trusts')),
cfg.ListOpt('trusts_delegated_roles', cfg.ListOpt('trusts_delegated_roles',
default=['heat_stack_owner'], default=['heat_stack_owner'],
help=_('Subset of trustor roles to be delegated to heat'))] help=_('Subset of trustor roles to be delegated to heat')),
cfg.IntOpt('max_resources_per_stack',
default=1000,
help='Maximum resources allowed per top-level stack.')]
rpc_opts = [ rpc_opts = [
cfg.StrOpt('host', cfg.StrOpt('host',

View File

@ -322,3 +322,7 @@ class InvalidContentType(HeatException):
class RequestLimitExceeded(HeatException): class RequestLimitExceeded(HeatException):
message = _('Request limit exceeded: %(message)s') message = _('Request limit exceeded: %(message)s')
class StackResourceLimitExceeded(HeatException):
message = _('Maximum resources per stack exceeded.')

View File

@ -19,6 +19,8 @@ import json
from oslo.config import cfg from oslo.config import cfg
import webob import webob
cfg.CONF.import_opt('max_resources_per_stack', 'heat.common.config')
from heat.openstack.common import timeutils from heat.openstack.common import timeutils
from heat.common import context from heat.common import context
from heat.db import api as db_api from heat.db import api as db_api
@ -36,6 +38,7 @@ from heat.engine import parser
from heat.engine import properties from heat.engine import properties
from heat.engine import resource from heat.engine import resource
from heat.engine import resources from heat.engine import resources
from heat.engine import template as tpl
from heat.engine import watchrule from heat.engine import watchrule
from heat.openstack.common import log as logging from heat.openstack.common import log as logging
@ -249,6 +252,9 @@ class EngineService(service.Service):
tmpl = parser.Template(template, files=files) tmpl = parser.Template(template, files=files)
if len(tmpl[tpl.RESOURCES]) > cfg.CONF.max_resources_per_stack:
raise exception.StackResourceLimitExceeded()
# Extract the common query parameters # Extract the common query parameters
common_params = api.extract_args(args) common_params = api.extract_args(args)
env = environment.Environment(params) env = environment.Environment(params)

View File

@ -34,6 +34,7 @@ from heat.engine import parser
from heat.engine.resource import _register_class from heat.engine.resource import _register_class
from heat.engine import service from heat.engine import service
from heat.engine.properties import Properties from heat.engine.properties import Properties
from heat.engine import resource as res
from heat.engine.resources import instance as instances from heat.engine.resources import instance as instances
from heat.engine.resources import nova_utils from heat.engine.resources import nova_utils
from heat.engine import resource as rsrs from heat.engine import resource as rsrs
@ -413,6 +414,65 @@ class StackServiceCreateUpdateDeleteTest(HeatTestCase):
self.assertEqual( self.assertEqual(
'Missing required credential: X-Auth-User', ex.message) 'Missing required credential: X-Auth-User', ex.message)
def test_stack_create_total_resources_equals_max(self):
stack_name = 'service_create_stack_total_resources_equals_max'
params = {}
res._register_class('GenericResourceType',
generic_rsrc.GenericResource)
tpl = {'Resources': {
'A': {'Type': 'GenericResourceType'},
'B': {'Type': 'GenericResourceType'},
'C': {'Type': 'GenericResourceType'}}}
template = parser.Template(tpl)
stack = parser.Stack(self.ctx, stack_name, template,
environment.Environment({}))
self.m.StubOutWithMock(parser, 'Template')
self.m.StubOutWithMock(environment, 'Environment')
self.m.StubOutWithMock(parser, 'Stack')
parser.Template(template, files=None).AndReturn(stack.t)
environment.Environment(params).AndReturn(stack.env)
parser.Stack(self.ctx, stack.name,
stack.t,
stack.env).AndReturn(stack)
self.m.StubOutClassWithMocks(hkc.kc, "Client")
mock_ks_client = hkc.kc.Client(
auth_url=mox.IgnoreArg(),
tenant_name='test_tenant',
token='abcd1234')
mock_ks_client.authenticate().AndReturn(True)
self.m.StubOutWithMock(hkc.KeystoneClient, 'create_trust_context')
hkc.KeystoneClient.create_trust_context().AndReturn(None)
self.m.ReplayAll()
cfg.CONF.set_override('max_resources_per_stack', 3)
result = self.man.create_stack(self.ctx, stack_name, template, params,
None, {})
self.m.VerifyAll()
self.assertEquals(stack.identifier(), result)
self.assertEquals(3, stack.total_resources())
def test_stack_create_total_resources_exceeds_max(self):
stack_name = 'service_create_stack_total_resources_exceeds_max'
params = {}
res._register_class('GenericResourceType',
generic_rsrc.GenericResource)
tpl = {'Resources': {
'A': {'Type': 'GenericResourceType'},
'B': {'Type': 'GenericResourceType'},
'C': {'Type': 'GenericResourceType'}}}
template = parser.Template(tpl)
cfg.CONF.set_override('max_resources_per_stack', 2)
self.assertRaises(exception.StackResourceLimitExceeded,
self.man.create_stack, self.ctx, stack_name,
template, params, None, {})
def test_stack_validate(self): def test_stack_validate(self):
stack_name = 'service_create_test_validate' stack_name = 'service_create_test_validate'
stack = get_wordpress_stack(stack_name, self.ctx) stack = get_wordpress_stack(stack_name, self.ctx)